mandrel 2.36.0 → 2.38.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 (225) hide show
  1. package/.agents/README.md +16 -16
  2. package/.agents/agents/plan-critic.md +1 -1
  3. package/.agents/docs/SDLC.md +40 -40
  4. package/.agents/docs/configuration.md +41 -16
  5. package/.agents/docs/execution-reference.md +3 -3
  6. package/.agents/docs/quality-gates.md +2 -2
  7. package/.agents/docs/workflows.md +6 -6
  8. package/.agents/instructions.md +5 -5
  9. package/.agents/rules/changelog-style.md +3 -3
  10. package/.agents/rules/ci-remediation.md +1 -1
  11. package/.agents/rules/git-conventions-reference.md +4 -4
  12. package/.agents/rules/testing-standards.md +56 -0
  13. package/.agents/schemas/agentrc.schema.json +9 -9
  14. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  15. package/.agents/schemas/validation-evidence.schema.json +1 -1
  16. package/.agents/scripts/acceptance-eval.js +1 -1
  17. package/.agents/scripts/apply-quality-bootstrap.js +1 -1
  18. package/.agents/scripts/audit-to-stories.js +2 -2
  19. package/.agents/scripts/boot-sweep.js +7 -1
  20. package/.agents/scripts/check-context-budget.js +195 -103
  21. package/.agents/scripts/check-cyclomatic.js +112 -42
  22. package/.agents/scripts/check-doc-links.js +32 -4
  23. package/.agents/scripts/check-generated-validator.js +202 -0
  24. package/.agents/scripts/check-lifecycle-lint.js +2 -68
  25. package/.agents/scripts/check-schema-references.js +2 -28
  26. package/.agents/scripts/check-test-temp-hygiene.js +1 -1
  27. package/.agents/scripts/check-workflow-cli-lint.js +1 -1
  28. package/.agents/scripts/deliver-light.js +10 -10
  29. package/.agents/scripts/deliver-recover.js +1 -1
  30. package/.agents/scripts/drain-pending-cleanup.js +19 -2
  31. package/.agents/scripts/evidence-gate.js +1 -1
  32. package/.agents/scripts/generate-workflows-doc.js +1 -1
  33. package/.agents/scripts/lib/audit-suite/selector.js +1 -1
  34. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +1 -1
  35. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +1 -1
  36. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +5 -5
  37. package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +1 -1
  38. package/.agents/scripts/lib/baselines/components.js +32 -2
  39. package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
  40. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  41. package/.agents/scripts/lib/bdd-scenario-budget.js +1 -1
  42. package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
  43. package/.agents/scripts/lib/bdd-step-index.js +1 -1
  44. package/.agents/scripts/lib/bootstrap/branch-protection.js +2 -2
  45. package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
  46. package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
  47. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +1 -1
  48. package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
  49. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
  50. package/.agents/scripts/lib/checks/core-bare-clean.js +1 -1
  51. package/.agents/scripts/lib/checks/loop-health.js +1 -1
  52. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +1 -1
  53. package/.agents/scripts/lib/cli-args.js +1 -3
  54. package/.agents/scripts/lib/close-validation/gates.js +1 -1
  55. package/.agents/scripts/lib/close-validation/process.js +1 -1
  56. package/.agents/scripts/lib/command-header.js +1 -1
  57. package/.agents/scripts/lib/config/delivery-routing.js +1 -1
  58. package/.agents/scripts/lib/config/explain.js +1 -1
  59. package/.agents/scripts/lib/config/runners.js +2 -2
  60. package/.agents/scripts/lib/config/runtime.js +1 -1
  61. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  62. package/.agents/scripts/lib/config/temp-paths.js +2 -2
  63. package/.agents/scripts/lib/config-settings-schema-delivery.js +3 -3
  64. package/.agents/scripts/lib/config-settings-schema-quality.js +6 -6
  65. package/.agents/scripts/lib/config-settings-schema.js +49 -9
  66. package/.agents/scripts/lib/coverage-baseline.js +2 -2
  67. package/.agents/scripts/lib/cpu-pool.js +90 -10
  68. package/.agents/scripts/lib/crap-utils.js +6 -2
  69. package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
  70. package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
  71. package/.agents/scripts/lib/dependency-version.js +86 -0
  72. package/.agents/scripts/lib/duplicate-search.js +2 -2
  73. package/.agents/scripts/lib/errors/index.js +1 -1
  74. package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
  75. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +5 -5
  76. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +2 -2
  77. package/.agents/scripts/lib/findings/classify-finding.js +1 -1
  78. package/.agents/scripts/lib/findings/promote-finding.js +12 -12
  79. package/.agents/scripts/lib/findings/route-finding.js +2 -2
  80. package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
  81. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  82. package/.agents/scripts/lib/install-cmd-parser.js +1 -1
  83. package/.agents/scripts/lib/knip-entry-sync.js +1 -57
  84. package/.agents/scripts/lib/label-constants.js +2 -2
  85. package/.agents/scripts/lib/label-taxonomy.js +1 -1
  86. package/.agents/scripts/lib/maintainability-utils.js +108 -10
  87. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  88. package/.agents/scripts/lib/onboard/init-tail.js +4 -4
  89. package/.agents/scripts/lib/onboard/scaffold-docs.js +2 -2
  90. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
  91. package/.agents/scripts/lib/orchestration/code-review.js +4 -4
  92. package/.agents/scripts/lib/orchestration/complexity-gate.js +5 -5
  93. package/.agents/scripts/lib/orchestration/deliver-recover.js +3 -3
  94. package/.agents/scripts/lib/orchestration/docs-digest.js +3 -3
  95. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +2 -2
  96. package/.agents/scripts/lib/orchestration/light-backstop.js +1 -1
  97. package/.agents/scripts/lib/orchestration/light-escalation.js +6 -6
  98. package/.agents/scripts/lib/orchestration/light-suitability.js +19 -19
  99. package/.agents/scripts/lib/orchestration/plan-context.js +4 -4
  100. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +2 -2
  101. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +2 -2
  102. package/.agents/scripts/lib/orchestration/plan-metrics.js +1 -1
  103. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +1 -1
  104. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +3 -3
  105. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +6 -6
  106. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +86 -25
  107. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -3
  108. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +6 -6
  109. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +7 -7
  110. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -1
  111. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +3 -3
  112. package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
  113. package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
  114. package/.agents/scripts/lib/orchestration/remote-verifier.js +2 -2
  115. package/.agents/scripts/lib/orchestration/resolve-stories.js +4 -4
  116. package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
  117. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
  118. package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
  119. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
  120. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
  121. package/.agents/scripts/lib/orchestration/run-epilogue.js +2 -2
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +2 -2
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +1 -1
  124. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +5 -1
  125. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
  126. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +1 -1
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +3 -3
  128. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -2
  129. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  130. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
  131. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +14 -12
  132. package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
  133. package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
  134. package/.agents/scripts/lib/orchestration/ticket-validator.js +1 -1
  135. package/.agents/scripts/lib/orchestration/ticketing/reads.js +6 -4
  136. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
  137. package/.agents/scripts/lib/preflight-runner.js +3 -3
  138. package/.agents/scripts/lib/qa/qa-session.js +1 -1
  139. package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
  140. package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
  141. package/.agents/scripts/lib/signals/schema.js +1 -1
  142. package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
  143. package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
  144. package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
  145. package/.agents/scripts/lib/story-adjacency.js +2 -2
  146. package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
  147. package/.agents/scripts/lib/test-isolate/runner.js +3 -3
  148. package/.agents/scripts/lib/test-runner-contract.js +14 -6
  149. package/.agents/scripts/lib/test-tiers.js +135 -18
  150. package/.agents/scripts/lib/ticket-body-sections.js +1 -1
  151. package/.agents/scripts/lib/transpile.js +16 -3
  152. package/.agents/scripts/lib/util/concurrent-map.js +1 -1
  153. package/.agents/scripts/lib/util/parse-id-list.js +1 -1
  154. package/.agents/scripts/lib/wave-runner/live-probe.js +4 -4
  155. package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
  156. package/.agents/scripts/lib/workflow-closure.js +1 -1
  157. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +170 -32
  158. package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
  159. package/.agents/scripts/lint-issue-body.js +3 -3
  160. package/.agents/scripts/plan-context.js +4 -4
  161. package/.agents/scripts/plan-critics.js +2 -2
  162. package/.agents/scripts/plan-persist.js +2 -2
  163. package/.agents/scripts/plan-run-epilogue.js +2 -2
  164. package/.agents/scripts/pr-watch-with-update.js +305 -137
  165. package/.agents/scripts/providers/github/board-add.js +1 -1
  166. package/.agents/scripts/providers/github/errors.js +1 -1
  167. package/.agents/scripts/providers/github/tickets.js +114 -21
  168. package/.agents/scripts/quality-preview.js +162 -70
  169. package/.agents/scripts/resolve-stories.js +3 -3
  170. package/.agents/scripts/resync-status-column.js +1 -1
  171. package/.agents/scripts/run-lint.js +1 -1
  172. package/.agents/scripts/run-test-profile.js +8 -5
  173. package/.agents/scripts/run-tests.js +79 -14
  174. package/.agents/scripts/single-story-close.js +2 -4
  175. package/.agents/scripts/single-story-init.js +10 -6
  176. package/.agents/scripts/stories-wave-tick.js +8 -8
  177. package/.agents/skills/core/idea-refinement/SKILL.md +5 -5
  178. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  179. package/.agents/skills/skills.index.json +1 -1
  180. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -1
  181. package/.agents/workflows/audit-data-model.md +1 -1
  182. package/.agents/workflows/audit-documentation.md +2 -2
  183. package/.agents/workflows/audit-to-stories.md +11 -11
  184. package/.agents/workflows/git-deliver.md +4 -4
  185. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  186. package/.agents/workflows/helpers/audit-lens-core.md +1 -1
  187. package/.agents/workflows/helpers/code-review.md +4 -4
  188. package/.agents/workflows/helpers/deliver-light.md +26 -26
  189. package/.agents/workflows/helpers/deliver-reference.md +15 -7
  190. package/.agents/workflows/helpers/deliver-story-reference.md +9 -4
  191. package/.agents/workflows/helpers/deliver-story.md +6 -7
  192. package/.agents/workflows/helpers/diagnose.md +2 -2
  193. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  194. package/.agents/workflows/helpers/plan-reference.md +9 -9
  195. package/.agents/workflows/helpers/qa-core.md +8 -8
  196. package/.agents/workflows/helpers/worktree-lifecycle.md +6 -6
  197. package/.agents/workflows/{deliver.md → mandrel-deliver.md} +15 -19
  198. package/.agents/workflows/{plan.md → mandrel-plan.md} +9 -9
  199. package/.agents/workflows/memory-consolidate.md +4 -4
  200. package/.agents/workflows/prototype.md +3 -3
  201. package/.agents/workflows/qa-assist.md +12 -12
  202. package/.agents/workflows/qa-explore.md +6 -6
  203. package/.agents/workflows/qa-run.md +1 -1
  204. package/README.md +7 -7
  205. package/docs/CHANGELOG.md +43 -0
  206. package/lib/cli/doctor.js +1 -1
  207. package/lib/cli/init.js +1 -1
  208. package/lib/cli/migrate.js +100 -94
  209. package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
  210. package/lib/migrations/index.js +32 -33
  211. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
  212. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
  213. package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
  214. package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +1 -1
  215. package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
  216. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
  217. package/package.json +7 -3
  218. package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
  219. package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
  220. package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
  221. package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
  222. package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
  223. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
  224. package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
  225. package/.agents/scripts/lib/orchestration/recut.js +0 -56
