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
package/docs/CHANGELOG.md CHANGED
@@ -15,6 +15,42 @@ All notable changes to this project will be documented in this file.
15
15
  -->
16
16
  <!-- markdownlint-disable-file MD004 MD012 MD037 -->
17
17
 
18
+ ## [2.58.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.57.0...mandrel-v2.58.0) (2026-09-12)
19
+
20
+
21
+ ### Added
22
+
23
+ * make the close test credit earnable on any test runner, and surface the gap at setup instead of mid-close ([#5324](https://github.com/dsj1984/mandrel/issues/5324)) ([#5329](https://github.com/dsj1984/mandrel/issues/5329)) ([04db145](https://github.com/dsj1984/mandrel/commit/04db145b8f1017121e129897f9207ab51db1c3f8))
24
+ * tickets-mode planning re-derives the Story instead of carrying the source ticket's shape: acceptance handles, tier suffixes, generated-artifact footprints and pinned identifiers ([#5323](https://github.com/dsj1984/mandrel/issues/5323)) ([#5326](https://github.com/dsj1984/mandrel/issues/5326)) ([5563e7a](https://github.com/dsj1984/mandrel/commit/5563e7a264864f78001beb19ce58160a1b483cda))
25
+
26
+
27
+ ### Fixed
28
+
29
+ * give base-sync and the Story-scope code review one base ref, so a stale local base branch cannot raise false blockers ([#5325](https://github.com/dsj1984/mandrel/issues/5325)) ([#5328](https://github.com/dsj1984/mandrel/issues/5328)) ([c4032b6](https://github.com/dsj1984/mandrel/commit/c4032b63a63c4cb8cc3f9c9956b6c646e5acd460))
30
+
31
+ ## [2.57.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.56.0...mandrel-v2.57.0) (2026-09-12)
32
+
33
+
34
+ ### ⚠ BREAKING CHANGES
35
+
36
+ * the .agentrc keys delivery.routing.freshCriticSampleRate, delivery.codeReview.maxFixScopeFiles, delivery.signals and delivery.quality.codingGuardrails.cyclomaticMustFix are removed; a 2.57.0 migration step strips them on upgrade.
37
+ * the .agentrc.json keys planning.complexityGate, planning.riskHeuristics, planning.failOnSharedEditors, planning.requireExplicitCrossStoryDeps, planning.failOnRegistryConflicts, planning.failOnLargeFanOut, planning.largeFanOutThreshold, planning.crossCuttingRegistries, planning.memoryPool.staleAfterDays and planning.memoryPool.growthDelta are removed; the planning block is strict, so a config still carrying one fails validation until the 2.57.0 migration step strips it on `mandrel update`. plan-persist.js no longer accepts --route-downgrade-reason, --allow-over-budget or --allow-large-fan-out, and a verify[] entry is a bare command with no tier suffix.
38
+
39
+ ### Added
40
+
41
+ * delivery diet: drop the sampling, round-ceiling, fix-scope, signal and cyclomatic knobs, script the ceremony derivation, let a bare test run earn credit, and make the light gate and footprint guard read the diff ([#5313](https://github.com/dsj1984/mandrel/issues/5313)) ([#5319](https://github.com/dsj1984/mandrel/issues/5319)) ([33fa9db](https://github.com/dsj1984/mandrel/commit/33fa9db4d01d8932bd886d380fe32b29db9a7bce))
42
+ * planning diet: delete the sizing, spec, lite-route and verify-tier limits, demote the footprint probes to warnings, and render the author prompt for the one-Story default ([#5312](https://github.com/dsj1984/mandrel/issues/5312)) ([#5318](https://github.com/dsj1984/mandrel/issues/5318)) ([cbcfdac](https://github.com/dsj1984/mandrel/commit/cbcfdacbca2cce9731e12c6cdfdab095d23b3e12))
43
+
44
+
45
+ ### Fixed
46
+
47
+ * score the CRAP worker path through the escomplex AST shim, and report an unscorable file as unscorable ([#5311](https://github.com/dsj1984/mandrel/issues/5311)) ([#5314](https://github.com/dsj1984/mandrel/issues/5314)) ([0a8e8c8](https://github.com/dsj1984/mandrel/commit/0a8e8c8cd8afb7af88b3d0d5a49e6359a91f3524))
48
+
49
+
50
+ ### Changed
51
+
52
+ * free the CLI entrypoints' logic from `main` so it can be tested, and tighten the CRAP floor back to 2 ([#5316](https://github.com/dsj1984/mandrel/issues/5316)) ([#5317](https://github.com/dsj1984/mandrel/issues/5317)) ([f67cfb3](https://github.com/dsj1984/mandrel/commit/f67cfb3b6e531c38300690e4dcd16a4de7dd18f8))
53
+
18
54
  ## [2.56.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.55.0...mandrel-v2.56.0) (2026-09-11)
19
55
 
20
56
 
@@ -4,8 +4,11 @@
4
4
  *
5
5
  * Exports an ordered array of check objects each shaped `{ name, run() }`.
6
6
  * `run()` returns `{ ok, detail, remedy? }` — `remedy` is present and
7
- * non-empty only when `ok` is false. The registry is the single source of
8
- * truth for which checks the doctor command runs and in what order.
7
+ * non-empty whenever `ok` is false, and on an `advisory` check that passes
8
+ * while still having something actionable to say (`test-credit-path`), which
9
+ * repeats it in `detail` because that is the field the doctor prints for a
10
+ * passing check. The registry is the single source of truth for which checks
11
+ * the doctor command runs and in what order.
9
12
  *
10
13
  * Checks run sequentially in the doctor runner (not in parallel) because
11
14
  * some checks are meaningless without a prerequisite having passed first
@@ -1176,6 +1179,90 @@ function probeConfiguredDriver(command, runner, projectRoot) {
1176
1179
  };
1177
1180
  }
1178
1181
 
1182
+ // ---------------------------------------------------------------------------
1183
+ // check: test-credit-path
1184
+ // ---------------------------------------------------------------------------
1185
+
1186
+ /**
1187
+ * The runner whose own green full run deposits the close `test` credit as a
1188
+ * side effect (`.agents/scripts/run-tests.js` → `lib/test-run-credit.js`).
1189
+ */
1190
+ const MANDREL_TEST_RUNNER = 'run-tests.js';
1191
+
1192
+ /**
1193
+ * The deposit that works whatever `npm test` resolves to: it spawns the
1194
+ * project's own suite and stamps the result, so it is honest by construction.
1195
+ */
1196
+ const TEST_CREDIT_DEPOSIT_COMMAND =
1197
+ 'node .agents/scripts/evidence-gate.js --standalone --scope-id <storyId> --gate test --worktree <workCwd> -- npm test';
1198
+
1199
+ /** Remedy shared by every shape that does not earn the credit on its own. */
1200
+ const TEST_CREDIT_REMEDY = `run the suite through the depositor instead of bare \`npm test\`: ${TEST_CREDIT_DEPOSIT_COMMAND}`;
1201
+
1202
+ /**
1203
+ * The project's `test` script — what `npm test` runs, and therefore what the
1204
+ * close `test` gate spawns. Deliberately read from `package.json` rather than
1205
+ * `project.commands.test`: the close gate's argv is the literal `npm test`, so
1206
+ * the npm script is the command whose shape decides whether a bare run
1207
+ * deposits anything.
1208
+ *
1209
+ * @param {string} projectRoot
1210
+ * @param {typeof fs.readFileSync} readFileImpl
1211
+ * @returns {string|null} The trimmed script, or null when there is none.
1212
+ */
1213
+ function readProjectTestScript(projectRoot, readFileImpl) {
1214
+ try {
1215
+ const raw = readFileImpl(path.join(projectRoot, 'package.json'), 'utf8');
1216
+ const script = JSON.parse(raw)?.scripts?.test;
1217
+ return typeof script === 'string' && script.trim().length > 0
1218
+ ? script.trim()
1219
+ : null;
1220
+ } catch {
1221
+ return null;
1222
+ }
1223
+ }
1224
+
1225
+ /**
1226
+ * Report whether this project's test command reaches mandrel's own runner,
1227
+ * and therefore whether a bare `npm test` deposits the close `test` credit
1228
+ * by itself (Story #5324).
1229
+ *
1230
+ * **Always `ok: true`.** A project on `vitest`, `jest` or any other runner is
1231
+ * a supported setup, not a broken install — the only thing it lacks is the
1232
+ * runner-side bonus deposit, and the remedy command covers it. So the check
1233
+ * informs rather than gates, and (unlike every other entry here) carries its
1234
+ * `remedy` alongside a passing verdict; the same guidance also rides in
1235
+ * `detail`, which is the field the doctor prints for a passing check.
1236
+ *
1237
+ * The runner is detected by name, never by executing anything: a script that
1238
+ * reaches `run-tests.js` indirectly reads as "own runner", which errs toward
1239
+ * naming the deposit command — correct on either shape.
1240
+ *
1241
+ * @param {{ projectRoot?: string, readFile?: typeof fs.readFileSync }} [opts]
1242
+ * @returns {{ ok: boolean, detail: string, remedy?: string }}
1243
+ */
1244
+ function runTestCreditPath({ projectRoot, readFile = fs.readFileSync } = {}) {
1245
+ const script = readProjectTestScript(projectRoot ?? process.cwd(), readFile);
1246
+ if (script === null) {
1247
+ return {
1248
+ ok: true,
1249
+ detail: `no \`test\` script in package.json — close still spawns \`npm test\`, so ${TEST_CREDIT_REMEDY}`,
1250
+ remedy: TEST_CREDIT_REMEDY,
1251
+ };
1252
+ }
1253
+ if (script.includes(MANDREL_TEST_RUNNER)) {
1254
+ return {
1255
+ ok: true,
1256
+ detail: `\`npm test\` → \`${script}\` reaches mandrel's runner — a green full run on a story branch deposits the close \`test\` credit itself`,
1257
+ };
1258
+ }
1259
+ return {
1260
+ ok: true,
1261
+ detail: `\`npm test\` → \`${script}\` is this project's own runner and never reaches \`${MANDREL_TEST_RUNNER}\`, so it deposits no close \`test\` credit and prints nothing — ${TEST_CREDIT_REMEDY}`,
1262
+ remedy: TEST_CREDIT_REMEDY,
1263
+ };
1264
+ }
1265
+
1179
1266
  /**
1180
1267
  * Ordered array of doctor checks. Each entry follows the
1181
1268
  * `{ name: string, run(opts?): { ok: boolean, detail: string, remedy?: string } }` contract.
@@ -1243,6 +1330,15 @@ export const registry = [
1243
1330
  advisory: true,
1244
1331
  run: (opts) => runVersionCurrent(opts),
1245
1332
  },
1333
+ {
1334
+ name: 'test-credit-path',
1335
+ // Non-fatal: `run()` always returns ok:true. A project on its own test
1336
+ // runner is a supported setup that simply has to deposit the close
1337
+ // `test` credit explicitly, so this reports a condition rather than
1338
+ // failing the install (Story #5324).
1339
+ advisory: true,
1340
+ run: (opts) => runTestCreditPath(opts),
1341
+ },
1246
1342
  ];
1247
1343
 
1248
1344
  export default registry;
@@ -59,6 +59,8 @@ import { retireEpicAcTags } from './steps/2.2.0-retire-epic-ac-tags.js';
59
59
  import { retireMaxSeedWords } from './steps/2.11.0-retire-max-seed-words.js';
60
60
  import { retireCodebaseSnapshot } from './steps/2.20.0-retire-codebase-snapshot.js';
61
61
  import { retireLintBaselineCommand } from './steps/2.32.0-retire-lint-baseline-command.js';
62
+ import { retireDeliveryLimitKnobs } from './steps/2.57.0-retire-delivery-limit-knobs.js';
63
+ import { retirePlanningLimitKnobs } from './steps/2.57.0-retire-planning-limit-knobs.js';
62
64
 
63
65
  /**
64
66
  * Ordered registry of migration steps. MUST stay sorted ascending by
@@ -78,6 +80,8 @@ export const migrations = [
78
80
  retireMaxSeedWords,
79
81
  retireCodebaseSnapshot,
80
82
  retireLintBaselineCommand,
83
+ retirePlanningLimitKnobs,
84
+ retireDeliveryLimitKnobs,
81
85
  ];
82
86
 
83
87
  /**
@@ -0,0 +1,45 @@
1
+ // lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js
2
+ /**
3
+ * Story #5313 — the delivery diet. Strip the retired `delivery.*` keys from a
4
+ * consumer's config:
5
+ *
6
+ * - `delivery.routing.freshCriticSampleRate` — the maker-checker sampling
7
+ * floor is gone; the standard profile routes purely off the derived
8
+ * change level (high or underivable → fresh critic, low → inline).
9
+ * - `delivery.codeReview.maxFixScopeFiles` — the auto-fix file-count
10
+ * ceiling bounded remediation by count rather than by risk.
11
+ * - `delivery.signals` (`rework.editsPerFile`, `retry.repeatCount`) — the
12
+ * detector thresholds and `SIGNALS_DEFAULTS` are retired wholesale.
13
+ * - `delivery.quality.codingGuardrails.cyclomaticMustFix` — the cyclomatic
14
+ * ratchet keeps its fixed ceiling of 12; `cyclomaticFlag` stays advisory.
15
+ *
16
+ * Every affected block carries `additionalProperties: false`, so a config
17
+ * still setting any of them fails validation on upgrade rather than warning.
18
+ * It sweeps **both** config surfaces (`.agentrc.json` and the gitignored
19
+ * `.agentrc.local.json`), because `config-resolver.js` deep-merges the
20
+ * overlay before the AJV gate runs.
21
+ *
22
+ * Pruning: `delivery.signals` is removed whole (`pruneDepth: 1` prunes an
23
+ * emptied `delivery`); the nested keys prune their emptied ancestors up to
24
+ * `delivery` itself. `delivery` is optional, so an emptied block is removed
25
+ * rather than left as `{}`. A sibling key that survives keeps its block.
26
+ */
27
+
28
+ import { createRetireAgentrcKeyStep } from '../helpers/retire-agentrc-key.js';
29
+
30
+ export const retireDeliveryLimitKnobs = createRetireAgentrcKeyStep({
31
+ version: '2.57.0',
32
+ description:
33
+ 'strip the retired delivery.* limit knobs from .agentrc.json — ' +
34
+ 'routing.freshCriticSampleRate, codeReview.maxFixScopeFiles, signals, ' +
35
+ 'and quality.codingGuardrails.cyclomaticMustFix (Story #5313)',
36
+ keys: [
37
+ { path: ['delivery', 'routing', 'freshCriticSampleRate'], pruneDepth: 2 },
38
+ { path: ['delivery', 'codeReview', 'maxFixScopeFiles'], pruneDepth: 2 },
39
+ { path: ['delivery', 'signals'], pruneDepth: 1 },
40
+ {
41
+ path: ['delivery', 'quality', 'codingGuardrails', 'cyclomaticMustFix'],
42
+ pruneDepth: 3,
43
+ },
44
+ ],
45
+ });
@@ -0,0 +1,59 @@
1
+ // lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js
2
+ /**
3
+ * Story #5312 — the planning diet. Strip the ten retired `planning.*` keys
4
+ * from a consumer's config:
5
+ *
6
+ * - `planning.complexityGate` — the plan-side lite claim and its persist
7
+ * backstop are gone; nothing reads the switch.
8
+ * - `planning.riskHeuristics` — the phrase list was empty in every consumer
9
+ * that resolved it, and the critic trigger that matched it went with the
10
+ * consolidation critic.
11
+ * - `planning.failOnSharedEditors`, `planning.requireExplicitCrossStoryDeps`,
12
+ * `planning.failOnRegistryConflicts`, `planning.failOnLargeFanOut`,
13
+ * `planning.largeFanOutThreshold`, `planning.crossCuttingRegistries` —
14
+ * every conflict finding is advisory now, and the registry and fan-out
15
+ * findings no longer exist.
16
+ * - `planning.memoryPool.staleAfterDays`, `planning.memoryPool.growthDelta`
17
+ * — the memory-hygiene advisory keeps only its index-byte arm.
18
+ *
19
+ * The `planning` block carries `additionalProperties: false`, so a config
20
+ * still setting any of them fails validation on upgrade rather than warning.
21
+ * It sweeps **both** config surfaces (`.agentrc.json` and the gitignored
22
+ * `.agentrc.local.json`), because `config-resolver.js` deep-merges the
23
+ * overlay before the AJV gate runs.
24
+ *
25
+ * Pruning: the two `memoryPool` keys prune the `memoryPool` object when it
26
+ * empties and then `planning` itself (`pruneDepth: 2`); the top-level keys
27
+ * prune `planning` (`pruneDepth: 1`). `planning` is optional, so an emptied
28
+ * block is removed rather than left as `{}`. A `memoryPool` that still
29
+ * carries `indexByteCeiling` survives untouched.
30
+ */
31
+
32
+ import { createRetireAgentrcKeyStep } from '../helpers/retire-agentrc-key.js';
33
+
34
+ const TOP_LEVEL_KEYS = [
35
+ 'complexityGate',
36
+ 'riskHeuristics',
37
+ 'failOnSharedEditors',
38
+ 'requireExplicitCrossStoryDeps',
39
+ 'failOnRegistryConflicts',
40
+ 'failOnLargeFanOut',
41
+ 'largeFanOutThreshold',
42
+ 'crossCuttingRegistries',
43
+ ];
44
+
45
+ export const retirePlanningLimitKnobs = createRetireAgentrcKeyStep({
46
+ version: '2.57.0',
47
+ description:
48
+ 'strip the retired planning.* limit knobs from .agentrc.json — ' +
49
+ 'complexityGate, riskHeuristics, the conflict-severity and fan-out ' +
50
+ 'knobs, and memoryPool.{staleAfterDays, growthDelta} (Story #5312)',
51
+ keys: [
52
+ ...TOP_LEVEL_KEYS.map((key) => ({
53
+ path: ['planning', key],
54
+ pruneDepth: 1,
55
+ })),
56
+ { path: ['planning', 'memoryPool', 'staleAfterDays'], pruneDepth: 2 },
57
+ { path: ['planning', 'memoryPool', 'growthDelta'], pruneDepth: 2 },
58
+ ],
59
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.56.0",
3
+ "version": "2.58.0",
4
4
  "description": "Claude Code-first opinionated workflow framework: instructions, skills, rules, and SDLC workflows that govern AI coding assistants.",
5
5
  "files": [
6
6
  ".agents/",
@@ -1,39 +0,0 @@
1
- // .agents/scripts/lib/framework-version.js
2
- /**
3
- * framework-version.js — the visible authoring-marker surface for the legacy
4
- * ticket-body provenance stamp.
5
- *
6
- * Tickets authored under v1 carry a hybrid stamp: a hidden
7
- * `<!-- meta: {"mandrel_version":"…","authored_at":"…"} -->` block plus a
8
- * single visible footer line
9
- * `> 🏷️ Authored with Mandrel v<version> · <YYYY-MM-DD>`. The producer of
10
- * new stamps was retired with the Epic write surface (#4574) — nothing stamps
11
- * a new ticket — but bodies already stamped are live in the backlog, so the
12
- * Story-body serializer/parser must keep round-tripping them:
13
- *
14
- * - {@link AUTHORED_MARKER_LINE_RE} lets the parser skip the marker line during
15
- * section parsing so it never pollutes the last structured section.
16
- * - {@link authoredMarkerLine} lets the serializer re-emit a byte-identical
17
- * marker line for a stamp it parsed, preserving provenance verbatim.
18
- *
19
- * This module imports nothing so it can be pulled in from the story-body
20
- * serializer without risking an import cycle.
21
- */
22
-
23
- /**
24
- * The visible authoring marker line. A blockquote so GitHub renders it as a
25
- * callout. Used in the Story-body parser to skip the line during section
26
- * parsing so it never pollutes the last structured section.
27
- */
28
- export const AUTHORED_MARKER_LINE_RE = /^\s*>\s*🏷️\s+Authored with Mandrel\b/;
29
-
30
- /**
31
- * Build the visible authoring marker line for a given stamp. The Story-body
32
- * serializer uses this to re-emit a legacy stamp it parsed, byte-identically.
33
- *
34
- * @param {{ version: string, authoredAt: string }} stamp
35
- * @returns {string}
36
- */
37
- export function authoredMarkerLine({ version, authoredAt }) {
38
- return `> 🏷️ Authored with Mandrel v${version} · ${authoredAt}`;
39
- }
@@ -1,223 +0,0 @@
1
- /**
2
- * consolidation-precondition.js — deterministic dispatch gate for the Phase
3
- * 8.3 Holistic Consolidation sub-agent (Story #4431, Epic #4429).
4
- *
5
- * The Phase 8.3 consolidation critic (`epic-plan-consolidate`) is a genuine
6
- * fresh-context `Agent` dispatch — every call re-pays the full always-loaded
7
- * context (`.agents/instructions.md` and its always-on rules, § 4). When the
8
- * decomposer's draft `tickets.json` already matches the Tech Spec's `##
9
- * Delivery Slicing` target 1:1 (same shippable-Story count, and the
10
- * `depends_on` shape already agrees with each slice's declared
11
- * "Independent?" answer), there is nothing left for the critic to
12
- * reconcile — dispatching it is pure token spend for a no-op. This module
13
- * computes that decision **deterministically**, off the same two inputs the
14
- * critic itself reads (the draft array and the Epic body's Delivery Slicing
15
- * table), so the planning workflow can skip the sub-agent
16
- * dispatch when it is provably safe to.
17
- *
18
- * **Fail-open by design.** Every ambiguous case — a missing or unparseable
19
- * Delivery Slicing section, an unparseable "Independent?" cell — resolves to
20
- * `dispatch: true`. This gate can only ever *save* a dispatch when it is
21
- * confident the critic has nothing to do; it never disables the critic's
22
- * ability to catch a real divergence. Since Epic #4474 PR6 this precondition
23
- * is one input to the risk/size-conditional dispatch layer
24
- * (`plan-critic-conditions.js`): reachability (8.4) is a deterministic
25
- * persist-side check (`plan-reachability.js`) and the pre-mortem critic
26
- * (8.5) is risk/size-gated; the deterministic ticket validator remains
27
- * unconditional.
28
- *
29
- * Pure, synchronous, no I/O — callers own reading `tickets.json` and the
30
- * Epic body off disk / the GitHub API.
31
- */
32
-
33
- import { DELIVERY_SLICING_RE as DELIVERY_SLICING_HEADING_RE } from '../ticket-body-sections.js';
34
-
35
- /** A row is a markdown table line: starts with `|` once trimmed. */
36
- const TABLE_ROW_RE = /^\|/;
37
-
38
- /** A markdown table separator row: `|---|:---:|---:|` (dashes, colons, pipes only). */
39
- const TABLE_SEPARATOR_RE = /^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?$/;
40
-
41
- /** The literal goal-section token that marks the wave-0 BDD scaffold Story. */
42
- const BDD_SCAFFOLD_GOAL_TOKEN = 'bdd-scaffold';
43
-
44
- /**
45
- * Split one markdown table row into trimmed cell strings.
46
- *
47
- * @param {string} line
48
- * @returns {string[]}
49
- */
50
- function splitTableRow(line) {
51
- let trimmed = line.trim();
52
- if (trimmed.startsWith('|')) trimmed = trimmed.slice(1);
53
- if (trimmed.endsWith('|')) trimmed = trimmed.slice(0, -1);
54
- return trimmed.split('|').map((cell) => cell.trim());
55
- }
56
-
57
- /**
58
- * Parse an "Independent?" cell per the pinned rule: match the cell's
59
- * leading word case-insensitively as `Yes` or `No`. Any other leading word
60
- * (or an empty cell) is unparseable and returns `null` — the caller must
61
- * fail open (`dispatch: true`) rather than guess.
62
- *
63
- * @param {string} cell
64
- * @returns {boolean|null} `true` for Yes, `false` for No, `null` when unparseable.
65
- */
66
- function parseIndependentCell(cell) {
67
- const match = String(cell ?? '')
68
- .trim()
69
- .match(/^[A-Za-z]+/);
70
- if (!match) return null;
71
- const word = match[0].toLowerCase();
72
- if (word === 'yes') return true;
73
- if (word === 'no') return false;
74
- return null;
75
- }
76
-
77
- /**
78
- * Locate and parse the `## Delivery Slicing` markdown table out of the Epic
79
- * body (which carries the folded Tech Spec sections — Story #4324). Returns
80
- * `null` when the heading is absent, no table follows it, the table has no
81
- * "Independent?" column, or any data row's "Independent?" cell is
82
- * unparseable — every one of those is a fail-open signal for the caller.
83
- *
84
- * @param {string} epicBody
85
- * @returns {{ slice: string, independent: boolean }[] | null}
86
- */
87
- export function parseDeliverySlicingTable(epicBody) {
88
- if (typeof epicBody !== 'string' || epicBody.length === 0) return null;
89
-
90
- const lines = epicBody.split(/\r?\n/);
91
- const headingIdx = lines.findIndex((line) =>
92
- DELIVERY_SLICING_HEADING_RE.test(line.trim()),
93
- );
94
- if (headingIdx === -1) return null;
95
-
96
- let i = headingIdx + 1;
97
- while (i < lines.length && lines[i].trim() === '') i++;
98
- if (i >= lines.length || !TABLE_ROW_RE.test(lines[i].trim())) return null;
99
-
100
- const headerCells = splitTableRow(lines[i]);
101
- i++;
102
- if (i >= lines.length || !TABLE_SEPARATOR_RE.test(lines[i].trim())) {
103
- return null;
104
- }
105
- i++;
106
-
107
- const independentIdx = headerCells.findIndex((cell) =>
108
- /independent/i.test(cell),
109
- );
110
- if (independentIdx === -1) return null;
111
-
112
- const rows = [];
113
- while (i < lines.length && TABLE_ROW_RE.test(lines[i].trim())) {
114
- const cells = splitTableRow(lines[i]);
115
- const independent = parseIndependentCell(cells[independentIdx]);
116
- if (independent === null) return null; // unparseable cell → fail open
117
- rows.push({ slice: (cells[0] ?? '').trim(), independent });
118
- i++;
119
- }
120
-
121
- return rows.length > 0 ? rows : null;
122
- }
123
-
124
- /**
125
- * True when `story` is the recognized wave-0 BDD scaffold Story — identified
126
- * by the literal `bdd-scaffold` goal token the decomposer prompt
127
- * skill's WAVE-0 BDD SCAFFOLD STORY section requires. Scaffold Stories are
128
- * not a Delivery Slicing slice, so they are excluded from the count
129
- * comparison — BDD-adopting consumer repos still benefit from the
130
- * precondition gate rather than always paying the 8.3 dispatch.
131
- *
132
- * @param {{ body?: unknown }} story
133
- * @returns {boolean}
134
- */
135
- function isBddScaffoldStory(story) {
136
- const body = story?.body;
137
- return (
138
- typeof body === 'string' &&
139
- body.toLowerCase().includes(BDD_SCAFFOLD_GOAL_TOKEN)
140
- );
141
- }
142
-
143
- /**
144
- * Evaluate whether the Phase 8.3 consolidation sub-agent needs to run.
145
- *
146
- * @param {object} input
147
- * @param {object[]} input.draftStories - The draft `tickets.json` array
148
- * (the decomposer's output) — raw Story ticket objects with
149
- * top-level `slug` / `depends_on` / `body` (serialized string).
150
- * @param {string} input.epicBody - The Epic body carrying the folded Tech
151
- * Spec sections (`## Delivery Slicing` onward).
152
- * @returns {{ dispatch: boolean, cause: 'match'|'divergence'|'fail-open', reasons: string[] }}
153
- * `dispatch: false` only when the draft matches the Delivery Slicing table
154
- * 1:1 in count and dependency shape; `dispatch: true` (with `reasons`)
155
- * otherwise, including every fail-open case. `cause` distinguishes a
156
- * **confirmed** divergence (count or dependency-shape mismatch) from the
157
- * fail-open ambiguity (missing/unparseable table) — the #4474 PR6
158
- * conditional-dispatch layer treats only the former as a firing condition
159
- * on small drafts.
160
- */
161
- export function evaluateConsolidationPrecondition({ draftStories, epicBody }) {
162
- if (!Array.isArray(draftStories)) {
163
- throw new TypeError(
164
- 'evaluateConsolidationPrecondition: draftStories must be an array',
165
- );
166
- }
167
-
168
- const slicing = parseDeliverySlicingTable(epicBody);
169
- if (!slicing) {
170
- return {
171
- dispatch: true,
172
- cause: 'fail-open',
173
- reasons: [
174
- 'Delivery Slicing section is missing or unparseable — fail-open to the critic.',
175
- ],
176
- };
177
- }
178
-
179
- const slicedStories = draftStories.filter(
180
- (story) => !isBddScaffoldStory(story),
181
- );
182
-
183
- if (slicedStories.length !== slicing.length) {
184
- return {
185
- dispatch: true,
186
- cause: 'divergence',
187
- reasons: [
188
- `Story count diverges from Delivery Slicing: ${slicing.length} proposed slice(s) vs ${slicedStories.length} non-scaffold draft Story(ies).`,
189
- ],
190
- };
191
- }
192
-
193
- const reasons = [];
194
- for (let idx = 0; idx < slicing.length; idx++) {
195
- const slice = slicing[idx];
196
- const story = slicedStories[idx];
197
- const dependsOn = Array.isArray(story?.depends_on) ? story.depends_on : [];
198
- const hasDeps = dependsOn.length > 0;
199
- const storyLabel = story?.slug ?? story?.title ?? `<story ${idx + 1}>`;
200
-
201
- if (slice.independent === false && !hasDeps) {
202
- reasons.push(
203
- `Slice "${slice.slice}" (position ${idx + 1}) is marked Independent: No but draft Story "${storyLabel}" declares no depends_on.`,
204
- );
205
- } else if (slice.independent === true && hasDeps) {
206
- reasons.push(
207
- `Slice "${slice.slice}" (position ${idx + 1}) is marked Independent: Yes but draft Story "${storyLabel}" declares depends_on [${dependsOn.join(', ')}].`,
208
- );
209
- }
210
- }
211
-
212
- if (reasons.length > 0) {
213
- return { dispatch: true, cause: 'divergence', reasons };
214
- }
215
-
216
- return {
217
- dispatch: false,
218
- cause: 'match',
219
- reasons: [
220
- `Draft matches Delivery Slicing 1:1 in count and dependency shape (${slicing.length} slice(s)) — skipping the 8.3 consolidation dispatch.`,
221
- ],
222
- };
223
- }
@@ -1,97 +0,0 @@
1
- /**
2
- * Fan-out / soft-conflict gates used by plan-persist (extracted from the
3
- * retired epic-plan-decompose persist phase in Stage 5).
4
- *
5
- * @module lib/orchestration/plan-persist/fan-out-gate
6
- */
7
-
8
- import { Logger } from '../../Logger.js';
9
- import {
10
- CONFLICT_KINDS,
11
- renderFanOutEvidence,
12
- renderFanOutRemedy,
13
- renderHardConflictError,
14
- } from '../ticket-validator-conflicts.js';
15
-
16
- /**
17
- * @param {object[]} findings
18
- * @param {boolean} allowLargeFanOut
19
- * @param {string} [tag]
20
- */
21
- export function enforceFanOutGate(
22
- findings,
23
- allowLargeFanOut,
24
- tag = 'plan-persist',
25
- ) {
26
- const fanOut = (findings ?? []).filter((f) => f.kind === 'fan-out-warning');
27
- if (fanOut.length === 0) return;
28
- if (allowLargeFanOut) {
29
- for (const f of fanOut) {
30
- Logger.warn(
31
- `[${tag}] Persisting a large-fan-out deletion: ` +
32
- `Story "${f.storySlug}" deletes "${f.path}" with ${f.callSiteCount} ` +
33
- `importer(s) (threshold ${f.threshold}). Operator override --allow-large-fan-out.` +
34
- renderFanOutEvidence(f),
35
- );
36
- }
37
- return;
38
- }
39
- const lines = fanOut
40
- .map(
41
- (f) =>
42
- ` - Story "${f.storySlug}" deletes "${f.path}" — ` +
43
- `${f.callSiteCount} importer(s) (threshold ${f.threshold})` +
44
- renderFanOutEvidence(f),
45
- )
46
- .join('\n');
47
- // Each finding carries its own remedy — a rename-shaped deletion has no
48
- // subsystems to split across, so a blanket "split it up" footer would be
49
- // wrong advice for it (Story #4547).
50
- const remedies = [...new Set(fanOut.map((f) => renderFanOutRemedy(f)))];
51
- throw new Error(
52
- `[${tag}] ${fanOut.length} Task(s) declare large-fan-out deletions:\n${lines}\n\n` +
53
- remedies.join('\n\n'),
54
- );
55
- }
56
-
57
- /**
58
- * Report every soft finding the validator produced, each under its own kind.
59
- *
60
- * Only the {@link CONFLICT_KINDS} are cross-Story conflicts. The advisory
61
- * kinds — `spec-word-budget` and the sizing findings — are single-Story
62
- * nudges, and announcing them as conflicts overstated them and taught readers
63
- * to discount the whole channel (Story #4907). `fan-out-warning` is excluded
64
- * throughout: {@link enforceFanOutGate} owns it and has already either thrown
65
- * or logged the override.
66
- *
67
- * @param {object[]} findings
68
- * @param {string} [tag]
69
- */
70
- export function surfaceSoftConflictFindings(findings, tag = 'plan-persist') {
71
- const soft = (findings ?? []).filter(
72
- (f) => f?.severity === 'soft' && f?.kind !== 'fan-out-warning',
73
- );
74
- if (soft.length === 0) return;
75
- const conflicts = soft.filter((f) => CONFLICT_KINDS.has(f?.kind));
76
- const advisories = soft.filter((f) => !CONFLICT_KINDS.has(f?.kind));
77
- if (conflicts.length > 0) {
78
- Logger.warn(
79
- `[${tag}] ${conflicts.length} soft cross-Story conflict finding(s) — review before approving the plan:`,
80
- );
81
- for (const finding of conflicts) {
82
- Logger.warn(
83
- `[${tag}] soft conflict: ${renderHardConflictError(finding)}`,
84
- );
85
- }
86
- }
87
- if (advisories.length > 0) {
88
- Logger.warn(
89
- `[${tag}] ${advisories.length} advisory finding(s) — the persist proceeds:`,
90
- );
91
- for (const finding of advisories) {
92
- Logger.warn(
93
- `[${tag}] advisory (${finding.kind}): ${renderHardConflictError(finding)}`,
94
- );
95
- }
96
- }
97
- }