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
@@ -1,28 +1,23 @@
1
1
  /**
2
- * Limits/budgets/signals accessors (Epic #1720 Story #1739 — top-level reshape).
2
+ * Limits accessors (Epic #1720 Story #1739 — top-level reshape).
3
3
  *
4
4
  * Pre-reshape, every runtime ceiling lived under the legacy `agentSettings.limits.*` bag.
5
- * Post-reshape, the surviving operator-configurable keys are split across
6
- * `planning.*` and `delivery.*`:
5
+ * Post-reshape, the surviving operator-configurable key is
6
+ * `delivery.execution.timeoutMs` (per-process execution timeout).
7
7
  *
8
- * - `delivery.execution.timeoutMs` (per-process execution timeout)
9
- * - `delivery.signals.{rework, retry}` (performance-signal detector
10
- * thresholds — `hotspot` retired with Epic #4406; `churn`/`idle` dropped)
11
- *
12
- * Framework constants (not operator-tunable via `.agentrc.json`):
13
- * - `maxTickets` — decomposer reviewability budget (Story #4163)
14
- *
15
- * Dropped entirely: `maxInstructionSteps`, `friction.*`, `executionMaxBuffer`,
16
- * `signals.{churn, idle}`, `delivery.preflight`, `delivery.lease.ttlMs`
17
- * (Story #5006 deleted the lease TTL: with no heartbeat source every foreign
18
- * claim read live, so the window decided nothing), `delivery.maxTokenBudget`
19
- * (planning no longer sizes against a token-budget envelope; session-mass
20
- * ceilings are absolute in `DEFAULT_MODEL_CAPACITY`), and
21
- * `planning.context.{maxBytes, summaryMode}` (Story #4541 — the `applyBudget`
22
- * pass they fed lost its last caller in the v2 cutover, and it bounded a field
23
- * the envelope builders discarded; the live bound on planner-context size is
24
- * the fixed `PLAN_CONTEXT_ENVELOPE_BYTE_CEILING` in
25
- * `lib/orchestration/plan-context.js`).
8
+ * Dropped entirely: `maxTickets` (Story #5312 — the reviewability budget
9
+ * never fired on a real plan and duplicated the default-single split policy),
10
+ * `delivery.signals.{rework, retry}` with `SIGNALS_DEFAULTS` (Story #5313 —
11
+ * the detector thresholds bounded execution by count, not by risk),
12
+ * `maxInstructionSteps`, `friction.*`, `executionMaxBuffer`,
13
+ * `signals.{churn, idle, hotspot}`, `delivery.preflight`,
14
+ * `delivery.lease.ttlMs` (Story #5006 deleted the lease TTL: with no
15
+ * heartbeat source every foreign claim read live, so the window decided
16
+ * nothing), `delivery.maxTokenBudget` (planning no longer sizes against a
17
+ * token-budget envelope), and `planning.context.{maxBytes, summaryMode}`
18
+ * (Story #4541 — the `applyBudget` pass they fed lost its last caller in the
19
+ * v2 cutover; the live bound on planner-context size is the fixed
20
+ * `PLAN_CONTEXT_ENVELOPE_BYTE_CEILING` in `lib/orchestration/plan-context.js`).
26
21
  *
27
22
  * The historic combined accessor `getLimits(config)` is preserved as a
28
23
  * compatibility surface: it returns a wrapper carrying the surviving
@@ -30,59 +25,19 @@
30
25
  * working. New call sites should prefer the specific accessors below.
31
26
  */
32
27
 
33
- /**
34
- * Framework defaults for the performance-signal detector thresholds.
35
- * `hotspot` was retired with its detector (Epic #4406); `churn` and `idle`
36
- * were dropped earlier.
37
- */
38
- export const SIGNALS_DEFAULTS = Object.freeze({
39
- rework: Object.freeze({ editsPerFile: 5 }),
40
- retry: Object.freeze({ repeatCount: 3 }),
41
- });
42
-
43
28
  /**
44
29
  * Framework defaults for the surviving limits surface.
45
30
  */
46
31
  export const LIMITS_DEFAULTS = Object.freeze({
47
- maxTickets: 80,
48
32
  executionTimeoutMs: 600000,
49
- signals: SIGNALS_DEFAULTS,
50
33
  });
51
34
 