@@ -31,7 +31,7 @@ workflow states only the `/qa-explore`-specific phases (Plan / Capture) plus a
31
31
  Constraints delta.
32
32
 
33
33
  > **When to run**: ad-hoc agent-driven exploration of a freshly delivered Story
34
- > or Feature, a regression sweep over a risky surface before `/deliver`, or a
34
+ > or Feature, a regression sweep over a risky surface before `/mandrel-deliver`, or a
35
35
  > structured agent-driven bug-hunt captured into a triageable ledger.
36
36
  >
37
37
  > **Skills**: `core/qa-coverage-mapping`, `stack/qa/qa-explore-driving`
@@ -198,14 +198,14 @@ For each observation the agent makes while driving:
198
198
  Route the captured ledger through the shared classify → route → disposition →
199
199
  promote procedure in [`helpers/qa-core.md`](helpers/qa-core.md), with the
200
200
  operator deciding each `file` / `defer` / `dismiss` and every write
201
- operator-gated. `file` findings are promoted through `/plan` (never a raw
201
+ operator-gated. `file` findings are promoted through `/mandrel-plan` (never a raw
202
202
  Issue); `defer` carries an item forward as backlog; `dismiss` marks it
203
203
  non-actionable.
204
204
 
205
205
  After triage, write the updated dispositions back to the ledger (still under
206
206
  `temp/qa/`), and summarize: items captured, the driving method used, classes,
207
207
  routes (`new`/`update-existing`/`duplicate`/`regression-of-closed`), the
208
- Stories (`/plan --seed-file`) promoted, and the deferred rolling backlog a
208
+ Stories (`/mandrel-plan --seed-file`) promoted, and the deferred rolling backlog a
209
209
  resumed session will pick up.
210
210
 
211
211
  ---
@@ -238,11 +238,11 @@ the `/qa-explore`-specific deltas are:
238
238
 
239
239
  ## See also
240
240
 
241
- - [`/plan`](plan.md) — the planning pipeline Triage chains into for a
241
+ - [`/mandrel-plan`](mandrel-plan.md) — the planning pipeline Triage chains into for a
242
242
  `file`-dispositioned finding. The plan→deliver hard stop is preserved.
243
243
  - [`/qa-assist`](qa-assist.md) — the human-led sibling that enriches a single
244
- operator observation and triages through the same `/plan` handoff.
244
+ operator observation and triages through the same `/mandrel-plan` handoff.
245
245
  - [`/audit-to-stories`](audit-to-stories.md) — the precedent for the
246
- findings → `/plan` handoff and the shared fingerprint-footer dedup contract.
246
+ findings → `/mandrel-plan` handoff and the shared fingerprint-footer dedup contract.
247
247
  - [`helpers/qa-core.md`](helpers/qa-core.md) — the shared contract/session/
248
248
  redaction/QaLedgerItem/triage/HITL core.
@@ -26,7 +26,7 @@ the agent never invents those decisions in prose.
26
26
 
27
27
  > **When to run**: during sprint testing to exercise a targeted slice of the
28
28
  > acceptance suite (a feature, a tag expression, or a domain), for regression
29
- > passes before `/deliver`, or on demand while debugging a Story's
29
+ > passes before `/mandrel-deliver`, or on demand while debugging a Story's
30
30
  > user-visible behavior in a live browser.
31
31
  >
32
32
  > **Skills**: `stack/qa/gherkin-authoring`, `stack/qa/playwright-bdd`
package/README.md CHANGED
@@ -30,26 +30,26 @@ provisions both as part of a cold start (`git init` → `gh repo create --push`
30
30
  The canonical cold-start path is one command, then one slash command:
31
31
 
32
32
  ```bash
33
- npx mandrel init # install mandrel → sync → prompt → bootstrap → onboarding tail → /plan handoff
33
+ npx mandrel init # install mandrel → sync → prompt → bootstrap → onboarding tail → /mandrel-plan handoff
34
34
  ```
35
35
 
36
36
  ```text
37
37
  # then, inside Claude Code (commands load from .claude/commands/):
38
- /plan --seed "…" # interrogate → author one Story (default) → persist
39
- /deliver <id> # story-<id> → PR → main
38
+ /mandrel-plan --seed "…" # interrogate → author one Story (default) → persist
39
+ /mandrel-deliver <id> # story-<id> → PR → main
40
40
  ```
41
41
 
42
42
  `npx mandrel init` installs `mandrel` (when `./.agents/` is absent),
43
43
  materializes it via `mandrel sync`, then asks whether to **configure now**
44
44
  (option 1 → runs `bootstrap.js`, then the onboarding tail: stack detection,
45
- docs scaffolding offer, `mandrel doctor` readiness gate, and a `/plan`
45
+ docs scaffolding offer, `mandrel doctor` readiness gate, and a `/mandrel-plan`
46
46
  handoff) or stop at **just the files** (option 2 → re-run `mandrel init`
47
47
  any time to configure). Pass `--assume-yes` for a non-interactive run that
48
48
  proceeds straight to configure (and forwards the flag to bootstrap). When
49
49
  `./.agents/` is already present (you ran `npm install mandrel` first), `init`
50
50
  skips the install/sync and goes straight to the prompt. Once `mandrel init`
51
- completes, you land at the `/plan` handoff — run `/plan --seed "<idea>"` to
52
- author your first Story, then deliver it with `/deliver <storyId>`
51
+ completes, you land at the `/mandrel-plan` handoff — run `/mandrel-plan --seed "<idea>"` to
52
+ author your first Story, then deliver it with `/mandrel-deliver <storyId>`
53
53
  (`story-<id>` → PR → `main`).
54
54
 
55
55
  ### Manual equivalent
@@ -164,7 +164,7 @@ npx mandrel doctor # verify the install
164
164
  Mandrel's effectiveness is measured by a separate companion repo,
165
165
  **[mandrel-bench](https://github.com/dsj1984/mandrel-bench)** — a *consumer* of
166
166
  the published `mandrel` package. It pins a specific framework version,
167
- materializes it via `mandrel sync`, and drives Mandrel's own `/plan`→`/deliver`
167
+ materializes it via `mandrel sync`, and drives Mandrel's own `/mandrel-plan`→`/mandrel-deliver`
168
168
  pipeline (plus a bare-model control) over a scenario corpus. Each run is scored
169
169
  across five dimensions — Quality, Planning fidelity, and Autonomy (what the
170
170
  scaffolding *buys*) versus Efficiency and Overhead ratio (what it *costs*) —
package/docs/CHANGELOG.md CHANGED
@@ -15,6 +15,49 @@ 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.38.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.37.0...mandrel-v2.38.0) (2026-09-04)
19
+
20
+
21
+ ### ⚠ BREAKING CHANGES
22
+
23
+ * the `/plan` and `/deliver` slash commands are renamed to `/mandrel-plan` and `/mandrel-deliver`. Run `mandrel sync` (or `npm run sync:commands`) to reap the old command files and project the new ones. No `/plan` or `/deliver` alias remains.
24
+
25
+ ### Added
26
+
27
+ * rename /plan and /deliver to /mandrel-plan and /mandrel-deliver ([#5126](https://github.com/dsj1984/mandrel/issues/5126)) ([#5127](https://github.com/dsj1984/mandrel/issues/5127)) ([945d8de](https://github.com/dsj1984/mandrel/commit/945d8de4a41aeca7adc89c009a0252f5bb5387b4))
28
+
29
+ ## [2.37.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.36.0...mandrel-v2.37.0) (2026-09-03)
30
+
31
+
32
+ ### ⚠ BREAKING CHANGES
33
+
34
+ * `single-story-close.js` now exits non-zero on `--dry-run` or `--no-evidence` instead of silently ignoring them. Neither flag ever had an effect; a caller passing one was getting a full real close.
35
+
36
+ ### Added
37
+
38
+ * cut the test suite's child-process budget: seed the e2e consumer once and move tests/e2e to its own tier, replace CLI cold-start spawns with in-process calls, and settle the isolation trade-off ([#5111](https://github.com/dsj1984/mandrel/issues/5111)) ([#5122](https://github.com/dsj1984/mandrel/issues/5122)) ([a4fbd56](https://github.com/dsj1984/mandrel/commit/a4fbd5628b8bc11c918e0ff658fe62baf078a17f))
39
+ * detect high-severity advisories on main between PRs, and bound the TypeScript devDependency range ([#5104](https://github.com/dsj1984/mandrel/issues/5104)) ([#5107](https://github.com/dsj1984/mandrel/issues/5107)) ([af8f1b8](https://github.com/dsj1984/mandrel/commit/af8f1b8ee28300391a3a43cca1d43ece5851ce83))
40
+
41
+
42
+ ### Fixed
43
+
44
+ * close five concurrency holes: sweep-lock steal and heartbeat, one lock for the merged-branch reap, atomic pending-cleanup manifest, idempotent createIssue retry, additive lease claim ([#5112](https://github.com/dsj1984/mandrel/issues/5112)) ([#5119](https://github.com/dsj1984/mandrel/issues/5119)) ([8651514](https://github.com/dsj1984/mandrel/commit/86515147381cf653c1e41c01f6405f5333826253))
45
+ * **deps:** bump fast-uri to 3.1.7 for four high-severity advisories ([#5103](https://github.com/dsj1984/mandrel/issues/5103)) ([06c37b1](https://github.com/dsj1984/mandrel/commit/06c37b11952fb8079027c0beceef831bed677485))
46
+ * **drain-pending-cleanup:** honour --no-escalate and guard advertised --no-* flags (refs [#5101](https://github.com/dsj1984/mandrel/issues/5101)) ([#5106](https://github.com/dsj1984/mandrel/issues/5106)) ([52ef3cb](https://github.com/dsj1984/mandrel/commit/52ef3cb0c546714900474c0613d78b23bc40a068))
47
+ * restore the suite profiler, make run-tests --help print usage, and stop three tests burning worker slots on real backoff and a live-repo scan ([#5110](https://github.com/dsj1984/mandrel/issues/5110)) ([#5115](https://github.com/dsj1984/mandrel/issues/5115)) ([dbe0742](https://github.com/dsj1984/mandrel/commit/dbe07420a7d8d3c99016968d9c15e45da64d0de2))
48
+ * single-story-close advertises --dry-run and --no-evidence but never wired either: retire both and reject them loudly ([#5100](https://github.com/dsj1984/mandrel/issues/5100)) ([#5102](https://github.com/dsj1984/mandrel/issues/5102)) ([2f4dd52](https://github.com/dsj1984/mandrel/commit/2f4dd52c682bcc3697f67124d6dc574e209ab204))
49
+
50
+
51
+ ### Performance
52
+
53
+ * stop the baseline gates doing pointless heavy work: lazy TypeScript, small-batch in-process scoring, precompiled globs, diff-scoped cyclomatic, precompiled .agentrc validator ([#5109](https://github.com/dsj1984/mandrel/issues/5109)) ([#5117](https://github.com/dsj1984/mandrel/issues/5117)) ([6b04d5e](https://github.com/dsj1984/mandrel/commit/6b04d5e7c014b4bc052afbef8fd88d85c7a6b00f))
54
+
55
+
56
+ ### Changed
57
+
58
+ * collapse the tripled semver comparator and the copy-pasted migration-step scaffold in the published lib/ payload ([#5113](https://github.com/dsj1984/mandrel/issues/5113)) ([#5116](https://github.com/dsj1984/mandrel/issues/5116)) ([ae2b6d6](https://github.com/dsj1984/mandrel/commit/ae2b6d6c40b26ea6d2f94b07698867863011a659))
59
+ * extract one JavaScript comment stripper, bring two cyclomatic-20 CLI mains under the ceiling, and delete the orchestration payload’s dead code ([#5114](https://github.com/dsj1984/mandrel/issues/5114)) ([#5118](https://github.com/dsj1984/mandrel/issues/5118)) ([747d9a5](https://github.com/dsj1984/mandrel/commit/747d9a5bce66a6230fe6f46d132b3e8aacbc5a09))
60
+
18
61
  ## [2.36.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.35.0...mandrel-v2.36.0) (2026-08-29)
19
62
 
20
63
 
package/lib/cli/doctor.js CHANGED
@@ -122,7 +122,7 @@ export function formatClosureReport({
122
122
 
123
123
  /**
124
124
  * Best-effort write of the doctor verdict to `temp/doctor-result.json` under
125
- * the consumer root so downstream workflows (e.g. the `/plan` first-run
125
+ * the consumer root so downstream workflows (e.g. the `/mandrel-plan` first-run
126
126
  * preflight) can read the last recorded verdict without re-running doctor.
127
127
  * `temp/` is the gitignored scratch root, so neither git nor the sync prune
128
128
  * pass ever sees the cache. Any write failure is swallowed — the cache is
package/lib/cli/init.js CHANGED
@@ -299,7 +299,7 @@ export async function planInit({
299
299
  }
300
300
 
301
301
  // Bootstrap succeeded — run the onboarding tail: stack detection, docs
302
- // scaffolding offer, doctor gate, and /plan handoff. A tail that reports
302
+ // scaffolding offer, doctor gate, and /mandrel-plan handoff. A tail that reports
303
303
  // `ok: false` (the doctor gate failed) makes the whole init exit
304
304
  // non-zero; the tail already printed its own remediation message, and the
305
305
  // earlier install/sync/bootstrap phases' results stand as completed.
@@ -54,6 +54,7 @@
54
54
  import {
55
55
  migrations as defaultRegistry,
56
56
  runMigrations as defaultRunMigrations,
57
+ selectStepsInRange,
57
58
  } from '../migrations/index.js';
58
59
 
59
60
  /**
@@ -77,36 +78,97 @@ function parseOption(argv, flag) {
77
78
  }
78
79
 
79
80
  /**
80
- * Parse a dotted semver-ish string into a numeric tuple. Mirrors the runner's
81
- * own comparator so the dry-run in-range filter matches the live filter
82
- * exactly.
81
+ * The envelope shape every `runMigrate` exit returns. Centralised so the four
82
+ * exits cannot drift in which fields they populate.
83
83
  *
84
- * @param {string} version
85
- * @returns {[number, number, number]}
84
+ * @param {object} fields
85
+ * @returns {object}
86
86
  */
87
- function parseVersion(version) {
88
- const [major, minor, patch] = String(version).split('.');
89
- return [
90
- Number.parseInt(major, 10) || 0,
91
- Number.parseInt(minor, 10) || 0,
92
- Number.parseInt(patch, 10) || 0,
93
- ];
87
+ function migrateResult(fields) {
88
+ return {
89
+ ok: false,
90
+ action: 'usage-error',
91
+ fromVersion: null,
92
+ toVersion: null,
93
+ dryRun: false,
94
+ applied: [],
95
+ skipped: [],
96
+ wouldApply: [],
97
+ wouldSkip: [],
98
+ ...fields,
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Both bounds are required: the runner filters `fromVersion < v <= toVersion`,
104
+ * so an absent bound is ambiguous rather than a sensible default.
105
+ *
106
+ * @param {{ writeErr: (s: string) => void, exit: (code: number) => void }} io
107
+ * @returns {void}
108
+ */
109
+ function reportUsageError({ writeErr, exit }) {
110
+ writeErr(
111
+ 'mandrel migrate: both --from <version> and --to <version> are required.\n' +
112
+ ' → Usage: mandrel migrate --from <version> --to <version> [--dry-run]\n',
113
+ );
114
+ exit(1);
94
115
  }
95
116
 
96
117
  /**
97
- * Compare two version strings (standard `Array.sort` comparator contract).
118
+ * Report the in-range steps and whether each WOULD apply or be skipped,
119
+ * probing a throwaway context so no step's `apply` runs and nothing on disk
120
+ * changes. The ordering comes from `selectStepsInRange`, the same selector the
121
+ * live run uses, so the preview cannot disagree with what would happen.
98
122
  *
99
- * @param {string} a
100
- * @param {string} b
101
- * @returns {number}
123
+ * @param {{
124
+ * registry: Array<object>,
125
+ * fromVersion: string,
126
+ * toVersion: string,
127
+ * write: (s: string) => void,
128
+ * }} params
129
+ * @returns {{ wouldApply: string[], wouldSkip: string[] }}
102
130
  */
103
- function compareVersions(a, b) {
104
- const pa = parseVersion(a);
105
- const pb = parseVersion(b);
106
- for (let i = 0; i < 3; i += 1) {
107
- if (pa[i] !== pb[i]) return pa[i] - pb[i];
131
+ function previewMigrations({ registry, fromVersion, toVersion, write }) {
132
+ const inRange = selectStepsInRange({ registry, fromVersion, toVersion });
133
+ const wouldApply = [];
134
+ const wouldSkip = [];
135
+ const probeCtx = {};
136
+
137
+ write(`mandrel migrate — dry run v${fromVersion} → v${toVersion}\n`);
138
+ if (inRange.length === 0) {
139
+ write(' (no migration steps in range)\n');
108
140
  }
109
- return 0;
141
+ for (const step of inRange) {
142
+ const bucket = step.detect(probeCtx) ? wouldApply : wouldSkip;
143
+ const verb = bucket === wouldApply ? 'would apply ' : 'would skip ';
144
+ bucket.push(step.version);
145
+ write(` ${verb} ${step.version}: ${step.description}\n`);
146
+ }
147
+ write('Dry run: no migrations applied, nothing written.\n');
148
+
149
+ return { wouldApply, wouldSkip };
150
+ }
151
+
152
+ /**
153
+ * @param {{
154
+ * applied: string[],
155
+ * fromVersion: string,
156
+ * toVersion: string,
157
+ * write: (s: string) => void,
158
+ * }} params
159
+ * @returns {void}
160
+ */
161
+ function reportApplied({ applied, fromVersion, toVersion, write }) {
162
+ if (applied.length === 0) {
163
+ write(
164
+ `mandrel migrate: no migrations to apply for v${fromVersion} → v${toVersion}.\n`,
165
+ );
166
+ return;
167
+ }
168
+ const plural = applied.length === 1 ? '' : 's';
169
+ write(
170
+ `✅ Applied ${applied.length} migration${plural} (v${fromVersion} → v${toVersion}).\n`,
171
+ );
110
172
  }
111
173
 
112
174
  /**
@@ -146,107 +208,51 @@ export function runMigrate({
146
208
  const fromVersion = parseOption(argv, '--from');
147
209
  const toVersion = parseOption(argv, '--to');
148
210
 
149
- // --- Usage validation -----------------------------------------------------
150
- // Both bounds are required: the runner filters fromVersion < v <= toVersion,
151
- // so an absent bound is ambiguous rather than a sensible default.
152
211
  if (!fromVersion || !toVersion) {
153
- writeErr(
154
- 'mandrel migrate: both --from <version> and --to <version> are required.\n' +
155
- ' → Usage: mandrel migrate --from <version> --to <version> [--dry-run]\n',
156
- );
157
- exit(1);
158
- return {
159
- ok: false,
160
- action: 'usage-error',
212
+ reportUsageError({ writeErr, exit });
213
+ return migrateResult({
161
214
  fromVersion: fromVersion ?? null,
162
215
  toVersion: toVersion ?? null,
163
216
  dryRun,
164
- applied: [],
165
- skipped: [],
166
- wouldApply: [],
167
- wouldSkip: [],
168
- };
217
+ });
169
218
  }
170
219
 
171
- // --- Dry run --------------------------------------------------------------
172
- // Report the in-range steps and whether each WOULD apply or be skipped,
173
- // probing a throwaway context so no step's apply runs and nothing on disk
174
- // changes.
175
220
  if (dryRun) {
176
- const inRange = registry
177
- .filter(
178
- (step) =>
179
- compareVersions(step.version, fromVersion) > 0 &&
180
- compareVersions(step.version, toVersion) <= 0,
181
- )
182
- .sort((a, b) => compareVersions(a.version, b.version));
183
-
184
- const wouldApply = [];
185
- const wouldSkip = [];
186
- const probeCtx = {};
187
-
188
- write(`mandrel migrate — dry run v${fromVersion} → v${toVersion}\n`);
189
- if (inRange.length === 0) {
190
- write(' (no migration steps in range)\n');
191
- }
192
- for (const step of inRange) {
193
- const willApply = step.detect(probeCtx);
194
- if (willApply) {
195
- wouldApply.push(step.version);
196
- write(` would apply ${step.version}: ${step.description}\n`);
197
- } else {
198
- wouldSkip.push(step.version);
199
- write(` would skip ${step.version}: ${step.description}\n`);
200
- }
201
- }
202
- write('Dry run: no migrations applied, nothing written.\n');
203
-
204
- return {
221
+ const { wouldApply, wouldSkip } = previewMigrations({
222
+ registry,
223
+ fromVersion,
224
+ toVersion,
225
+ write,
226
+ });
227
+ return migrateResult({
205
228
  ok: true,
206
229
  action: 'dry-run',
207
230
  fromVersion,
208
231
  toVersion,
209
232
  dryRun: true,
210
- applied: [],
211
- skipped: [],
212
233
  wouldApply,
213
234
  wouldSkip,
214
- };
235
+ });
215
236
  }
216
237
 
217
- // --- Live run -------------------------------------------------------------
218
- // Delegate to the runner, which owns ordering, range filtering, idempotency,
219
- // and the per-step `migrated …` log line.
238
+ // The runner owns ordering, range filtering, idempotency, and the per-step
239
+ // `migrated …` log line.
220
240
  const { applied, skipped } = runMigrations({
221
241
  fromVersion,
222
242
  toVersion,
223
243
  ctx,
224
244
  registry,
225
245
  });
246
+ reportApplied({ applied, fromVersion, toVersion, write });
226
247
 
227
- if (applied.length === 0) {
228
- write(
229
- `mandrel migrate: no migrations to apply for v${fromVersion} → v${toVersion}.\n`,
230
- );
231
- } else {
232
- write(
233
- `✅ Applied ${applied.length} migration${
234
- applied.length === 1 ? '' : 's'
235
- } (v${fromVersion} → v${toVersion}).\n`,
236
- );
237
- }
238
-
239
- return {
248
+ return migrateResult({
240
249
  ok: true,
241
250
  action: 'migrated',
242
251
  fromVersion,
243
252
  toVersion,
244
- dryRun: false,
245
253
  applied,
246
254
  skipped,
247
- wouldApply: [],
248
- wouldSkip: [],
249
- };
255
+ });
250
256
  }
251
257
 
252
258
  /**
@@ -0,0 +1,200 @@
1
+ // lib/migrations/helpers/retire-agentrc-key.js
2
+ /**
3
+ * The shared scaffold behind every "strip a retired `.agentrc.json` key"
4
+ * migration step.
5
+ *
6
+ * Retiring a config key is the framework's most common migration: a key is
7
+ * dropped from the runtime AJV schema, the block it lived under carries
8
+ * `additionalProperties: false`, and a consumer whose config still sets it
9
+ * hits a hard validation failure on upgrade rather than a warning. Every such
10
+ * step reads the config, decides whether the key is present, deletes it,
11
+ * prunes whatever containers that emptied, and writes the file back — the
12
+ * same mechanics each time, previously copy-pasted per step (jscpd recorded
13
+ * five of them between 48 and 58 percent duplicated).
14
+ *
15
+ * Only three things actually vary between steps, so only those three are
16
+ * declared:
17
+ *
18
+ * - **which config surfaces to sweep** — `.agentrc.json` alone, or that plus
19
+ * the gitignored `.agentrc.local.json`. `config-resolver.js` deep-merges
20
+ * the overlay over the base *before* the AJV gate runs, so a key surviving
21
+ * in the overlay fails exactly as a base one would. A step that swept only
22
+ * the base would report "nothing to migrate" and leave that consumer hard
23
+ * broken with no self-service remedy.
24
+ * - **which key paths are retired** — a step may retire more than one.
25
+ * - **how far to prune emptied ancestors** — this is deliberately per-key
26
+ * and not a global policy. `planning` is optional, so an emptied
27
+ * `planning` block is removed; `project` is required and an empty
28
+ * `commands` object is valid against the schema, so pruning it would be a
29
+ * cosmetic edit to a config the consumer owns.
30
+ *
31
+ * Builtins only, and the `fs` seam stays injectable via `ctx.fs`, because
32
+ * these steps run during `mandrel update` before third-party packages are
33
+ * guaranteed to be present.
34
+ */
35
+
36
+ import nodeFs from 'node:fs';
37
+ import path from 'node:path';
38
+
39
+ /** The committed config every consumer has. */
40
+ export const AGENTRC_BASE_FILENAME = '.agentrc.json';
41
+
42
+ /** The operator-owned, gitignored overlay the resolver merges over the base. */
43
+ const AGENTRC_LOCAL_FILENAME = '.agentrc.local.json';
44
+
45
+ /**
46
+ * Both surfaces the resolver reads — the default sweep for a retired key.
47
+ *
48
+ * Module-local: a step that wants both surfaces takes the default and names
49
+ * nothing, so exporting this would ship a symbol with no importer.
50
+ */
51
+ const AGENTRC_FILENAMES = Object.freeze([
52
+ AGENTRC_BASE_FILENAME,
53
+ AGENTRC_LOCAL_FILENAME,
54
+ ]);
55
+
56
+ /**
57
+ * @param {unknown} ctx
58
+ * @param {string} filename
59
+ * @returns {string}
60
+ */
61
+ function resolveAgentrcPath(ctx, filename) {
62
+ const projectRoot = ctx?.projectRoot ?? process.cwd();
63
+ return path.join(projectRoot, filename);
64
+ }
65
+
66
+ /**
67
+ * Read and parse one config surface. An absent or unparseable file is the
68
+ * common case (no overlay, fresh checkout), not an error.
69
+ *
70
+ * @param {unknown} ctx
71
+ * @param {string} filename
72
+ * @param {typeof nodeFs} fsImpl
73
+ * @returns {object | null}
74
+ */
75
+ function readAgentrcConfig(ctx, filename, fsImpl) {
76
+ try {
77
+ const raw = fsImpl.readFileSync(resolveAgentrcPath(ctx, filename), 'utf8');
78
+ return JSON.parse(raw);
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Walk the container chain for a key path, returning every object along it.
86
+ * Returns `null` as soon as a level is missing or is not a plain object, which
87
+ * is what makes `hasKey` false for a config that never had the block.
88
+ *
89
+ * @param {object | null} config
90
+ * @param {string[]} keyPath
91
+ * @returns {object[] | null}
92
+ */
93
+ function resolveContainers(config, keyPath) {
94
+ if (!config || typeof config !== 'object') return null;
95
+ const containers = [config];
96
+ let cursor = config;
97
+ for (const segment of keyPath.slice(0, -1)) {
98
+ const next = cursor[segment];
99
+ if (!next || typeof next !== 'object') return null;
100
+ containers.push(next);
101
+ cursor = next;
102
+ }
103
+ return containers;
104
+ }
105
+
106
+ /**
107
+ * @param {object | null} config
108
+ * @param {string[]} keyPath
109
+ * @returns {boolean}
110
+ */
111
+ function hasKey(config, keyPath) {
112
+ const containers = resolveContainers(config, keyPath);
113
+ if (!containers) return false;
114
+ const leafOwner = containers[containers.length - 1];
115
+ return Object.hasOwn(leafOwner, keyPath[keyPath.length - 1]);
116
+ }
117
+
118
+ /**
119
+ * Delete the leaf key, then remove up to `pruneDepth` ancestors that the
120
+ * deletion left empty. `pruneDepth: 0` deletes the key and nothing else.
121
+ *
122
+ * @param {object} config
123
+ * @param {{ path: string[], pruneDepth?: number }} key
124
+ * @returns {void}
125
+ */
126
+ function stripKey(config, key) {
127
+ const keyPath = key.path;
128
+ const containers = resolveContainers(config, keyPath);
129
+ if (!containers) return;
130
+
131
+ const leafOwner = containers[containers.length - 1];
132
+ delete leafOwner[keyPath[keyPath.length - 1]];
133
+
134
+ const pruneDepth = key.pruneDepth ?? 0;
135
+ for (let level = 0; level < pruneDepth; level += 1) {
136
+ const index = containers.length - 1 - level;
137
+ const emptied = containers[index];
138
+ const parent = containers[index - 1];
139
+ if (!parent || Object.keys(emptied).length > 0) break;
140
+ delete parent[keyPath[index - 1]];
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Build a migration step that strips one or more retired keys from the
146
+ * consumer's config surfaces.
147
+ *
148
+ * `detect` is true when any declared key is present in any swept surface;
149
+ * `apply` rewrites only the surfaces that actually carry one, which is what
150
+ * makes a repeat pass a genuine no-op rather than a reformat.
151
+ *
152
+ * @param {{
153
+ * version: string,
154
+ * description: string,
155
+ * filenames?: readonly string[],
156
+ * keys: Array<{ path: string[], pruneDepth?: number }>,
157
+ * }} spec
158
+ * @returns {{
159
+ * version: string,
160
+ * description: string,
161
+ * detect: (ctx?: { projectRoot?: string, fs?: typeof nodeFs }) => boolean,
162
+ * apply: (ctx?: { projectRoot?: string, fs?: typeof nodeFs }) => void,
163
+ * }}
164
+ */
165
+ export function createRetireAgentrcKeyStep({
166
+ version,
167
+ description,
168
+ filenames = AGENTRC_FILENAMES,
169
+ keys,
170
+ }) {
171
+ const carriesRetiredKey = (config) =>
172
+ keys.some((key) => hasKey(config, key.path));
173
+
174
+ return {
175
+ version,
176
+ description,
177
+ detect(ctx) {
178
+ const fsImpl = ctx?.fs ?? nodeFs;
179
+ return filenames.some((filename) =>
180
+ carriesRetiredKey(readAgentrcConfig(ctx, filename, fsImpl)),
181
+ );
182
+ },
183
+ apply(ctx) {
184
+ const fsImpl = ctx?.fs ?? nodeFs;
185
+ for (const filename of filenames) {
186
+ const config = readAgentrcConfig(ctx, filename, fsImpl);
187
+ if (!carriesRetiredKey(config)) continue;
188
+
189
+ for (const key of keys) {
190
+ if (hasKey(config, key.path)) stripKey(config, key);
191
+ }
192
+
193
+ fsImpl.writeFileSync(
194
+ resolveAgentrcPath(ctx, filename),
195
+ `${JSON.stringify(config, null, 2)}\n`,
196
+ );
197
+ }
198
+ },
199
+ };
200
+ }