mandrel 2.56.0 → 2.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/plan-critic.md +13 -18
- package/.agents/agents/story-worker.md +25 -33
- package/.agents/docs/agentrc-reference.json +0 -30
- package/.agents/docs/configuration.md +8 -28
- package/.agents/docs/execution-reference.md +5 -5
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/instructions.md +9 -10
- package/.agents/schemas/agentrc.schema.json +9 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- package/.agents/scripts/ceremony-derive.js +191 -0
- package/.agents/scripts/check-context-budget.js +28 -33
- package/.agents/scripts/check-cyclomatic.js +4 -3
- package/.agents/scripts/deliver-light.js +31 -94
- package/.agents/scripts/evidence-gate.js +17 -1
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
- package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
- package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
- package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
- package/.agents/scripts/lib/close-validation/gates.js +52 -1
- package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
- package/.agents/scripts/lib/config/delivery-routing.js +7 -33
- package/.agents/scripts/lib/config/explain.js +0 -19
- package/.agents/scripts/lib/config/limits.js +18 -78
- package/.agents/scripts/lib/config/quality.js +6 -3
- package/.agents/scripts/lib/config/runners.js +3 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
- package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
- package/.agents/scripts/lib/config-settings-schema.js +16 -143
- package/.agents/scripts/lib/crap-engine.js +35 -4
- package/.agents/scripts/lib/crap-utils.js +17 -1
- package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
- package/.agents/scripts/lib/orchestration/code-review.js +7 -3
- package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
- package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
- package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
- package/.agents/scripts/lib/orchestration/pinned-identifier-lint.js +137 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +189 -387
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
- package/.agents/scripts/lib/orchestration/plan-persist/acceptance-handle-repair.js +107 -0
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +305 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +138 -170
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +128 -297
- package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +36 -135
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
- package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
- package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
- package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
- package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
- package/.agents/scripts/lib/story-body/story-body.js +54 -240
- package/.agents/scripts/lib/templates/decomposer-prompts.js +133 -121
- package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
- package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
- package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
- package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
- package/.agents/scripts/lib/test-run-credit.js +277 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
- package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
- package/.agents/scripts/lib/workers/crap-worker.js +32 -41
- package/.agents/scripts/plan-context.js +7 -9
- package/.agents/scripts/plan-critics.js +28 -54
- package/.agents/scripts/plan-persist.js +25 -68
- package/.agents/scripts/quality-preview.js +51 -0
- package/.agents/scripts/run-tests.js +12 -0
- package/.agents/scripts/stories-wave-tick.js +23 -45
- package/.agents/scripts/test-isolate.js +13 -180
- package/.agents/scripts/update-coverage-baseline.js +25 -70
- package/.agents/scripts/update-crap-baseline.js +19 -123
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/workflows/audit-clean-code.md +4 -3
- package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
- package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
- package/.agents/workflows/helpers/code-review.md +2 -3
- package/.agents/workflows/helpers/deliver-digest.md +46 -55
- package/.agents/workflows/helpers/deliver-light.md +40 -105
- package/.agents/workflows/helpers/deliver-reference.md +1 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +54 -55
- package/.agents/workflows/helpers/deliver-story.md +10 -13
- package/.agents/workflows/helpers/plan-reference.md +163 -221
- package/.agents/workflows/mandrel-plan.md +31 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +36 -0
- package/lib/cli/registry.js +98 -2
- package/lib/migrations/index.js +4 -0
- package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
- package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/framework-version.js +0 -39
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
- package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
- package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
|
@@ -76,7 +76,7 @@ export const RUNTIME_FRICTION_CATEGORIES = Object.freeze({
|
|
|
76
76
|
*/
|
|
77
77
|
TOOL_DEGRADED: 'tool-degraded',
|
|
78
78
|
/**
|
|
79
|
-
* The light delivery path refused a scope — a suitability-gate
|
|
79
|
+
* The light delivery path refused a scope — a suitability-gate escalation
|
|
80
80
|
* or a blocked diff backstop. Story #4856 added it because neither rejection
|
|
81
81
|
* emitted anything, so an over-tight ceiling could only reach the framework
|
|
82
82
|
* as anecdote; the roll-up aggregating these by category is what makes the
|
|
@@ -107,6 +107,7 @@ const FRAMEWORK_SCRIPT_BASENAMES = Object.freeze([
|
|
|
107
107
|
'check-workflow-citations.js',
|
|
108
108
|
'check-workflow-cli-lint.js',
|
|
109
109
|
'check-workflow-timeouts.js',
|
|
110
|
+
'ceremony-derive.js',
|
|
110
111
|
'cleanup-repo-test-temp.js',
|
|
111
112
|
'coverage-capture.js',
|
|
112
113
|
'deliver-light.js',
|
|
@@ -43,11 +43,12 @@
|
|
|
43
43
|
* The Story escalates to `agent::blocked`; it never
|
|
44
44
|
* silently proceeds to close.
|
|
45
45
|
*
|
|
46
|
-
* ## The
|
|
46
|
+
* ## The cap
|
|
47
47
|
*
|
|
48
|
-
* `maxRounds` arrives
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* `maxRounds` arrives normalized by `lib/config/acceptance-eval.js` as a
|
|
49
|
+
* non-negative integer (Story #5313: `0` means "score once, no redraft").
|
|
50
|
+
* This reducer maps that onto an effective cap of scored rounds: a cap of
|
|
51
|
+
* `0`, a negative value or a non-integer is coerced to 1, so there is no
|
|
51
52
|
* input — config or verdict — that yields an unbounded `redraft` chain.
|
|
52
53
|
* When `round >= effectiveCap` and criteria remain unmet, the only
|
|
53
54
|
* possible action is `block`.
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
*
|
|
20
20
|
* ## Ceremony profiles (`delivery.routing.ceremonyProfile`)
|
|
21
21
|
*
|
|
22
|
-
* - `minimal` — always `inline` (
|
|
22
|
+
* - `minimal` — always `inline` (no fresh critic spawn).
|
|
23
23
|
* Use for tiny N=1 Stories the operator trusts.
|
|
24
|
-
* - `standard` — level-routed (default). Low → inline
|
|
25
|
-
*
|
|
24
|
+
* - `standard` — level-routed (default). Low → inline; high or
|
|
25
|
+
* underivable → fresh.
|
|
26
26
|
* - `strict` — always `fresh` regardless of the derived level.
|
|
27
27
|
*
|
|
28
28
|
* ## The load-bearing invariant (M4-B acceptance floor — DO NOT VIOLATE)
|
|
@@ -51,9 +51,7 @@
|
|
|
51
51
|
*
|
|
52
52
|
* - `high` level → `fresh` (a sensitive path was touched — a
|
|
53
53
|
* fresh-context maker-blind spawn).
|
|
54
|
-
* - `low` level → `inline` (the contract-identical inline critic)
|
|
55
|
-
* UNLESS the maker-checker sampling floor
|
|
56
|
-
* selects this cluster → `fresh`.
|
|
54
|
+
* - `low` level → `inline` (the contract-identical inline critic).
|
|
57
55
|
* - missing / unknown → `fresh` (fail-safe: the diff could not be
|
|
58
56
|
* enumerated, so there is no evidence the
|
|
59
57
|
* change is unremarkable; treat it as
|
|
@@ -61,15 +59,14 @@
|
|
|
61
59
|
* exactly as `resolveDepth` degrades to
|
|
62
60
|
* `standard` on the same signal).
|
|
63
61
|
*
|
|
64
|
-
* ##
|
|
62
|
+
* ## No sampling floor (Story #5313)
|
|
65
63
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* ignore the sampling floor.
|
|
64
|
+
* The maker-checker sampling floor (`delivery.routing.freshCriticSampleRate`,
|
|
65
|
+
* `sampledFresh`) bounded independent checking by a cluster-index stride —
|
|
66
|
+
* a count, not a risk signal — and was retired with the delivery diet. The
|
|
67
|
+
* standard profile now routes purely off the derived level, so the decision
|
|
68
|
+
* carries no `sampled` field and `clusterIndex` is accepted only for call-site
|
|
69
|
+
* compatibility (it never changes the outcome).
|
|
73
70
|
*
|
|
74
71
|
* Pure and total: inputs in, decision out. No I/O, no throws. `null` /
|
|
75
72
|
* `undefined` / malformed inputs degrade to `fresh` + `full` ceremony.
|
|
@@ -119,49 +116,19 @@ function normalizeCeremonyProfile(value) {
|
|
|
119
116
|
}
|
|
120
117
|
|
|
121
118
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* `round(1 / r)` and every `stride`-th cluster (0-based indices 0, stride,
|
|
126
|
-
* 2·stride, …) is forced fresh, yielding ≈`r` of clusters fresh. `r <= 0`
|
|
127
|
-
* disables the floor (no cluster forced); `r >= 1` forces every cluster.
|
|
128
|
-
*
|
|
129
|
-
* @param {number} clusterIndex Zero-based cluster position (from the
|
|
130
|
-
* caller-owned fan-out — an INPUT, never mutated here).
|
|
131
|
-
* @param {number} rate Sampling rate, already clamped into [0, 1] by
|
|
132
|
-
* `getDeliveryRouting`.
|
|
133
|
-
* @returns {boolean} `true` when the floor forces this cluster fresh.
|
|
134
|
-
*/
|
|
135
|
-
export function sampledFresh(clusterIndex, rate) {
|
|
136
|
-
if (typeof rate !== 'number' || !Number.isFinite(rate) || rate <= 0) {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
if (rate >= 1) return true;
|
|
140
|
-
const idx =
|
|
141
|
-
typeof clusterIndex === 'number' &&
|
|
142
|
-
Number.isInteger(clusterIndex) &&
|
|
143
|
-
clusterIndex >= 0
|
|
144
|
-
? clusterIndex
|
|
145
|
-
: 0;
|
|
146
|
-
const stride = Math.max(1, Math.round(1 / rate));
|
|
147
|
-
return idx % stride === 0;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Resolve the acceptance ceremony for one cluster from the ceremony profile,
|
|
152
|
-
* the derived change level, and the maker-checker sampling floor. See the
|
|
153
|
-
* module header for the tier rules and the untouchable cluster-count invariant.
|
|
119
|
+
* Resolve the acceptance ceremony for one cluster from the ceremony profile
|
|
120
|
+
* and the derived change level. See the module header for the tier rules and
|
|
121
|
+
* the untouchable cluster-count invariant.
|
|
154
122
|
*
|
|
155
123
|
* @param {{
|
|
156
124
|
* derivedLevel?: (ChangeLevel|string|null|undefined),
|
|
157
125
|
* clusterIndex?: (number|null|undefined),
|
|
158
|
-
* freshCriticSampleRate?: (number|null|undefined),
|
|
159
126
|
* ceremonyProfile?: (CeremonyProfile|string|null|undefined),
|
|
160
|
-
* }} [input]
|
|
127
|
+
* }} [input] `clusterIndex` is accepted for call-site compatibility only
|
|
128
|
+
* (Story #5313 retired the sampling floor that read it).
|
|
161
129
|
* @returns {{
|
|
162
130
|
* mode: CeremonyMode,
|
|
163
131
|
* reason: string,
|
|
164
|
-
* sampled: boolean,
|
|
165
132
|
* profile: CeremonyProfile,
|
|
166
133
|
* verdictOwner: VerdictOwner,
|
|
167
134
|
* }}
|
|
@@ -172,27 +139,20 @@ export function resolveCeremonyForRisk(input = {}) {
|
|
|
172
139
|
}
|
|
173
140
|
|
|
174
141
|
/**
|
|
175
|
-
* Internal mode/reason resolution — the tier rules
|
|
176
|
-
*
|
|
177
|
-
*
|
|
142
|
+
* Internal mode/reason resolution — the tier rules. `resolveCeremonyForRisk`
|
|
143
|
+
* decorates the result with the single `verdictOwner` derived from the mode
|
|
144
|
+
* (Story #4723).
|
|
178
145
|
*
|
|
179
146
|
* @param {Parameters<typeof resolveCeremonyForRisk>[0]} [input]
|
|
180
147
|
* @returns {{
|
|
181
148
|
* mode: CeremonyMode,
|
|
182
149
|
* reason: string,
|
|
183
|
-
* sampled: boolean,
|
|
184
150
|
* profile: CeremonyProfile,
|
|
185
151
|
* }}
|
|
186
152
|
*/
|
|
187
153
|
function resolveCeremonyDecision(input = {}) {
|
|
188
154
|
const derivedLevel =
|
|
189
155
|
input && typeof input === 'object' ? input.derivedLevel : undefined;
|
|
190
|
-
const clusterIndex =
|
|
191
|
-
input && typeof input === 'object' ? input.clusterIndex : undefined;
|
|
192
|
-
const rate =
|
|
193
|
-
input && typeof input === 'object'
|
|
194
|
-
? input.freshCriticSampleRate
|
|
195
|
-
: undefined;
|
|
196
156
|
const profile = normalizeCeremonyProfile(
|
|
197
157
|
input && typeof input === 'object' ? input.ceremonyProfile : undefined,
|
|
198
158
|
);
|
|
@@ -201,7 +161,6 @@ function resolveCeremonyDecision(input = {}) {
|
|
|
201
161
|
return {
|
|
202
162
|
mode: 'inline',
|
|
203
163
|
reason: 'ceremonyProfile=minimal: inline critic (no fresh spawn)',
|
|
204
|
-
sampled: false,
|
|
205
164
|
profile,
|
|
206
165
|
};
|
|
207
166
|
}
|
|
@@ -209,7 +168,6 @@ function resolveCeremonyDecision(input = {}) {
|
|
|
209
168
|
return {
|
|
210
169
|
mode: 'fresh',
|
|
211
170
|
reason: 'ceremonyProfile=strict: fresh-context critic',
|
|
212
|
-
sampled: false,
|
|
213
171
|
profile,
|
|
214
172
|
};
|
|
215
173
|
}
|
|
@@ -218,24 +176,13 @@ function resolveCeremonyDecision(input = {}) {
|
|
|
218
176
|
return {
|
|
219
177
|
mode: 'fresh',
|
|
220
178
|
reason: 'sensitive path touched: fresh-context critic',
|
|
221
|
-
sampled: false,
|
|
222
179
|
profile,
|
|
223
180
|
};
|
|
224
181
|
}
|
|
225
182
|
if (derivedLevel === 'low') {
|
|
226
|
-
if (sampledFresh(clusterIndex, rate)) {
|
|
227
|
-
return {
|
|
228
|
-
mode: 'fresh',
|
|
229
|
-
reason:
|
|
230
|
-
'low-level cluster forced fresh by the maker-checker sampling floor',
|
|
231
|
-
sampled: true,
|
|
232
|
-
profile,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
183
|
return {
|
|
236
184
|
mode: 'inline',
|
|
237
185
|
reason: 'no sensitive path touched: contract-identical inline critic',
|
|
238
|
-
sampled: false,
|
|
239
186
|
profile,
|
|
240
187
|
};
|
|
241
188
|
}
|
|
@@ -245,7 +192,6 @@ function resolveCeremonyDecision(input = {}) {
|
|
|
245
192
|
mode: 'fresh',
|
|
246
193
|
reason:
|
|
247
194
|
'change level underivable: fail-safe fresh-context critic + full ceremony',
|
|
248
|
-
sampled: false,
|
|
249
195
|
profile,
|
|
250
196
|
};
|
|
251
197
|
}
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
import { hasSurvivingCritical } from '../audit-suite/findings.js';
|
|
43
43
|
import { resolveConfig } from '../config-resolver.js';
|
|
44
44
|
import { computeChangeSet } from './change-set.js';
|
|
45
|
+
import { remoteBaseRef } from './review-base-ref.js';
|
|
45
46
|
import { deriveChangeLevel, resolveDepth } from './review-depth.js';
|
|
46
47
|
import {
|
|
47
48
|
collectProviderDegradations,
|
|
@@ -70,11 +71,14 @@ import { upsertStructuredComment } from './ticketing.js';
|
|
|
70
71
|
*/
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
|
-
* Resolve the
|
|
74
|
-
*
|
|
74
|
+
* Resolve the base ref used when a caller omits `baseRef`. Remote-qualified,
|
|
75
|
+
* never the bare branch name — a caller that does not name a base must not
|
|
76
|
+
* silently inherit the local ref's drift (Story #5325). An unfetched remote
|
|
77
|
+
* then yields an unenumerable diff, which every downstream consumer already
|
|
78
|
+
* fails safe on, instead of a confidently-wrong wide one.
|
|
75
79
|
*/
|
|
76
80
|
function resolveConfigBase(config) {
|
|
77
|
-
return config?.project?.baseBranch ?? 'main';
|
|
81
|
+
return remoteBaseRef(config?.project?.baseBranch ?? 'main');
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
/** Positive-integer override, else the supplied default. */
|
|
@@ -1,76 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/orchestration/complexity-gate.js — shape-derived
|
|
3
|
-
* (Story #4722
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* **
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* them **fails closed to `full`** (`run-plan-persist.js`). Artifact
|
|
31
|
-
* cardinality is deliberately not an axis (Story #4764).
|
|
32
|
-
* 4. **Deliver dispatches on topology alone.** The dispatch *mode*
|
|
33
|
-
* ({@link resolveStoryDispatchMode}) answers a different question from
|
|
34
|
-
* the route: may the engine run in the router's own session? Only a
|
|
35
|
-
* **single-Story run** may (Story #4736) — sub-agent isolation buys
|
|
36
|
-
* nothing when there is no concurrent sibling to isolate from. Shape
|
|
37
|
-
* cannot grant that session (Story #4829): a lite body makes work cheap,
|
|
38
|
-
* it does not conjure a second session for a sibling to run in. Story
|
|
39
|
-
* #5006 removed the shape derivation that survived there for reporting,
|
|
40
|
-
* since no consumer read it. The `route::lite` label is a
|
|
41
|
-
* **human-visible hint only**, never the control signal. Either way every
|
|
42
|
-
* `single-story-close.js` gate runs unchanged.
|
|
2
|
+
* lib/orchestration/complexity-gate.js — shape-derived Story routing for the
|
|
3
|
+
* deliver side (Story #4722; plan-side lite claim deleted by Story #5312).
|
|
4
|
+
*
|
|
5
|
+
* Three surfaces survive, all read at delivery time:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Seed signals ({@link buildComplexitySignals}).** `/mandrel-plan`'s
|
|
8
|
+
* context envelope carries the paths a seed predicts, their repo state
|
|
9
|
+
* (existing paths predict refactors; missing ones predict creates) and
|
|
10
|
+
* the `audit-rules.json` sensitive-path classes the footprint intersects.
|
|
11
|
+
* They ground the authoring template's `changes[]` skeleton and the
|
|
12
|
+
* `/prototype` offer; they route nothing.
|
|
13
|
+
* 2. **Story shape ({@link deriveStoryShape}).** The light path
|
|
14
|
+
* (`deliver-light`) reads a predicted footprint's effort and risk —
|
|
15
|
+
* distinct change kinds, declared magnitude, uncertainty,
|
|
16
|
+
* deployable/migration span, sensitive-path classes — against
|
|
17
|
+
* {@link STORY_SHAPE_CEILINGS} to decide whether a prompt may skip the
|
|
18
|
+
* Story-authoring ceremony. Artifact cardinality is deliberately not an
|
|
19
|
+
* axis (Story #4764).
|
|
20
|
+
* 3. **Dispatch mode ({@link resolveStoryDispatchMode}).** `/mandrel-deliver`
|
|
21
|
+
* answers a different question from the route: may the engine run in the
|
|
22
|
+
* router's own session? Only a **single-Story run** may (Story #4736).
|
|
23
|
+
*
|
|
24
|
+
* Story #5312 deleted the plan-side half: the planner's authored lite claim
|
|
25
|
+
* (`--route-downgrade-reason`), the persist-time shape backstop that
|
|
26
|
+
* validated it, the `route::lite` hint label, and the
|
|
27
|
+
* `planning.complexityGate` knobs. Persist no longer routes; every Story
|
|
28
|
+
* lands through the same engine and the same close gates, and the shape
|
|
29
|
+
* ceilings below are read only where a shape is actually decided on.
|
|
43
30
|
*
|
|
44
31
|
* The shape taxonomy is deliberately the one `review-depth.js` already
|
|
45
32
|
* applies to the landed diff at close (`deriveChangeLevel` over the
|
|
46
33
|
* `audit-rules.json` sensitive-path classes): **predicted shape at dispatch,
|
|
47
|
-
* actual diff at close** — one taxonomy, two read points.
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* The lite route collapses the **advisory ceremony** only: the story-worker
|
|
55
|
-
* sub-agent boot and the fresh acceptance-critic spawn. It **never** relaxes
|
|
56
|
-
* a non-negotiable. {@link LITE_PATH_INVARIANTS} is the machine-readable
|
|
57
|
-
* contract that the lite path still produces a Story ticket, still lands via
|
|
58
|
-
* a PR to `main`, still runs every repo quality gate, and still honours
|
|
34
|
+
* actual diff at close** — one taxonomy, two read points. Sensitivity always
|
|
35
|
+
* wins: a small change whose footprint intersects a sensitive-path class
|
|
36
|
+
* routes `full`, which keeps its fresh acceptance critic.
|
|
37
|
+
*
|
|
38
|
+
* {@link LITE_PATH_INVARIANTS} is the machine-readable contract that the
|
|
39
|
+
* light path still produces a Story ticket, still lands via a PR to `main`,
|
|
40
|
+
* still runs every repo quality gate, and still honours
|
|
59
41
|
* `rules/security-baseline.md`. Those gates run in `single-story-close.js`
|
|
60
42
|
* regardless of route; the router cannot and does not switch them off.
|
|
61
43
|
*
|
|
62
|
-
* ## Configuration
|
|
63
|
-
*
|
|
64
|
-
* Operators tune the surface via `planning.complexityGate` in `.agentrc.json`:
|
|
65
|
-
*
|
|
66
|
-
* - `enabled` (default `true`) — `false` disables lite routing
|
|
67
|
-
* everywhere: persist refuses lite claims and dispatch always takes the
|
|
68
|
-
* sub-agent path.
|
|
69
|
-
* - `maxArtifacts` (default `1`) — enumerated-artifact signal threshold;
|
|
70
|
-
* an **input signal** for the planner, no longer a deterministic router.
|
|
71
|
-
*
|
|
72
|
-
* `maxSeedWords` is **removed** (hard cutover): word count routes nothing.
|
|
73
|
-
*
|
|
74
44
|
* @typedef {'lite'|'full'} ComplexityRoute
|
|
75
45
|
*/
|
|
76
46
|
|
|
@@ -79,27 +49,6 @@ import path from 'node:path';
|
|
|
79
49
|
import { extractChangePaths } from '../story-body/story-body.js';
|
|
80
50
|
import { deriveChangeLevel } from './review-depth.js';
|
|
81
51
|
|
|
82
|
-
/**
|
|
83
|
-
* Framework defaults for the complexity-routing surface. The SSOT the config
|
|
84
|
-
* schema mirror and the configuration reference both cite. `maxSeedWords` is
|
|
85
|
-
* gone: seed word count carries no routing authority (Story #4722).
|
|
86
|
-
*/
|
|
87
|
-
const DEFAULT_COMPLEXITY_GATE = Object.freeze({
|
|
88
|
-
enabled: true,
|
|
89
|
-
maxArtifacts: 1,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* The persisted route marker for a lite-routed Story.
|
|
94
|
-
*
|
|
95
|
-
* **A human-visible hint only (Story #4722)** — never the control signal.
|
|
96
|
-
* Persist applies it so a lite cohort is filterable in the GitHub UI, and
|
|
97
|
-
* `deliver-light` reads the Story's own shape ({@link deriveStoryShape}) when
|
|
98
|
-
* it needs one. Nothing routes on the label: a lost label or an unread marker
|
|
99
|
-
* cannot misroute delivery.
|
|
100
|
-
*/
|
|
101
|
-
export const LITE_ROUTE_LABEL = 'route::lite';
|
|
102
|
-
|
|
103
52
|
/**
|
|
104
53
|
* Effort/risk ceilings a Story's work must fit for the `lite` route
|
|
105
54
|
* ({@link deriveStoryShape}). Framework constants, not operator knobs — a
|
|
@@ -378,54 +327,6 @@ const LITE_PATH_INVARIANTS = Object.freeze({
|
|
|
378
327
|
securityBaseline: true,
|
|
379
328
|
});
|
|
380
329
|
|
|
381
|
-
/**
|
|
382
|
-
* Coerce a candidate ceiling into a non-negative integer, falling back to the
|
|
383
|
-
* framework default for anything malformed — a stray `-1` or `NaN` must never
|
|
384
|
-
* widen the lite path (fail conservative).
|
|
385
|
-
*
|
|
386
|
-
* @param {unknown} value
|
|
387
|
-
* @param {number} fallback
|
|
388
|
-
* @returns {number}
|
|
389
|
-
*/
|
|
390
|
-
function normalizeCeiling(value, fallback) {
|
|
391
|
-
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
392
|
-
return fallback;
|
|
393
|
-
}
|
|
394
|
-
return Math.floor(value);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Resolve the effective complexity-gate config, shallow-overlaying an
|
|
399
|
-
* operator `planning.complexityGate` block onto
|
|
400
|
-
* {@link DEFAULT_COMPLEXITY_GATE}. Accepts the full resolved config, the bare
|
|
401
|
-
* `planning` bag, or the bare `complexityGate` bag, mirroring the tolerant
|
|
402
|
-
* unwrap the other routing accessors use.
|
|
403
|
-
*
|
|
404
|
-
* Exported for persist (`run-plan-persist.js#resolveEffectiveRoute`), which
|
|
405
|
-
* consults `enabled` to refuse a planner lite claim when the gate is off —
|
|
406
|
-
* the schema's documented contract. It is the only read point: Story #5006
|
|
407
|
-
* removed the second one in {@link resolveStoryDispatchMode}, where the switch
|
|
408
|
-
* gated a shape derivation whose result no consumer read.
|
|
409
|
-
*
|
|
410
|
-
* @param {object | null | undefined} config
|
|
411
|
-
* @returns {{ enabled: boolean, maxArtifacts: number }}
|
|
412
|
-
*/
|
|
413
|
-
export function resolveComplexityGate(config) {
|
|
414
|
-
const raw =
|
|
415
|
-
config?.planning?.complexityGate ?? config?.complexityGate ?? config ?? {};
|
|
416
|
-
const bag = raw && typeof raw === 'object' ? raw : {};
|
|
417
|
-
return {
|
|
418
|
-
enabled:
|
|
419
|
-
typeof bag.enabled === 'boolean'
|
|
420
|
-
? bag.enabled
|
|
421
|
-
: DEFAULT_COMPLEXITY_GATE.enabled,
|
|
422
|
-
maxArtifacts: normalizeCeiling(
|
|
423
|
-
bag.maxArtifacts,
|
|
424
|
-
DEFAULT_COMPLEXITY_GATE.maxArtifacts,
|
|
425
|
-
),
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
|
|
429
330
|
/**
|
|
430
331
|
* Count top-level enumerated items (`- `, `* `, `1. `) in a free-form seed —
|
|
431
332
|
* each enumerated line is one predicted artifact.
|
|
@@ -464,20 +365,17 @@ function extractPredictedPaths(text) {
|
|
|
464
365
|
}
|
|
465
366
|
|
|
466
367
|
/**
|
|
467
|
-
* Build the advisory complexity **signals** for a planning seed
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
* - `artifactCount` — enumerated items in the seed,
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
* pre-mortem critic uses).
|
|
479
|
-
* - `predictedPaths` / `repoState` — path-like tokens in the seed and
|
|
480
|
-
* which of them exist in the repo (existing
|
|
368
|
+
* Build the advisory complexity **signals** for a planning seed. Signals,
|
|
369
|
+
* not routing: the result carries `routingAuthority: false` and no `route`
|
|
370
|
+
* field — they ground the authoring template's pre-resolved `changes[]` and
|
|
371
|
+
* the `/prototype` offer, nothing else (Story #5312 deleted the risk-heuristic
|
|
372
|
+
* hits and the `planning.complexityGate` echo that used to ride alongside).
|
|
373
|
+
*
|
|
374
|
+
* - `artifactCount` — enumerated items in the seed, a rough width
|
|
375
|
+
* signal for the operator's eye only.
|
|
376
|
+
* - `predictedPaths` — path-like tokens the seed names, in order of
|
|
377
|
+
* first appearance (capped).
|
|
378
|
+
* - `repoState` — which predicted paths exist in the repo (existing
|
|
481
379
|
* paths predict refactors; missing predict
|
|
482
380
|
* creates).
|
|
483
381
|
* - `sensitivePathClasses` — `audit-rules.json` sensitive-path classes the
|
|
@@ -489,8 +387,6 @@ function extractPredictedPaths(text) {
|
|
|
489
387
|
*
|
|
490
388
|
* @param {{
|
|
491
389
|
* seedText?: string,
|
|
492
|
-
* config?: object,
|
|
493
|
-
* riskHeuristics?: string[],
|
|
494
390
|
* cwd?: string,
|
|
495
391
|
* pathExistsFn?: (absPath: string) => boolean,
|
|
496
392
|
* injectedRules?: object,
|
|
@@ -498,37 +394,21 @@ function extractPredictedPaths(text) {
|
|
|
498
394
|
* }} [args]
|
|
499
395
|
* @returns {{
|
|
500
396
|
* artifactCount: number,
|
|
501
|
-
* maxArtifacts: number,
|
|
502
|
-
* riskHeuristicHits: string[],
|
|
503
397
|
* predictedPaths: string[],
|
|
504
398
|
* repoState: { existingPaths: string[], missingPaths: string[] },
|
|
505
399
|
* sensitivePathClasses: string[],
|
|
506
|
-
* gate: { enabled: boolean },
|
|
507
400
|
* advisory: true,
|
|
508
401
|
* routingAuthority: false,
|
|
509
402
|
* }}
|
|
510
403
|
*/
|
|
511
404
|
export function buildComplexitySignals({
|
|
512
405
|
seedText = '',
|
|
513
|
-
config,
|
|
514
|
-
riskHeuristics = [],
|
|
515
406
|
cwd,
|
|
516
407
|
pathExistsFn = existsSync,
|
|
517
408
|
injectedRules,
|
|
518
409
|
selectSensitivePathClassesFn,
|
|
519
410
|
} = {}) {
|
|
520
|
-
const gate = resolveComplexityGate(config);
|
|
521
411
|
const text = typeof seedText === 'string' ? seedText : '';
|
|
522
|
-
const haystack = text.toLowerCase();
|
|
523
|
-
|
|
524
|
-
const riskHeuristicHits = (
|
|
525
|
-
Array.isArray(riskHeuristics) ? riskHeuristics : []
|
|
526
|
-
).filter(
|
|
527
|
-
(phrase) =>
|
|
528
|
-
typeof phrase === 'string' &&
|
|
529
|
-
phrase.trim().length > 0 &&
|
|
530
|
-
haystack.includes(phrase.trim().toLowerCase()),
|
|
531
|
-
);
|
|
532
412
|
|
|
533
413
|
const predictedPaths = extractPredictedPaths(text);
|
|
534
414
|
const root = typeof cwd === 'string' && cwd !== '' ? cwd : process.cwd();
|
|
@@ -552,60 +432,14 @@ export function buildComplexitySignals({
|
|
|
552
432
|
|
|
553
433
|
return {
|
|
554
434
|
artifactCount: countSeedArtifacts(text),
|
|
555
|
-
maxArtifacts: gate.maxArtifacts,
|
|
556
|
-
riskHeuristicHits,
|
|
557
435
|
predictedPaths,
|
|
558
436
|
repoState: { existingPaths, missingPaths },
|
|
559
437
|
sensitivePathClasses: classes,
|
|
560
|
-
gate: { enabled: gate.enabled },
|
|
561
438
|
advisory: /** @type {const} */ (true),
|
|
562
439
|
routingAuthority: /** @type {const} */ (false),
|
|
563
440
|
};
|
|
564
441
|
}
|
|
565
442
|
|
|
566
|
-
/**
|
|
567
|
-
* Resolve the planner's authored trivial-vs-standard verdict
|
|
568
|
-
* (Story #4722 AC-2, generalizing the former one-way `applyPlannerDowngrade`
|
|
569
|
-
* seam into the verdict itself).
|
|
570
|
-
*
|
|
571
|
-
* The planner — not a word count — owns the judgment, and the contract keeps
|
|
572
|
-
* it auditable: `lite` **only** with a non-empty recorded reason (carried on
|
|
573
|
-
* `authored` and ledgered on every created Story's `story-plan-state`
|
|
574
|
-
* checkpoint by persist). Absent a recorded reason the conservative default
|
|
575
|
-
* stands: `full`, with `authored: null`. Pure and total.
|
|
576
|
-
*
|
|
577
|
-
* The verdict is a **claim**, not the decision — persist validates it against
|
|
578
|
-
* the authored Story's shape ({@link deriveStoryShape}) and fails closed to
|
|
579
|
-
* `full` when the shape exceeds the ceilings.
|
|
580
|
-
*
|
|
581
|
-
* @param {{ reason?: unknown }} [args]
|
|
582
|
-
* @returns {{
|
|
583
|
-
* route: ComplexityRoute,
|
|
584
|
-
* reasons: string[],
|
|
585
|
-
* authored: Readonly<{ route: 'lite', reason: string }>|null,
|
|
586
|
-
* preserves: typeof LITE_PATH_INVARIANTS,
|
|
587
|
-
* }}
|
|
588
|
-
*/
|
|
589
|
-
export function resolvePlannerRouteVerdict({ reason } = {}) {
|
|
590
|
-
const recorded = typeof reason === 'string' ? reason.trim() : '';
|
|
591
|
-
if (recorded === '') {
|
|
592
|
-
return {
|
|
593
|
-
route: 'full',
|
|
594
|
-
reasons: [
|
|
595
|
-
'no authored lite verdict (no recorded reason) — standard full route',
|
|
596
|
-
],
|
|
597
|
-
authored: null,
|
|
598
|
-
preserves: LITE_PATH_INVARIANTS,
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
return {
|
|
602
|
-
route: 'lite',
|
|
603
|
-
reasons: [`planner verdict: lite (recorded reason): ${recorded}`],
|
|
604
|
-
authored: Object.freeze({ route: 'lite', reason: recorded }),
|
|
605
|
-
preserves: LITE_PATH_INVARIANTS,
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
|
|
609
443
|
/**
|
|
610
444
|
* Assemble the effort/risk shape of a footprint — the evidence
|
|
611
445
|
* {@link deriveStoryShape} decides on and carries on its result.
|