52
- /**
53
- * Per-detector merge of an operator-supplied `delivery.signals.*` block
54
- * with framework defaults. Each detector is shallow-overlaid so an
55
- * operator can override a single threshold without re-listing the others.
56
- *
57
- * @param {object|undefined} userSignals
58
- * @returns {{ rework: {editsPerFile: number}, retry: {repeatCount: number} }}
59
- */
60
- function mergeSignals(userSignals) {
61
- const user =
62
- userSignals && typeof userSignals === 'object' ? userSignals : {};
63
- const merged = {};
64
- for (const detector of Object.keys(SIGNALS_DEFAULTS)) {
65
- const userDetector =
66
- user[detector] && typeof user[detector] === 'object'
67
- ? user[detector]
68
- : {};
69
- merged[detector] = { ...SIGNALS_DEFAULTS[detector], ...userDetector };
70
- }
71
- return merged;
72
- }
73
-
74
35
  /**
75
36
  * Resolve the surviving limits surface against a `.agentrc.json` shape
76
- * (post-reshape). `maxTickets` is a framework constant (never read from
77
- * config); pulls `executionTimeoutMs` from `delivery.*`, pulls signals from
78
- * `delivery.signals.*`.
37
+ * (post-reshape): `executionTimeoutMs` from `delivery.execution.*`.
79
38
  *
80
39
  * @param {object|undefined} config
81
- * @returns {{
82
- * maxTickets: number,
83
- * executionTimeoutMs: number,
84
- * signals: ReturnType<typeof mergeSignals>,
85
- * }}
40
+ * @returns {{ executionTimeoutMs: number }}
86
41
  */
