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.
Files changed (114) hide show
  1. package/.agents/agents/plan-critic.md +13 -18
  2. package/.agents/agents/story-worker.md +25 -33
  3. package/.agents/docs/agentrc-reference.json +0 -30
  4. package/.agents/docs/configuration.md +8 -28
  5. package/.agents/docs/execution-reference.md +5 -5
  6. package/.agents/docs/quality-gates.md +8 -7
  7. package/.agents/instructions.md +9 -10
  8. package/.agents/schemas/agentrc.schema.json +9 -185
  9. package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
  10. package/.agents/scripts/acceptance-eval.js +107 -17
  11. package/.agents/scripts/ceremony-derive.js +191 -0
  12. package/.agents/scripts/check-context-budget.js +28 -33
  13. package/.agents/scripts/check-cyclomatic.js +4 -3
  14. package/.agents/scripts/deliver-light.js +31 -94
  15. package/.agents/scripts/evidence-gate.js +17 -1
  16. package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
  17. package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
  18. package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
  19. package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
  20. package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
  21. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
  22. package/.agents/scripts/lib/close-validation/gates.js +52 -1
  23. package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
  24. package/.agents/scripts/lib/config/delivery-routing.js +7 -33
  25. package/.agents/scripts/lib/config/explain.js +0 -19
  26. package/.agents/scripts/lib/config/limits.js +18 -78
  27. package/.agents/scripts/lib/config/quality.js +6 -3
  28. package/.agents/scripts/lib/config/runners.js +3 -2
  29. package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
  30. package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
  31. package/.agents/scripts/lib/config-settings-schema.js +16 -143
  32. package/.agents/scripts/lib/crap-engine.js +35 -4
  33. package/.agents/scripts/lib/crap-utils.js +17 -1
  34. package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
  35. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  36. package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
  37. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  38. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
  39. package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
  40. package/.agents/scripts/lib/orchestration/code-review.js +7 -3
  41. package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
  42. package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
  43. package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
  44. package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
  45. package/.agents/scripts/lib/orchestration/pinned-identifier-lint.js +137 -0
  46. package/.agents/scripts/lib/orchestration/plan-context.js +189 -387
  47. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
  48. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
  49. package/.agents/scripts/lib/orchestration/plan-persist/acceptance-handle-repair.js +107 -0
  50. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +305 -0
  51. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +138 -170
  52. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +128 -297
  53. package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
  54. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
  55. package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
  56. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +36 -135
  57. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
  58. package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
  59. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
  60. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
  61. package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
  62. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
  63. package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
  64. package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
  65. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
  66. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
  67. package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
  68. package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
  69. package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
  70. package/.agents/scripts/lib/story-body/story-body.js +54 -240
  71. package/.agents/scripts/lib/templates/decomposer-prompts.js +133 -121
  72. package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
  73. package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
  74. package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
  75. package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
  76. package/.agents/scripts/lib/test-run-credit.js +277 -0
  77. package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
  78. package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
  79. package/.agents/scripts/lib/workers/crap-worker.js +32 -41
  80. package/.agents/scripts/plan-context.js +7 -9
  81. package/.agents/scripts/plan-critics.js +28 -54
  82. package/.agents/scripts/plan-persist.js +25 -68
  83. package/.agents/scripts/quality-preview.js +51 -0
  84. package/.agents/scripts/run-tests.js +12 -0
  85. package/.agents/scripts/stories-wave-tick.js +23 -45
  86. package/.agents/scripts/test-isolate.js +13 -180
  87. package/.agents/scripts/update-coverage-baseline.js +25 -70
  88. package/.agents/scripts/update-crap-baseline.js +19 -123
  89. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  90. package/.agents/workflows/audit-clean-code.md +4 -3
  91. package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
  92. package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
  93. package/.agents/workflows/helpers/code-review.md +2 -3
  94. package/.agents/workflows/helpers/deliver-digest.md +46 -55
  95. package/.agents/workflows/helpers/deliver-light.md +40 -105
  96. package/.agents/workflows/helpers/deliver-reference.md +1 -1
  97. package/.agents/workflows/helpers/deliver-story-reference.md +54 -55
  98. package/.agents/workflows/helpers/deliver-story.md +10 -13
  99. package/.agents/workflows/helpers/plan-reference.md +163 -221
  100. package/.agents/workflows/mandrel-plan.md +31 -40
  101. package/.agents/workflows/memory-consolidate.md +9 -13
  102. package/docs/CHANGELOG.md +36 -0
  103. package/lib/cli/registry.js +98 -2
  104. package/lib/migrations/index.js +4 -0
  105. package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
  106. package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
  107. package/package.json +1 -1
  108. package/.agents/scripts/lib/framework-version.js +0 -39
  109. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
  110. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
  111. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
  112. package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
  113. package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
  114. 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 `ask-operator`
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 undisableable cap
46
+ * ## The cap
47
47
  *