87
42
  export function resolveLimits(config) {
88
43
  const delivery =
@@ -94,10 +49,8 @@ export function resolveLimits(config) {
94
49
  ? delivery.execution
95
50
  : {};
96
51
  return {
97
- maxTickets: LIMITS_DEFAULTS.maxTickets,
98
52
  executionTimeoutMs:
99
53
  execution.timeoutMs ?? LIMITS_DEFAULTS.executionTimeoutMs,
100
- signals: mergeSignals(delivery.signals),
101
54
  };
102
55
  }
103
56
 
@@ -111,16 +64,3 @@ export function resolveLimits(config) {
111
64
  export function getLimits(config) {
112
65
  return resolveLimits(config ?? undefined);
113
66
  }
114
-
115
- /**
116
- * Read the merged `delivery.signals` block. Equivalent to
117
- * `getLimits(config).signals` but exposed as a standalone accessor so
118
- * detector wiring can import it without dragging the whole limits
119
- * surface into their bundle.
120
- *
121
- * @param {object | null | undefined} config
122
- * @returns {ReturnType<typeof resolveLimits>['signals']}
123
- */
124
- export function getSignals(config) {
125
- return getLimits(config).signals;
126
- }
@@ -491,10 +491,15 @@ function resolveCoverageGate(userBlock) {
491
491
  * before this value is ever read. `maintainability.tolerance` is the one
492
492
  * documented MI-drop control now; see `lib/migrations/index.js` for the
493
493
  * consumer-config migration that strips a leftover key on upgrade.
494
+ *
495
+ * `cyclomaticMustFix` was retired in Story #5313: the cyclomatic ratchet
496
+ * (`check-cyclomatic.js`) keeps a fixed ceiling of 12
497
+ * (`lib/cyclomatic-ceiling.js#CYCLOMATIC_CEILING`), and `cyclomaticFlag` is
498
+ * the one advisory knob left — `quality-preview.js` reports over-flag
499
+ * methods without failing on them.
494
500
  */
495
501
  export const CODING_GUARDRAILS_DEFAULTS = Object.freeze({
496
502
  cyclomaticFlag: 8,
497
- cyclomaticMustFix: 12,
498
503
  requireSiblingTest: false,
499
504
  });
500
505
 
@@ -512,8 +517,6 @@ export function resolveCodingGuardrails(userBlock) {
512
517
  );
513
518
  return {
514
519
  cyclomaticFlag: userBlock.cyclomaticFlag ?? defaults.cyclomaticFlag,
515
- cyclomaticMustFix:
516
- userBlock.cyclomaticMustFix ?? defaults.cyclomaticMustFix,
517
520
  requireSiblingTest:
518
521
  typeof userBlock.requireSiblingTest === 'boolean'
519
522
  ? userBlock.requireSiblingTest
@@ -49,10 +49,11 @@ const DEFAULT_DELIVER_RUNNER = Object.freeze({
49
49
  * Default auto-fix loop ceilings for /mandrel-deliver code-review. Operators
50
50
  * override via `delivery.codeReview.*` in `.agentrc.json` (Story #2611,
51
51
  * Epic #2586; `autoFixSeverity` default `'medium'` per Story #4399).
52
+ * `maxFixScopeFiles` was retired in Story #5313 — it bounded a fix by file
53
+ * count rather than by risk.
52
54
  */
53
55
  export const DEFAULT_CODE_REVIEW = Object.freeze({
54
56
  maxFixAttempts: 3,
55
- maxFixScopeFiles: 5,
56
57
  autoFixSeverity: 'medium',
57
58
  });
58
59
 
@@ -62,7 +63,7 @@ export const DEFAULT_CODE_REVIEW = Object.freeze({
62
63
  * @param {object | null | undefined} config
63
64
  * @returns {{
64
65
  * deliverRunner: { concurrencyCap: number, footprintGuard: 'enforce'|'advisory' },
65
- * codeReview: { maxFixAttempts: number, maxFixScopeFiles: number, autoFixSeverity: 'high'|'medium' },
66
+ * codeReview: { maxFixAttempts: number, autoFixSeverity: 'high'|'medium' },
66
67
  * decomposer: { concurrencyCap: number },
67
68
  * }}
68
69
  */
@@ -173,49 +173,6 @@ const WORKTREE_ISOLATION_SCHEMA = {
173
173
  ],
174
174
  };
175
175
 
176
- /**
177
- * `delivery.signals` — detector thresholds for the surviving
178
- * performance-signal categories. `hotspot` was retired with its detector
179
- * (Epic #4406); `churn` and `idle` were dropped earlier (low signal-to-noise).
180
- * Each block is shallow-merged by the resolver.
181
- */
182
- const SIGNALS_SCHEMA = {
183
- type: 'object',
184
- description:
185
- 'Detector thresholds for the surviving performance-signal categories. Each block is shallow-merged by the resolver.',
186
- properties: {
187
- rework: {
188
- type: 'object',
189
- description: 'Rework detector — repeated edits to one file in a run.',
190
- properties: {
191
- editsPerFile: {
192
- type: 'integer',
193
- minimum: 1,
194
- description:
195
- 'Edits to a single file within one run that trip the rework signal.',
196
- default: LIMITS_DEFAULTS.signals.rework.editsPerFile,
197
- },
198
- },
199
- additionalProperties: false,
200
- },
201
- retry: {
202
- type: 'object',
203
- description: 'Retry detector — the same command failing repeatedly.',
204
- properties: {
205
- repeatCount: {
206
- type: 'integer',
207
- minimum: 1,
208
- description:
209
- 'Repeats of an identical failing command that trip the retry signal.',
210
- default: LIMITS_DEFAULTS.signals.retry.repeatCount,
211
- },
212
- },
213
- additionalProperties: false,
214
- },
215
- },
216
- additionalProperties: false,
217
- };
218
-
219
176
  /**
220
177
  * `delivery.mergeWatch` — knobs consumed by the close-and-land merge wait
221
178
  * listener (Story #2896, Epic #2880) and by the close-and-land merge wait
@@ -294,19 +251,19 @@ const MERGE_WATCH_SCHEMA = {
294
251
  * (`CODE_REVIEW_SCHEMA` imported from the quality schema module).
295
252
  */
296
253
 
297
- // Epic #4478 (M7-B) — role-scoped-agent kill-switch + maker-checker floor.
254
+ // Epic #4478 (M7-B) — role-scoped-agent kill-switch.
298
255
  // Stage 6 dropped `delivery.routing.singleDelivery` (v1 epic route switch).
299
256
  // `delivery.routing.roleScopedAgents` (default true via getDeliveryRouting)
300
257
  // flips converted delivery spawns onto their `.claude/agents/<role>.md` boot
301
258
  // context; false falls back to `subagent_type: general-purpose` (the instant
302
259
  // per-consumer revert + the escape for hosts that ignore `.claude/agents/`).
303
- // `delivery.routing.freshCriticSampleRate` (default 0.2, clamped [0, 1]) is the
304
- // maker-checker sampling floor forcing a fraction of low-derived-level
305
- // acceptance clusters through a fresh critic.
260
+ // Story #5313 retired `delivery.routing.freshCriticSampleRate` (the
261
+ // maker-checker sampling floor): the standard profile now routes purely off
262
+ // the derived change level.
306
263
  const ROUTING_SCHEMA = {
307
264
  type: 'object',
308
265
  description:
309
- 'v2 delivery-spawn routing: role-scoped boot contexts and maker-checker sampling. The v1 singleDelivery epic-route kill-switch was removed in Stage 6.',
266
+ 'v2 delivery-spawn routing: role-scoped boot contexts and the ceremony profile. The v1 singleDelivery epic-route kill-switch was removed in Stage 6; the freshCriticSampleRate sampling floor was retired in Story #5313.',
310
267
  properties: {
311
268
  roleScopedAgents: {
312
269
  type: 'boolean',
@@ -314,19 +271,11 @@ const ROUTING_SCHEMA = {
314
271
  'Epic #4478 (M7-B). Kill-switch for the role-scoped boot contexts. When true (default), a converted delivery spawn (`story-worker`, `acceptance-critic`) boots on its own `.claude/agents/<role>.md` system prompt instead of re-paying the full CLAUDE.md @-import closure. When false, every converted spawn falls back to `subagent_type: general-purpose` — the instant, code-rollback-free per-consumer revert, and the universal escape for hosts that ignore `.claude/agents/`. The fallback is the full-closure agent that ran before M7-B, so flipping it off never drops a gate.',
315
272
  default: DELIVERY_ROUTING_DEFAULTS.roleScopedAgents,
316
273
  },
317
- freshCriticSampleRate: {
318
- type: 'number',
319
- minimum: 0,
320
- maximum: 1,
321
- description:
322
- 'Epic #4478 (M7-B, Part 2). Maker-checker sampling floor. Under the standard profile, a change set touching no sensitive path routes its acceptance clusters down the contract-identical inline critic path, but this fraction of them is still forced through a fresh-context critic so a low derived level never means zero independent checking. Clamped to [0, 1]; 0 disables the floor, 1 forces every cluster fresh. Consumed by resolveCeremonyForRisk (lib/orchestration/ceremony-routing.js).',
323
- default: DELIVERY_ROUTING_DEFAULTS.freshCriticSampleRate,
324
- },
325
274
  ceremonyProfile: {
326
275
  type: 'string',
327
276
  enum: ['minimal', 'standard', 'strict'],
328
277
  description:
329
- 'Acceptance-ceremony depth. minimal = always inline critic; strict = always fresh-context critic; standard (default) = routed off the change level derived from the Story diff, with the maker-checker sampling floor.',
278
+ 'Acceptance-ceremony depth. minimal = always inline critic; strict = always fresh-context critic; standard (default) = routed off the change level derived from the Story diff: high or underivable → fresh, low → inline.',
330
279
  default: DELIVERY_ROUTING_DEFAULTS.ceremonyProfile,
331
280
  },
332
281
  closeAndLand: {
@@ -458,23 +407,22 @@ const REFACTOR_STAGE_SCHEMA = {
458
407
  * re-evaluates — capped at `maxRounds` redraft rounds.
459
408
  *
460
409
  * `maxRounds` is the operator-tunable redraft ceiling (default 2 via
461
- * `lib/config/acceptance-eval.js`). It is a soft knob inside an
462
- * **undisableable** hard cap: `lib/config/acceptance-eval.js` clamps any
463
- * configured value into `[1, ACCEPTANCE_EVAL_MAX_ROUNDS_CEILING]`, so no
464
- * configuration can switch the loop off (`maxRounds: 0`) or let it spin
465
- * unbounded. There is intentionally **no** `enabled` flag — the loop is a
466
- * hard cutover, always on, per `rules/git-conventions.md`.
410
+ * `lib/config/acceptance-eval.js`). Story #5313 dropped the hard ceiling and
411
+ * the floor-of-one clamp: `maxRounds: 0` is valid and means one pass scored
412
+ * once with no redraft round. There is intentionally **no** `enabled` flag —
413
+ * the scoring pass is a hard cutover, always on, per
414
+ * `rules/git-conventions.md`.
467
415
  */
468
416
  const ACCEPTANCE_EVAL_SCHEMA = {
469
417
  type: 'object',
470
418
  description:
471
- 'Story #3819. Bounded per-Story acceptance self-eval loop. After the implementation commits land and before the Story-implementation phase flips to `closing`, an independent (fresh-context) critic pass scores the caller-injected change set against each inline `acceptance[]` item, redrafts the unmet items, and re-evaluates — capped at `maxRounds` redraft rounds, then escalates to `agent::blocked` when criteria remain unmet. There is no `enabled` flag: the loop is a hard cutover (always on).',
419
+ 'Story #3819. Bounded per-Story acceptance self-eval loop. After the implementation commits land and before the Story-implementation phase flips to `closing`, an independent (fresh-context) critic pass scores the caller-injected change set against each inline `acceptance[]` item, redrafts the unmet items, and re-evaluates — capped at `maxRounds` redraft rounds (0 = scored once, no redraft), then escalates to `agent::blocked` when criteria remain unmet. There is no `enabled` flag: the scoring pass is a hard cutover (always on).',
472
420
  properties: {
473
421
  maxRounds: {
474
422
  type: 'integer',
475
- minimum: 1,
423
+ minimum: 0,
476
424
  description:
477
- 'Maximum number of redraft rounds before escalation. Default 2; clamped into [1, hard ceiling] by lib/config/acceptance-eval.js so the cap can never be disabled (maxRounds: 0 clamps up to 1).',
425
+ 'Maximum number of redraft rounds before escalation. Default 2; 0 means the verdict is scored once with no redraft round (Story #5313 dropped the hard ceiling and the floor-of-one clamp).',
478
426
  default: ACCEPTANCE_EVAL_DEFAULTS.maxRounds,
479
427
  },
480
428
  },
@@ -656,14 +604,13 @@ const TEMP_RETENTION_SCHEMA = {
656
604
  export const DELIVERY_SCHEMA = {
657
605
  type: 'object',
658
606
  description:
659
- 'Everything `/mandrel-deliver` and `single-story-close` consume: execution timeouts, worktree isolation, runner concurrency, docs freshness, signals, quality gates, merge/CI watch, review ceremony, and the feedback loop.',
607
+ 'Everything `/mandrel-deliver` and `single-story-close` consume: execution timeouts, worktree isolation, runner concurrency, docs freshness, quality gates, merge/CI watch, review ceremony, and the feedback loop.',
660
608
  properties: {
661
609
  execution: EXECUTION_SCHEMA,
662
610
  docsFreshness: DOCS_FRESHNESS_SCHEMA,
663
611
  tempRetention: TEMP_RETENTION_SCHEMA,
664
612
  deliverRunner: DELIVER_RUNNER_SCHEMA,
665
613
  worktreeIsolation: WORKTREE_ISOLATION_SCHEMA,
666
- signals: SIGNALS_SCHEMA,
667
614
  // `quality.gates.crap.incrementalCoverage` (Story #4981) is declared in
668
615
  // `config/gates/crap.schema.js` and reaches AJV validation through this
669
616
  // property — QUALITY_SCHEMA → GATES_SCHEMA → CRAP_GATE. No separate
@@ -40,13 +40,6 @@ const CODING_GUARDRAILS_SCHEMA = {
40
40
  'Cyclomatic complexity at which a new or changed method is flagged for a refactor look.',
41
41
  default: CODING_GUARDRAILS_DEFAULTS.cyclomaticFlag,
42
42
  },
43
- cyclomaticMustFix: {
44
- type: 'integer',
45
- minimum: 1,
46
- description:
47
- 'Cyclomatic complexity at which a new or changed method must be decomposed before the diff closes.',
48
- default: CODING_GUARDRAILS_DEFAULTS.cyclomaticMustFix,
49
- },
50
43
  requireSiblingTest: {
51
44
  type: 'boolean',
52
45
  description:
@@ -356,13 +349,6 @@ export const CODE_REVIEW_SCHEMA = {
356
349
  'Maximum auto-fix retry attempts per finding in /mandrel-deliver Phase 5 (code-review). 0 disables auto-fix. Default 3.',
357
350
  default: DEFAULT_CODE_REVIEW.maxFixAttempts,
358
351
  },
359
- maxFixScopeFiles: {
360
- type: 'integer',
361
- minimum: 1,
362
- description:
363
- 'Maximum file count a single auto-fix may modify before escalating to agent::blocked. Default 5.',
364
- default: DEFAULT_CODE_REVIEW.maxFixScopeFiles,
365
- },
366
352
  autoFixSeverity: {
367
353
  type: 'string',
368
354
  enum: ['high', 'medium'],
@@ -64,22 +64,6 @@ const NULLABLE_NONEMPTY_SAFE_STRING = {
64
64
  not: { type: 'string', pattern: SHELL_INJECTION_PATTERN_STRING },
65
65
  };
66
66
 
67
- /** A list-valued config key may be a plain array (replace) or an extender
68
- * object `{ append, prepend }` that deep-merges with framework defaults. */
69
- const LIST_OR_EXTENDER_OF_STRINGS = {
70
- oneOf: [
71
- { type: 'array', items: { type: 'string' } },
72
- {
73
- type: 'object',
74
- properties: {
75
- append: { type: 'array', items: { type: 'string' } },
76
- prepend: { type: 'array', items: { type: 'string' } },
77
- },
78
- additionalProperties: false,
79
- },
80
- ],
81
- };
82
-
83
67
  /**
84
68
  * Backwards-compatible export used by a handful of call sites that historically
85
69
  * scanned the schema for string-shaped fields. Post-reshape, the only
@@ -494,149 +478,38 @@ const GITHUB_SCHEMA = {
494
478
  // `planning` carries `additionalProperties: false`, so a resurrected key fails
495
479
  // loudly; the 2.20.0 retirement migration strips it on upgrade.
496
480
 
481
+ // Story #5312 — the planning diet. `riskHeuristics`, `complexityGate`,
482
+ // `memoryPool.{staleAfterDays, growthDelta}`, `failOnSharedEditors`,
483
+ // `requireExplicitCrossStoryDeps`, `failOnRegistryConflicts`,
484
+ // `failOnLargeFanOut`, `largeFanOutThreshold` and `crossCuttingRegistries`
485
+ // were retired together: every one either never fired on real work, duplicated
486
+ // a judgment the authoring model already makes, or guarded a consumer that no
487
+ // longer exists. The block stays `additionalProperties: false`, so a config
488
+ // still carrying one fails loudly; the 2.57.0 retirement migration strips them
489
+ // on upgrade.
490
+
497
491
  const PLANNING_SCHEMA = {
498
492
  type: 'object',
499
493
  description:
500
- 'Inputs to `/mandrel-plan`: risk escalation heuristics, ceremony-lite routing, the memory-hygiene advisory thresholds, and the cross-Story conflict-finding severity gates.',
494
+ 'Inputs to `/mandrel-plan`: the memory-hygiene advisory ceiling and the opt-in navigability reachability gate.',
501
495
  properties: {
502
- riskHeuristics: {
503
- ...LIST_OR_EXTENDER_OF_STRINGS,
504
- description:
505
- 'Prose heuristics the planner escalates a Story against. A plain array replaces the framework list; the `{ append, prepend }` extender form deep-merges with it.',
506
- default: [
507
- 'Destructive or irreversible data mutations (dropping tables, deleting rows without soft-delete or backup, truncating production state).',
508
- 'Modifications to shared security or auth infrastructure (IAM policies, auth middleware, session or token handling, secret rotation).',
509
- 'Changes to CI/CD, deployment pipelines, or release gating that could disable safety checks or ship unverified code to production.',
510
- 'Monorepo-wide AST or text replacements touching overlapping files in parallel (catastrophic merge-conflict risk across concurrent agents).',
511
- 'Schema migrations that rewrite existing rows or drop columns without a backfill or rollback plan.',
512
- ],
513
- },
514
- // Story #4722 (superseding #4683's word-count gate) — shape-derived
515
- // ceremony-lite routing. Complexity routes on the objective shape of the
516
- // authored work (changes[] count, acceptance count, creates-vs-refactors
517
- // mix, sensitive-path classes), never on seed word count: `maxSeedWords`
518
- // was removed in the hard cutover and is rejected as an additional
519
- // property. The lite path never relaxes a non-negotiable (Story ticket,
520
- // PR-to-main, repo gates, security baseline). Defaults live on
521
- // DEFAULT_COMPLEXITY_GATE in `lib/orchestration/complexity-gate.js`;
522
- // shape ceilings are the framework constants STORY_SHAPE_CEILINGS.
523
- complexityGate: {
524
- type: 'object',
525
- description:
526
- 'Shape-derived ceremony-lite complexity routing. A lite claim is validated against the authored Story shape at persist and re-derived from the Story body at dispatch; conservative (full on any doubt). Never relaxes the Story-ticket / PR-to-main / repo-gates / security-baseline non-negotiables.',
527
- properties: {
528
- enabled: {
529
- type: 'boolean',
530
- description:
531
- 'Master switch. When false, lite routing is disabled everywhere: persist refuses lite claims and dispatch always takes the sub-agent path. Default true.',
532
- },
533
- maxArtifacts: {
534
- type: 'integer',
535
- minimum: 0,
536
- description:
537
- 'Enumerated-artifact threshold reported by the plan-context complexity signals. An input signal for the planner verdict — carries no routing authority. Default 1.',
538
- },
539
- },
540
- additionalProperties: false,
541
- },
542
- // Story #5182 — the `/mandrel-plan` Phase 0 memory-hygiene advisory's two
543
- // arms. The count arm this replaced was an absolute ceiling, which no
544
- // consolidation pass could ever bring a pool back under; `growthDelta`
545
- // measures entries written since the last pass instead, which a pass
546
- // does reset. Both are advisory thresholds — nothing here gates a plan.
496
+ // The `/mandrel-plan` Phase 0 memory-hygiene advisory's one surviving
497
+ // arm (Story #5285; the age and growth arms went with Story #5312).
547
498
  memoryPool: {
548
499
  type: 'object',
549
500
  description:
550
- 'Thresholds for the memory-hygiene advisory `/mandrel-plan` surfaces at Gate #1. Advisory only: it recommends `/memory-consolidate` and never gates, reroutes, or mutates the memory pool.',
501
+ 'Threshold for the memory-hygiene advisory `/mandrel-plan` surfaces at Gate #1. Advisory only: it recommends `/memory-consolidate` and never gates, reroutes, or mutates the memory pool.',
551
502
  properties: {
552
- staleAfterDays: {
553
- type: 'integer',
554
- minimum: 1,
555
- description:
556
- "Recommend a consolidation pass once the pool's stamp is older than this many days. Default 30.",
557
- default: 30,
558
- },
559
- growthDelta: {
560
- type: 'integer',
561
- minimum: 1,
562
- description:
563
- 'Recommend a consolidation pass once this many entries have been written since the last one. Measured against the entry count the last pass stamped, so a stamp predating that field leaves growth unmeasured and only the age threshold applies. Default 25.',
564
- default: 25,
565
- },
566
503
  indexByteCeiling: {
567
504
  type: 'integer',
568
505
  minimum: 1,
569
506
  description:
570
- "Recommend a consolidation pass once the pool's `MEMORY.md` index exceeds this many bytes. Independent of the age and growth thresholds: the harness truncates the index it loads into each session at its own byte cap, so an oversized index is a loss already happening — every entry listed after the cut is invisible — rather than a hygiene forecast. Default 24576, the harness cap itself.",
507
+ "Recommend a consolidation pass once the pool's `MEMORY.md` index exceeds this many bytes. The harness truncates the index it loads into each session at its own byte cap, so an oversized index is a loss already happening — every entry listed after the cut is invisible — rather than a hygiene forecast. Default 24576, the harness cap itself.",
571
508
  default: 24576,
572
509
  },
573
510
  },
574
511
  additionalProperties: false,
575
512
  },
576
-
577
- // Cross-Story conflict-finding severity gates. Off by default so
578
- // existing repos keep advisory-only behaviour; flipping either to
579
- // `true` upgrades the matching finding class to `'hard'`, which routes
580
- // it through the validator's `errors[]` channel and trips the bounded
581
- // decompose loop's re-prompt gate.
582
- // `planning.modelCapacity` was collapsed to the framework constant
583
- // `DEFAULT_MODEL_CAPACITY` in ticket-validator-sizing.js (authored-
584
- // tokens-only mass); setting it in a config is rejected as an
585
- // additional property.
586
- failOnSharedEditors: {
587
- type: 'boolean',
588
- description:
589
- 'When true, upgrade shared-editor conflict findings to hard errors (default false — advisory soft findings only).',
590
- default: false,
591
- },
592
- requireExplicitCrossStoryDeps: {
593
- type: 'boolean',
594
- description:
595
- 'When true, upgrade implicit cross-Story dependency findings to hard errors (default false — advisory soft findings only).',
596
- default: false,
597
- },
598
- // Cross-cutting registry conflict knobs consumed by
599
- // `ticket-validator-conflicts.js` (wired through
600
- // `epic-plan-decompose/phases/planning-artifacts.js`).
601
- // `crossCuttingRegistries` names the registry paths whose concurrent
602
- // edits are flagged; `failOnRegistryConflicts` upgrades that finding to
603
- // `'hard'`. `failOnLargeFanOut` / `largeFanOutThreshold` gate the
604
- // delete blast-radius finding (call sites of a module a Story marks
605
- // `assumption: "deletes"`).
606
- crossCuttingRegistries: {
607
- ...LIST_OR_EXTENDER_OF_STRINGS,
608
- description:
609
- 'Registry path patterns whose concurrent edits across Stories are flagged as conflicts. Defaults to the framework listener/handler index patterns when omitted.',
610
- // Mirrors DEFAULT_REGISTRY_PATTERNS in
611
- // `lib/orchestration/ticket-validator-conflicts.js`. Restated rather
612
- // than imported: that module pulls in the story-body parser and the
613
- // reachability walker, which have no business loading behind a schema
614
- // declaration. The rewritten parity suite asserts the two agree.
615
- default: [
616
- 'lib/orchestration/lifecycle/listeners/index.js',
617
- '**/listeners/index.js',
618
- '**/handlers/index.js',
619
- ],
620
- },
621
- failOnRegistryConflicts: {
622
- type: 'boolean',
623
- description:
624
- 'When true, upgrade cross-cutting registry conflict findings to hard errors (default false).',
625
- default: false,
626
- },
627
- failOnLargeFanOut: {
628
- type: 'boolean',
629
- description:
630
- 'When true, upgrade fan-out-warning findings (delete blast radius) to hard errors (default false — soft advisory).',
631
- default: false,
632
- },
633
- largeFanOutThreshold: {
634
- type: 'integer',
635
- minimum: 0,
636
- description:
637
- 'Call-site count above which a Story that deletes a module emits a fan-out-warning. Counts base-branch references to the deleted path basename. Soft by default; does not size or reject Stories. Default 10.',
638
- default: 10,
639
- },
640
513
  // Navigability-reachability config consumed by the plan-persist draft
641
514
  // reachability gate (Epic #4131 F7; demoted into persist by #4474 PR6).
642
515
  // Opt-in: absent or empty routeGlobs degrades to a silent no-op.
@@ -681,7 +554,7 @@ const PLANNING_SCHEMA = {
681
554
  * - `project` — identity, paths, commands, docs context.
682
555
  * - `github` — provider identity, branch protection, merge methods,
683
556
  * notifications.
684
- * - `planning` — risk heuristics, max tickets, planning-context limits.
557
+ * - `planning` — the memory-hygiene advisory ceiling, navigability gate.
685
558
  * - `delivery` — execution timeouts, worktree isolation, deliver-runner
686
559
  * concurrency, docs-freshness, signals, quality.
687
560
  *
@@ -14,9 +14,35 @@ import {
14
14
  crapFormula,
15
15
  } from './crap-coordinates.js';
16
16
  import { deriveMethodIdentities } from './crap-method-identity.js';
17
+ import { install as installAstCompat } from './escomplex-ast-compat.js';
17
18
 
18
19
  export { COORDINATE_ORIGINAL, COORDINATE_TRANSPILED, crapFormula };
19
20
 
21
+ /**
22
+ * Sentinel returned by {@link calculateCrapForSource} for a source the kernel
23
+ * cannot parse. Deliberately **not** `[]`: a caller receiving an empty array
24
+ * cannot tell an unscorable file from one with no methods, which is how a
25
+ * parse failure used to reach the baseline as a silent zero (Story #5311).
26
+ */
27
+ export const UNSCORABLE = null;
28
+
29
+ // The kernel's code generator predates the Babel AST its own parser emits, so
30
+ // ordinary modern syntax (`?.`, `await` or a regex in a loop head, object
31
+ // spread in a default parameter) aborts `analyzeModule` for the WHOLE file —
32
+ // see `escomplex-ast-compat.js` for the defect and the upstream status.
33
+ //
34
+ // Story #5311: the install belongs here, at the scoring kernel, because this
35
+ // is where both CRAP scorers converge — `calculateCrapForSource` (the worker
36
+ // path) and `crap-utils.js#analyzeOnce` (the serial path, which reaches this
37
+ // module for `methodRowsFromReport`). It used to be reached only as a side
38
+ // effect of `maintainability-engine.js` sitting somewhere in the serial path's
39
+ // import graph, which the worker's graph never included: 362 methods across
40
+ // 21 files scored zero via workers and scored fine serially, and
41
+ // `POOL_SERIAL_THRESHOLD` makes the worker path the only one a real repo
42
+ // takes. Anchoring it at the kernel makes the next worker entrypoint correct
43
+ // by construction rather than by an import nobody would guess is load-bearing.
44
+ installAstCompat();
45
+
20
46
  /**
21
47
  * Derive the raw per-method CRAP rows from an escomplex report.
22
48
  *
@@ -199,8 +225,12 @@ export { finalizeMethodRowsWithBaseline } from './crap-baseline-join.js';
199
225
  * produce `coverage: null` and `crap: null`. Callers apply their own
200
226
  * `requireCoverage` policy at the scanner level (`finalizeMethodRows`);
201
227
  * this kernel never decides to skip.
202
- * - A parse error returns an empty array — the file is unscorable, not
203
- * zero-complexity.
228
+ * - A parse error returns {@link UNSCORABLE} (`null`) — the file could not
229
+ * be scored at all, which is a different fact from "it has no methods"
230
+ * (`[]`). Story #5311: returning `[]` for both collapsed them, and every
231
+ * caller's drop path for an unscorable file became unreachable — the
232
+ * whole parse-failure class landed in the baseline as a clean zero.
233
+ * Callers MUST branch on `rows === null` before iterating.
204
234
  *
205
235
  * @param {string} source JavaScript source text (possibly transpiled).
206
236
  * @param {object|null} coverageForFile The inner value from a
@@ -216,7 +246,8 @@ export { finalizeMethodRowsWithBaseline } from './crap-baseline-join.js';
216
246
  * coverage: number|null,
217
247
  * crap: number|null,
218
248
  * coordinateSystem: 'original'|'transpiled',
219
- * }>}
249
+ * }>|null} The method rows, or {@link UNSCORABLE} when the source did not
250
+ * parse.
220
251
  */
221
252
  export function calculateCrapForSource(
222
253
  source,
@@ -227,7 +258,7 @@ export function calculateCrapForSource(
227
258
  try {
228
259
  report = escomplex.analyzeModule(source);
229
260
  } catch {
230
- return [];
261
+ return UNSCORABLE;
231
262
  }
232
263
  return methodRowsFromReport(report, coverageForFile, mapLine);
233
264
  }