48
- * `maxRounds` arrives already clamped by `lib/config/acceptance-eval.js`
49
- * into `[1, ceiling]`, but this reducer defends the invariant a second
50
- * time: a non-positive or non-integer cap is coerced to 1, so there is no
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` (skip fresh critic + sampling floor).
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 (+ sampling floor);
25
- * high → fresh.
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
- * ## Maker-checker sampling floor
62
+ * ## No sampling floor (Story #5313)
65
63
  *
66
- * Even at a `low` derived level under `standard`, a fraction of clusters
67
- * (`freshCriticSampleRate`, default 0.2) is forced `fresh` so a low level never
68
- * means zero independent checking. The selection is **deterministic** in the
69
- * cluster index (a fixed stride), so it is stable across re-runs and —
70
- * critically — never changes the cluster count: it only re-labels which of
71
- * the fixed set of clusters run fresh. Profiles `minimal` and `strict`
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
- * Decide whether the sampling floor forces this low-risk cluster fresh.
123
- *
124
- * Deterministic in the cluster index: with rate `r` (0 < r ≤ 1) the stride is
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 and sampling floor.
176
- * `resolveCeremonyForRisk` decorates the result with the single
177
- * `verdictOwner` derived from the mode (Story #4723).
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 project base branch fallback used when a caller omits
74
- * `baseRef`.
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 complexity routing
3
- * (Story #4722, superseding the word-count gate of Stories #4683/#4707).
4
- *
5
- * ## Route on the work, not the words
6
- *
7
- * The original gate routed a planning seed on its **word count**
8
- * (`maxSeedWords`), which is the wrong proxy in both directions: a detailed
9
- * prompt can describe trivial work, a terse one complex work. The bench
10
- * cohort (mandrel-bench 2.10.0) observed both failure modes — a lite verdict
11
- * fired at plan time and was then lost (a swallowed label write) or ignored
12
- * (deliver spawned a full story-worker anyway). This module now routes on the
13
- * **objective shape of the authored work**, staged across the pipeline:
14
- *
15
- * 1. **Plan time — signals, not routing.** {@link buildComplexitySignals}
16
- * emits advisory complexity *signals* (enumerated-artifact count,
17
- * risk-heuristic hits, repo state of predicted paths, sensitive-path
18
- * classes) carrying **no routing authority**. There is no word ceiling.
19
- * 2. **Planner judgment, ledgered.** The planner owns the
20
- * trivial-vs-standard verdict ({@link resolvePlannerRouteVerdict}) —
21
- * `lite` only with a recorded reason, persisted on plan state. This
22
- * generalizes the former one-way `applyPlannerDowngrade` seam into the
23
- * authored verdict itself; the conservative default without a recorded
24
- * reason is `full`.
25
- * 3. **Deterministic backstop at persist.** After authoring, the work has
26
- * measurable shape: {@link deriveStoryShape} reads the Story's own
27
- * effort and risk — distinct change kinds, declared magnitude,
28
- * uncertainty, deployable/migration span, and sensitive-path classes —
29
- * against {@link STORY_SHAPE_CEILINGS}. A `lite` claim whose work exceeds
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. And sensitivity
48
- * always wins: a small change whose footprint intersects a sensitive-path
49
- * class routes `full`, which keeps its fresh acceptance critic
50
- * (`ceremony-routing.js` routes a high derived level to a fresh spawn).
51
- *
52
- * ## What "lite" changes and — critically — what it never changes
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
- * (Story #4722 AC-2). Signals, not routing: the result carries
469
- * `routingAuthority: false` and no `route` field — the planner reads these
470
- * alongside its own judgment ({@link resolvePlannerRouteVerdict}) and the
471
- * deterministic shape backstop validates the authored Story at persist.
472
- *
473
- * - `artifactCount` — enumerated items in the seed, with the
474
- * configured `maxArtifacts` threshold beside it
475
- * as one input signal.
476
- * - `riskHeuristicHits` — `planning.riskHeuristics` phrases present in
477
- * the seed (same substring matcher the
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.