mandrel 2.0.0 → 2.2.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 (323) hide show
  1. package/.agents/README.md +59 -28
  2. package/.agents/agents/acceptance-critic.md +20 -9
  3. package/.agents/agents/story-worker.md +45 -48
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +60 -46
  6. package/.agents/docs/agentrc-reference.json +8 -13
  7. package/.agents/docs/configuration.md +33 -57
  8. package/.agents/docs/execution-reference.md +39 -10
  9. package/.agents/docs/quality-gates.md +17 -19
  10. package/.agents/docs/workflows.md +6 -6
  11. package/.agents/instructions.md +64 -79
  12. package/.agents/rules/ci-remediation.md +3 -3
  13. package/.agents/rules/gherkin-standards.md +10 -0
  14. package/.agents/rules/git-conventions-reference.md +42 -51
  15. package/.agents/schemas/acceptance-eval-verdict.schema.json +2 -2
  16. package/.agents/schemas/agentrc.schema.json +35 -46
  17. package/.agents/schemas/audit-rules.json +59 -1
  18. package/.agents/schemas/audit-rules.schema.json +33 -1
  19. package/.agents/schemas/lifecycle/README.md +1 -2
  20. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  21. package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
  22. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
  23. package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
  24. package/.agents/schemas/signal-event.schema.json +3 -3
  25. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  26. package/.agents/schemas/validation-evidence.schema.json +1 -1
  27. package/.agents/scripts/acceptance-eval.js +24 -68
  28. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  29. package/.agents/scripts/bootstrap.js +3 -3
  30. package/.agents/scripts/check-dead-exports.js +43 -104
  31. package/.agents/scripts/check-doc-links.js +2 -2
  32. package/.agents/scripts/check-lifecycle-lint.js +1 -1
  33. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  34. package/.agents/scripts/deliver-recover.js +122 -0
  35. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  36. package/.agents/scripts/evidence-gate.js +20 -50
  37. package/.agents/scripts/generate-skills-index.js +17 -1
  38. package/.agents/scripts/generate-workflows-doc.js +4 -4
  39. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  40. package/.agents/scripts/lib/audit-suite/selector.js +323 -23
  41. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
  42. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  43. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  44. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  45. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  46. package/.agents/scripts/lib/checks/core-bare-clean.js +4 -1
  47. package/.agents/scripts/lib/checks/index.js +1 -1
  48. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  49. package/.agents/scripts/lib/checks/state.js +17 -248
  50. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +3 -3
  51. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  52. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  53. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  54. package/.agents/scripts/lib/cli-args.js +23 -2
  55. package/.agents/scripts/lib/close-validation/gates.js +13 -13
  56. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  57. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  58. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  59. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  60. package/.agents/scripts/lib/config/acceptance-eval.js +2 -2
  61. package/.agents/scripts/lib/config/delivery-routing.js +7 -6
  62. package/.agents/scripts/lib/config/explain.js +10 -16
  63. package/.agents/scripts/lib/config/github.js +7 -5
  64. package/.agents/scripts/lib/config/limits.js +15 -25
  65. package/.agents/scripts/lib/config/quality.js +11 -14
  66. package/.agents/scripts/lib/config/runners.js +8 -21
  67. package/.agents/scripts/lib/config/temp-paths.js +18 -56
  68. package/.agents/scripts/lib/config-settings-schema-delivery.js +34 -16
  69. package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
  70. package/.agents/scripts/lib/config-settings-schema.js +48 -22
  71. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  72. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  73. package/.agents/scripts/lib/duplicate-search.js +38 -7
  74. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  75. package/.agents/scripts/lib/format-generated-json.js +97 -0
  76. package/.agents/scripts/lib/framework-version.js +19 -189
  77. package/.agents/scripts/lib/gh-exec.js +8 -0
  78. package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
  79. package/.agents/scripts/lib/git-utils.js +0 -14
  80. package/.agents/scripts/lib/json-utils.js +1 -2
  81. package/.agents/scripts/lib/label-constants.js +0 -15
  82. package/.agents/scripts/lib/label-taxonomy.js +1 -12
  83. package/.agents/scripts/lib/observability/active-story-env.js +42 -163
  84. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  85. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  86. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  87. package/.agents/scripts/lib/observability/source-classifier.js +2 -2
  88. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  89. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  90. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  91. package/.agents/scripts/lib/orchestration/ceremony-routing.js +43 -45
  92. package/.agents/scripts/lib/orchestration/change-set.js +103 -0
  93. package/.agents/scripts/lib/orchestration/code-review.js +70 -191
  94. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
  95. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  96. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  97. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  98. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  99. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  100. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +9 -11
  101. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  102. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  103. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +7 -3
  104. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  105. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  106. package/.agents/scripts/lib/orchestration/merge-block-class.js +76 -20
  107. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  108. package/.agents/scripts/lib/orchestration/plan-context.js +116 -33
  109. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +26 -36
  110. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +31 -22
  111. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  112. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
  113. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
  114. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  115. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -100
  116. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
  117. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
  118. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  119. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +230 -0
  120. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
  121. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -2
  122. package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
  123. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  124. package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
  125. package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
  126. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  127. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  128. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  129. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  130. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  131. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  132. package/.agents/scripts/lib/orchestration/run-epilogue.js +374 -16
  133. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
  134. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  135. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  136. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +4 -13
  137. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  138. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +72 -30
  139. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  140. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  141. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +12 -8
  142. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  143. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  144. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +264 -43
  145. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  146. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  147. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +104 -279
  148. package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +191 -0
  149. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +120 -0
  150. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  151. package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
  152. package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
  153. package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
  154. package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
  155. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  156. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
  157. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
  158. package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
  159. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
  160. package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
  161. package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
  162. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  163. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  164. package/.agents/scripts/lib/planning-corpus.js +12 -286
  165. package/.agents/scripts/lib/preflight-runner.js +2 -2
  166. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  167. package/.agents/scripts/lib/signals/index.js +4 -17
  168. package/.agents/scripts/lib/signals/read.js +35 -35
  169. package/.agents/scripts/lib/signals/schema.js +8 -11
  170. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  171. package/.agents/scripts/lib/signals/write.js +0 -1
  172. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  173. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  174. package/.agents/scripts/lib/story-adjacency.js +8 -7
  175. package/.agents/scripts/lib/story-body/story-body.js +81 -13
  176. package/.agents/scripts/lib/templates/decomposer-prompts.js +15 -16
  177. package/.agents/scripts/lib/test-env.js +14 -1
  178. package/.agents/scripts/lib/test-tiers.js +0 -3
  179. package/.agents/scripts/lib/ticket-body-sections.js +0 -14
  180. package/.agents/scripts/lib/validation-evidence.js +31 -59
  181. package/.agents/scripts/lib/wave-runner/live-probe.js +315 -0
  182. package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
  183. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  184. package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
  185. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  186. package/.agents/scripts/plan-context.js +38 -7
  187. package/.agents/scripts/plan-critics.js +203 -0
  188. package/.agents/scripts/plan-persist.js +145 -35
  189. package/.agents/scripts/plan-run-epilogue.js +83 -38
  190. package/.agents/scripts/post-structured-comment.js +0 -38
  191. package/.agents/scripts/pr-watch-with-update.js +43 -22
  192. package/.agents/scripts/providers/github/compose.js +0 -1
  193. package/.agents/scripts/providers/github/errors.js +0 -19
  194. package/.agents/scripts/providers/github/issues.js +1 -11
  195. package/.agents/scripts/providers/github/mappers.js +5 -0
  196. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  197. package/.agents/scripts/providers/github/tickets.js +33 -153
  198. package/.agents/scripts/providers/github.js +17 -6
  199. package/.agents/scripts/quality-preview.js +13 -6
  200. package/.agents/scripts/resolve-stories.js +236 -0
  201. package/.agents/scripts/run-coverage.js +4 -1
  202. package/.agents/scripts/run-lint.js +2 -2
  203. package/.agents/scripts/run-verify.js +31 -2
  204. package/.agents/scripts/signals-view.js +9 -10
  205. package/.agents/scripts/single-story-close.js +173 -18
  206. package/.agents/scripts/single-story-confirm-merge.js +288 -15
  207. package/.agents/scripts/single-story-init.js +6 -10
  208. package/.agents/scripts/stories-wave-tick.js +380 -53
  209. package/.agents/scripts/story-plan.js +3 -3
  210. package/.agents/scripts/update-ticket-state.js +8 -50
  211. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  212. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  213. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  214. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  215. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  216. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  217. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  218. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  219. package/.agents/skills/core/idea-refinement/SKILL.md +3 -3
  220. package/.agents/skills/core/scope-triage/SKILL.md +3 -0
  221. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  222. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  223. package/.agents/skills/skills.index.json +2 -12
  224. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  225. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  226. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  227. package/.agents/workflows/audit-architecture.md +3 -4
  228. package/.agents/workflows/audit-clean-code.md +4 -4
  229. package/.agents/workflows/audit-documentation.md +4 -5
  230. package/.agents/workflows/audit-lighthouse.md +8 -0
  231. package/.agents/workflows/audit-navigability.md +10 -0
  232. package/.agents/workflows/audit-performance.md +2 -3
  233. package/.agents/workflows/audit-quality.md +8 -9
  234. package/.agents/workflows/audit-security.md +1 -2
  235. package/.agents/workflows/audit-seo.md +10 -0
  236. package/.agents/workflows/audit-ux-ui.md +7 -0
  237. package/.agents/workflows/deliver.md +133 -45
  238. package/.agents/workflows/git-cleanup.md +2 -2
  239. package/.agents/workflows/git-deliver.md +1 -1
  240. package/.agents/workflows/helpers/acceptance-self-eval.md +34 -17
  241. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -12
  242. package/.agents/workflows/helpers/code-review.md +14 -12
  243. package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
  244. package/.agents/workflows/helpers/deliver-story.md +209 -118
  245. package/.agents/workflows/helpers/parallel-tooling.md +2 -2
  246. package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
  247. package/.agents/workflows/plan.md +239 -19
  248. package/.agents/workflows/qa-assist.md +6 -6
  249. package/.agents/workflows/qa-explore.md +3 -3
  250. package/.agents/workflows/qa-run.md +1 -5
  251. package/bin/mandrel.js +12 -1
  252. package/docs/CHANGELOG.md +62 -0
  253. package/lib/cli/registry.js +262 -19
  254. package/lib/cli/sync-agents.js +157 -0
  255. package/lib/cli/sync-commands.js +115 -6
  256. package/lib/cli/sync.js +168 -6
  257. package/lib/cli/update.js +105 -8
  258. package/lib/cli/version-helpers.js +131 -0
  259. package/lib/migrations/README.md +7 -5
  260. package/lib/migrations/index.js +17 -9
  261. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  262. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  263. package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +154 -0
  264. package/package.json +2 -2
  265. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  266. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  267. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  268. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  269. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  270. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  271. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  272. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  273. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  274. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  275. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  276. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  277. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  278. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  279. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  280. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  281. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  282. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  283. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  284. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  285. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  286. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  287. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  288. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  289. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  290. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  291. package/.agents/schemas/risk-verdict.schema.json +0 -53
  292. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  293. package/.agents/scripts/analyze-execution.js +0 -444
  294. package/.agents/scripts/check-prepush-recovery.js +0 -90
  295. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  296. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  297. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -187
  298. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  299. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  300. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  301. package/.agents/scripts/lib/orchestration/audit-lens-routing.js +0 -128
  302. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
  303. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  304. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  305. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  306. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  307. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  308. package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
  309. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  310. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  311. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  312. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -21
  313. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  314. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  315. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  316. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -397
  317. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  318. package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
  319. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  320. package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
  321. package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
  322. package/.agents/scripts/resolve-plan-run.js +0 -117
  323. package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
package/lib/cli/sync.js CHANGED
@@ -26,6 +26,16 @@
26
26
  * managed `.agents/` zone (everything except `.agents/local/`) that has
27
27
  * no counterpart in the package payload is deleted. Consumer additions
28
28
  * under `.agents/local/` are never touched.
29
+ * - Generated, never-pruned registry (Story #4534). Framework-generated
30
+ * `.agents/` files have no payload counterpart by design, so the prune
31
+ * pass would delete each one unless it is declared in
32
+ * {@link GENERATED_FILES}. That registry — not a per-file special case in
33
+ * {@link listDestFiles} — is the single place a generated artifact is
34
+ * exempted. Registered today: the version marker (Story #4530) and the
35
+ * install ledger the bootstrap writes and `mandrel uninstall` reads.
36
+ * - Version marker (Story #4530). After the prune pass, `.agents/.mandrel-
37
+ * version` is (re)written with the executing package's own version. See
38
+ * {@link readVersionMarker} for how callers consume it.
29
39
  *
30
40
  * Security (Tech Spec #3459 "Postinstall safety"):
31
41
  * - Does nothing beyond a local file copy: no network, no shell, no writes
@@ -45,8 +55,19 @@ import nodeFs from 'node:fs';
45
55
  import { createRequire } from 'node:module';
46
56
  import path from 'node:path';
47
57
 
58
+ import { LEDGER_RELATIVE_PATH } from '../../.agents/scripts/lib/bootstrap/install-ledger.js';
59
+
48
60
  export const PACKAGE_NAME = 'mandrel';
49
61
 
62
+ /**
63
+ * Name of the managed destination directory, relative to the project root.
64
+ * Generating modules export their artifact paths relative to the *project
65
+ * root* (e.g. `install-ledger.js`'s `LEDGER_RELATIVE_PATH`), whereas the
66
+ * prune pass compares paths relative to this directory — {@link
67
+ * toManagedZoneRelative} is the one conversion between the two.
68
+ */
69
+ const AGENTS_DIR = '.agents';
70
+
50
71
  /**
51
72
  * Top-level directory name (relative to `.agents/`) reserved as the
52
73
  * sync-exempt local-additions zone (Story #3498, f-drift-local-zone).
@@ -70,6 +91,119 @@ export const LOCAL_ZONE_DIR = 'local';
70
91
  */
71
92
  export const LOCAL_OVERRIDE_RE = /\.local\.[^.]+$/;
72
93
 
94
+ /**
95
+ * Relative path (from `.agents/`) of the version marker `runSync` writes
96
+ * after every sync, carrying the executing package's own version.
97
+ */
98
+ export const VERSION_MARKER_RELATIVE_PATH = '.mandrel-version';
99
+
100
+ /**
101
+ * Convert a project-root-relative POSIX path — the shape a generating module
102
+ * exports as its own source of truth (e.g. `install-ledger.js`'s
103
+ * `LEDGER_RELATIVE_PATH`, `'.agents/.install-manifest.json'`) — into the
104
+ * `.agents/`-relative, OS-separator form {@link listDestFiles} enumerates and
105
+ * {@link GENERATED_FILES} is compared against.
106
+ *
107
+ * Throws on a path outside `.agents/`: a generated file the sync prune pass
108
+ * never walks has no business in the registry, and registering one would be a
109
+ * silent no-op that reads as protection. Failing at module load surfaces the
110
+ * mistake at the first import rather than as a mystery deletion in a
111
+ * consumer's tree.
112
+ *
113
+ * @param {string} projectRelativePosixPath - e.g. `.agents/.install-manifest.json`.
114
+ * @returns {string} e.g. `.install-manifest.json` (OS separators).
115
+ */
116
+ function toManagedZoneRelative(projectRelativePosixPath) {
117
+ const segments = projectRelativePosixPath.split('/');
118
+ if (segments[0] !== AGENTS_DIR || segments.length < 2) {
119
+ throw new Error(
120
+ `generated-files registry: '${projectRelativePosixPath}' is not inside ${AGENTS_DIR}/ — ` +
121
+ 'only files the sync prune pass walks can be registered as never-pruned.',
122
+ );
123
+ }
124
+ return path.join(...segments.slice(1));
125
+ }
126
+
127
+ /**
128
+ * The **generated, never-pruned registry**: every framework-generated
129
+ * `.agents/` file that has no payload counterpart and must therefore be
130
+ * exempted from the prune pass. Paths are `.agents/`-relative with OS
131
+ * separators, matching what {@link listDestFiles} enumerates.
132
+ *
133
+ * The prune pass (Story #4046 A3) deletes any managed-zone file absent from
134
+ * the payload. That is the correct rule for consumer drift, but it is also
135
+ * exactly the shape of every file the framework itself generates into
136
+ * `.agents/` outside the payload — so each such artifact is deleted by the very
137
+ * sync that wrote it unless registered here. Two have hit this already: the
138
+ * install ledger (`.agents/.install-manifest.json`), whose loss made
139
+ * `mandrel uninstall` silently reverse nothing and exit 0 (Story #4534), and
140
+ * the version marker (Story #4530).
141
+ *
142
+ * This is deliberately a **registry, not a list of special cases**. Register a
143
+ * new generated file by adding one entry here — keyed off the generating
144
+ * module's own exported constant wherever one exists, never a duplicated
145
+ * string literal — rather than by adding another branch to
146
+ * {@link listDestFiles}. That keeps the never-prune contract declared in one
147
+ * place and keeps the registry honest: if the generator renames its artifact,
148
+ * the exemption moves with it.
149
+ *
150
+ * This registry does **not** subsume the `.agents/local/` zone or the
151
+ * `*.local.*` overrides ({@link LOCAL_ZONE_DIR}, {@link LOCAL_OVERRIDE_RE}).
152
+ * Those are a different concept — consumer-authored overrides, not
153
+ * framework-generated artifacts — and stay separate on purpose.
154
+ */
155
+ export const GENERATED_FILES = Object.freeze([
156
+ // Version marker (Story #4530) — written by runSync itself, below.
157
+ VERSION_MARKER_RELATIVE_PATH,
158
+ // Install ledger (Story #4534) — written by the bootstrap
159
+ // (`install-ledger.js`), read by `mandrel uninstall` as its single source of
160
+ // truth. Keyed off the generator's exported constant.
161
+ toManagedZoneRelative(LEDGER_RELATIVE_PATH),
162
+ ]);
163
+
164
+ /**
165
+ * Resolve the version of the package whose `.agents/` payload was just
166
+ * copied — i.e. the version the marker must carry. Reads `package.json` at
167
+ * the already-resolved `packageRoot`, never the two-directories-up
168
+ * self-referential pattern `lib/cli/update.js`'s pre-Story-#4530
169
+ * `defaultCurrentVersion` used (that pattern conflates "the CLI that is
170
+ * running" with "the payload that was materialized" — the exact confusion
171
+ * #4525 fixes on the consumer-pin side of this Story).
172
+ *
173
+ * @param {string} packageRoot - Absolute path to the resolved package root.
174
+ * @param {typeof nodeFs} fsImpl
175
+ * @returns {string}
176
+ */
177
+ function resolvePackageVersion(packageRoot, fsImpl) {
178
+ const pkgJsonPath = path.join(packageRoot, 'package.json');
179
+ const parsed = JSON.parse(fsImpl.readFileSync(pkgJsonPath, 'utf8'));
180
+ return String(parsed.version);
181
+ }
182
+
183
+ /**
184
+ * Read the consumer's `.agents/.mandrel-version` marker. Returns `null` when
185
+ * absent — a pre-marker install, or a tree that has never been synced —
186
+ * so callers (the sync-commands mismatch refusal, the doctor pin check) can
187
+ * degrade to their existing content-hash drift check rather than failing.
188
+ *
189
+ * @param {string} consumerRoot - Consumer project root (not `.agents/`).
190
+ * @param {typeof nodeFs} [fsImpl]
191
+ * @returns {string | null}
192
+ */
193
+ export function readVersionMarker(consumerRoot, fsImpl = nodeFs) {
194
+ const markerPath = path.join(
195
+ consumerRoot,
196
+ '.agents',
197
+ VERSION_MARKER_RELATIVE_PATH,
198
+ );
199
+ try {
200
+ const raw = fsImpl.readFileSync(markerPath, 'utf8').trim();
201
+ return raw || null;
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+
73
207
  /**
74
208
  * Default resolver: locate the installed `mandrel` package root by
75
209
  * resolving its `package.json` and returning the directory that contains it.
@@ -126,12 +260,22 @@ export function listFiles(dir, fsImpl, prefix = '') {
126
260
 
127
261
  /**
128
262
  * Recursively enumerate every regular file under `dir`, returning paths
129
- * relative to `dir` using OS separators. The top-level `local/` subtree is
130
- * skipped so consumer additions inside `.agents/local/` are never pruned
131
- * (Story #3498). Files following the `*.local.*` naming convention (e.g.
132
- * `.agents/instructions.local.md`the documented consumer override
133
- * mechanism, instructions.md § 1.E) are also skipped: they never ship in
134
- * the payload and must survive every sync.
263
+ * relative to `dir` using OS separators. Three disjoint classes are skipped,
264
+ * so a caller diffing this against the payload never treats them as stale:
265
+ *
266
+ * 1. The top-level `local/` subtree consumer additions inside
267
+ * `.agents/local/` are never pruned (Story #3498).
268
+ * 2. Files matching the `*.local.*` convention (e.g.
269
+ * `.agents/instructions.local.md`) — the documented consumer override
270
+ * mechanism (instructions.md § 1.E); they never ship in the payload and
271
+ * must survive every sync.
272
+ * 3. Everything declared in the {@link GENERATED_FILES} registry —
273
+ * framework-generated artifacts with no payload counterpart by design
274
+ * (Story #4534). New generated files are registered there, not added as
275
+ * a fourth branch here.
276
+ *
277
+ * (1) and (2) are consumer-authored; (3) is framework-generated. They are
278
+ * kept separate deliberately — conflating them would muddy both contracts.
135
279
  *
136
280
  * Mirrors `listFiles` but operates on the destination tree so we can
137
281
  * identify stale files that have no payload counterpart (Story #4046 A3).
@@ -162,6 +306,11 @@ function listDestFiles(dir, fsImpl, prefix = '') {
162
306
  continue;
163
307
  }
164
308
  const rel = prefix ? path.join(prefix, ent.name) : ent.name;
309
+ // Framework-generated artifacts — declared once in the never-pruned
310
+ // registry (Story #4534); see GENERATED_FILES.
311
+ if (!ent.isDirectory() && GENERATED_FILES.includes(rel)) {
312
+ continue;
313
+ }
165
314
  const abs = path.join(dir, ent.name);
166
315
  if (ent.isDirectory()) {
167
316
  out.push(...listDestFiles(abs, fsImpl, rel));
@@ -273,6 +422,19 @@ export function runSync({
273
422
  fs.unlinkSync(dest);
274
423
  }
275
424
 
425
+ // Version marker (Story #4530): written AFTER the prune pass, never
426
+ // before — the GENERATED_FILES registry already keeps listDestFiles from
427
+ // ever enumerating it as stale, but ordering the write after prune is a
428
+ // second, independent safeguard against the same class of bug (see the
429
+ // GENERATED_FILES doc comment) surviving a future prune-pass reorder. The
430
+ // install ledger gets no such belt-and-braces: it is written by the
431
+ // bootstrap, not here, so the registry is its only protection.
432
+ const packageVersion = resolvePackageVersion(packageRoot, fs);
433
+ fs.writeFileSync(
434
+ path.join(destRoot, VERSION_MARKER_RELATIVE_PATH),
435
+ `${packageVersion}\n`,
436
+ );
437
+
276
438
  if (staleFiles.length > 0) {
277
439
  write(
278
440
  `✅ Installed ${payloadFiles.length} file(s) into ./.agents/ (pruned ${staleFiles.length} stale file(s))\n`,
package/lib/cli/update.js CHANGED
@@ -136,8 +136,12 @@ import { fileURLToPath } from 'node:url';
136
136
  import { detectPackageManagerWithWorkspace } from '../../.agents/scripts/lib/detect-package-manager.js';
137
137
  import { runInstallCommand } from '../../.agents/scripts/lib/install-cmd-parser.js';
138
138
  import { runAgentsDrift } from './registry.js';
139
+ import { defaultResolvePackageRoot } from './sync.js';
139
140
  import { isStale } from './version-check.js';
140
- import { compareVersions } from './version-helpers.js';
141
+ import {
142
+ compareVersions,
143
+ resolveConsumerPinVersion,
144
+ } from './version-helpers.js';
141
145
 
142
146
  /** The published package whose newest version `mandrel update` advances to. */
143
147
  const PACKAGE_NAME = 'mandrel';
@@ -163,6 +167,15 @@ const DEFAULT_CACHE_FILENAME = 'version-check.json';
163
167
  * `package.json`. The module lives at `<root>/lib/cli/update.js`, so the
164
168
  * manifest is two directories up.
165
169
  *
170
+ * Pre-Story-#4525 this was the update decision's `current` — the exact
171
+ * self-referential confusion #4525 filed: "the version of the mandrel that
172
+ * is executing" is tautologically `>= target` whenever the installed
173
+ * package is newest, which made the `npm-update` step unreachable whenever
174
+ * a consumer's declared pin had fallen behind what happened to be resolved
175
+ * in `node_modules`. It survives here only as the last-resort fallback
176
+ * inside {@link resolveCurrentVersionForUpdate}, for the case where even
177
+ * `node_modules` resolution from the consumer root fails.
178
+ *
166
179
  * @param {typeof nodeFs} [fs]
167
180
  * @returns {string}
168
181
  */
@@ -173,6 +186,48 @@ function defaultCurrentVersion(fs = nodeFs) {
173
186
  return String(parsed.version);
174
187
  }
175
188
 
189
+ /**
190
+ * Resolve the "current" version for the `mandrel update` decision
191
+ * (Story #4525 / #4530): the consumer's declared `mandrel` dependency pin
192
+ * when it resolves to a plain semver, falling back — in order — to the
193
+ * version actually resolvable in the consumer's `node_modules` (anchored at
194
+ * `consumerRoot` via the same resolution `mandrel sync` uses, unlike the
195
+ * pre-#4525 self-referential `defaultCurrentVersion`), and finally to
196
+ * `defaultCurrentVersion` itself when neither resolves (a corrupted or
197
+ * highly unusual install — keeps `mandrel update` from throwing outright
198
+ * rather than silently misreporting "already current").
199
+ *
200
+ * The declared pin is preferred because it is exactly what the `npm-update`
201
+ * step moves: a consumer whose `package.json` pin lags an inflated
202
+ * `node_modules` resolution (e.g. an out-of-band symlink or manual
203
+ * `npm install mandrel@latest --no-save`) must still see `planUpdate`
204
+ * choose `updated`, not `resynced` — the bug #4525 reported.
205
+ *
206
+ * @param {string} consumerRoot
207
+ * @param {typeof nodeFs} [fs]
208
+ * @param {{ resolvePackageRoot?: (fromDir: string) => string }} [opts] - test
209
+ * seam for the `node_modules` resolution tier; defaults to the real
210
+ * `defaultResolvePackageRoot` from `sync.js`.
211
+ * @returns {string}
212
+ */
213
+ export function resolveCurrentVersionForUpdate(
214
+ consumerRoot,
215
+ fs = nodeFs,
216
+ { resolvePackageRoot = defaultResolvePackageRoot } = {},
217
+ ) {
218
+ const pinned = resolveConsumerPinVersion(consumerRoot, fs);
219
+ if (pinned) return pinned;
220
+ try {
221
+ const packageRoot = resolvePackageRoot(consumerRoot);
222
+ const parsed = JSON.parse(
223
+ fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'),
224
+ );
225
+ return String(parsed.version);
226
+ } catch {
227
+ return defaultCurrentVersion(fs);
228
+ }
229
+ }
230
+
176
231
  /**
177
232
  * Resolve the project root — the directory two levels up from this module
178
233
  * (`<root>/lib/cli/update.js`). Mirrors `lib/cli/registry.js#resolveProjectRoot`.
@@ -685,18 +740,20 @@ export function defaultSpawnPhase(
685
740
  * The ordered step names the orchestrator drives on an update. Shared
686
741
  * by the live path and the `--dry-run` plan printout so the two never drift.
687
742
  *
688
- * Step ordering (Story #4046 A1c):
743
+ * Step ordering (Story #4046 A1c; sync-agents added by Story #4528/#4530):
689
744
  * 1. npm-update — install the new version
690
745
  * 2. runSync — re-materialize .agents/ from the new payload
691
746
  * 3. sync-commands — regenerate .claude/commands/ from the new payload
692
- * 4. runMigrations apply version-keyed migrations
693
- * 5. doctor validate the post-upgrade state
694
- * 6. surface changelog print the changelog (always last, best-effort)
747
+ * 4. sync-agents regenerate .claude/agents/ from the new payload
748
+ * 5. runMigrations apply version-keyed migrations
749
+ * 6. doctor validate the post-upgrade state
750
+ * 7. surface changelog — print the changelog (always last, best-effort)
695
751
  */
696
752
  const STEP_PLAN = [
697
753
  'npm-update',
698
754
  'runSync',
699
755
  'sync-commands',
756
+ 'sync-agents',
700
757
  'runMigrations',
701
758
  'doctor',
702
759
  'surface changelog',
@@ -744,6 +801,20 @@ function fullUpgradeSteps(current, target) {
744
801
  'the .claude/commands/ tree may be out of sync. ' +
745
802
  'Run `npm run sync:commands` manually to restore.',
746
803
  },
804
+ {
805
+ // Story #4528/#4530: the CLI update path previously never projected the
806
+ // role-agent tree at all — only the bootstrap path did. Added alongside
807
+ // sync-commands so `.claude/agents/` materializes here too, which is
808
+ // what makes the tightened `agents-in-sync` doctor check satisfiable.
809
+ kind: 'spawn',
810
+ phase: 'sync-agents',
811
+ args: [],
812
+ label: 'sync-agents',
813
+ failMessage:
814
+ 'mandrel update: `mandrel sync-agents` from new binary exited non-zero — ' +
815
+ 'the .claude/agents/ tree may be out of sync. ' +
816
+ 'Run `mandrel sync-agents` manually to restore.',
817
+ },
747
818
  {
748
819
  kind: 'spawn',
749
820
  phase: 'migrate',
@@ -760,8 +831,9 @@ function fullUpgradeSteps(current, target) {
760
831
 
761
832
  /**
762
833
  * The ordered phase descriptors for a drift-heal (version already current, but
763
- * `.agents/` is stale). No npm-update, no migrations, no doctor — only the two
764
- * sync phases re-materialize the payload from the already-installed binary.
834
+ * `.agents/` is stale). No npm-update, no migrations, no doctor — only the
835
+ * three sync phases re-materialize the payload from the already-installed
836
+ * binary.
765
837
  *
766
838
  * @returns {Array<{ kind: 'spawn', phase: string, args: string[], label: string, failMessage: string }>}
767
839
  */
@@ -787,6 +859,17 @@ function driftHealSteps() {
787
859
  'the .claude/commands/ tree may be out of sync. ' +
788
860
  'Run `npm run sync:commands` manually to restore.',
789
861
  },
862
+ {
863
+ // Story #4528/#4530: see the matching entry in fullUpgradeSteps().
864
+ kind: 'spawn',
865
+ phase: 'sync-agents',
866
+ args: [],
867
+ label: 'sync-agents',
868
+ failMessage:
869
+ 'mandrel update: `mandrel sync-agents` from installed binary exited non-zero — ' +
870
+ 'the .claude/agents/ tree may be out of sync. ' +
871
+ 'Run `mandrel sync-agents` manually to restore.',
872
+ },
790
873
  ];
791
874
  }
792
875
 
@@ -1070,6 +1153,9 @@ export async function runUpdate({
1070
1153
  write(
1071
1154
  ' 2. sync-commands — regenerate .claude/commands/ from .agents/workflows/\n',
1072
1155
  );
1156
+ write(
1157
+ ' 3. sync-agents — regenerate .claude/agents/ from .agents/agents/\n',
1158
+ );
1073
1159
  write('Dry run: no files written.\n');
1074
1160
  } else {
1075
1161
  write(`mandrel update — planned upgrade v${current} → v${target}\n`);
@@ -1202,6 +1288,8 @@ export async function runUpdate({
1202
1288
  * changelogPath?: string,
1203
1289
  * fetchChangelog?: (version: string) => Promise<string>,
1204
1290
  * runUpdate?: typeof runUpdate,
1291
+ * cwd?: () => string,
1292
+ * checkDrift?: () => (boolean | Promise<boolean>),
1205
1293
  * write?: (s: string) => void,
1206
1294
  * writeErr?: (s: string) => void,
1207
1295
  * exit?: (code: number) => void,
@@ -1224,9 +1312,16 @@ export default async function run(argv = [], deps = {}) {
1224
1312
  writeErr = (s) => process.stderr.write(s),
1225
1313
  exit = (code) => process.exit(code),
1226
1314
  log,
1315
+ cwd,
1316
+ checkDrift,
1227
1317
  } = deps;
1228
1318
 
1229
- const current = deps.currentVersion ?? defaultCurrentVersion(fs);
1319
+ const cwdFn = typeof cwd === 'function' ? cwd : () => process.cwd();
1320
+
1321
+ // Story #4525/#4530: prefer the consumer's declared dependency pin over
1322
+ // the pre-#4525 self-referential read — see resolveCurrentVersionForUpdate.
1323
+ const current =
1324
+ deps.currentVersion ?? resolveCurrentVersionForUpdate(cwdFn(), fs);
1230
1325
 
1231
1326
  // The production spawnPhase: spawn each post-install phase from
1232
1327
  // node_modules/.bin/mandrel (the newly-installed binary). This is the sole
@@ -1260,6 +1355,7 @@ export default async function run(argv = [], deps = {}) {
1260
1355
  runInstall: runInstall ?? runInstallCommand,
1261
1356
  fs,
1262
1357
  }),
1358
+ ...(checkDrift ? { checkDrift } : {}),
1263
1359
  spawnPhase: productionSpawnPhase,
1264
1360
  surfaceChangelog: (target) =>
1265
1361
  defaultSurfaceChangelog(target, {
@@ -1273,5 +1369,6 @@ export default async function run(argv = [], deps = {}) {
1273
1369
  write,
1274
1370
  writeErr,
1275
1371
  exit,
1372
+ cwd: cwdFn,
1276
1373
  });
1277
1374
  }
@@ -7,11 +7,23 @@
7
7
  * `compareVersions`; this module is the single authoritative
8
8
  * implementation (Story #4048 B3 — multiplied helpers).
9
9
  *
10
+ * `resolveConsumerPinVersion` (Story #4530) lives here for the same
11
+ * no-mirror-copies reason rather than in either file directly: `update.js`
12
+ * already imports `registry.js` (for the drift-check fallback), so a
13
+ * function needed by both would otherwise force a two-way circular import
14
+ * between them. This module is a dependency-free leaf both already import.
15
+ *
10
16
  * Builtins only — this module is imported from both the CLI surface
11
17
  * (`lib/cli/`) and the doctor registry which runs before third-party
12
18
  * packages are guaranteed to be present.
13
19
  */
14
20
 
21
+ import nodeFs from 'node:fs';
22
+ import path from 'node:path';
23
+
24
+ /** The published package whose consumer pin is resolved below. */
25
+ const PACKAGE_NAME = 'mandrel';
26
+
15
27
  /**
16
28
  * Parse a dotted semver-ish string into a numeric tuple. Non-numeric or
17
29
  * missing segments coerce to 0 so a partial version still compares sanely.
@@ -57,3 +69,122 @@ export function compareVersions(a, b) {
57
69
  export function crossesMajor(current, target) {
58
70
  return parseVersion(target)[0] > parseVersion(current)[0];
59
71
  }
72
+
73
+ /**
74
+ * Does `version` satisfy the range described by `spec` — a
75
+ * `{ operator, version }` pin spec as returned by
76
+ * {@link resolveConsumerPinSpec}?
77
+ *
78
+ * A deliberately minimal caret/tilde/exact implementation rather than a
79
+ * `semver` dependency: `version-helpers.js` is builtins-only (it is imported
80
+ * by the doctor registry, which runs before third-party packages are
81
+ * guaranteed to be present), and the only ranges
82
+ * {@link resolveConsumerPinSpec} ever yields are `^`, `~`, and exact.
83
+ *
84
+ * Semantics match npm's:
85
+ * - `''` (exact) → `version === spec.version`
86
+ * - `~1.2.3` → `>=1.2.3 <1.3.0`
87
+ * - `^1.2.3` → `>=1.2.3 <2.0.0`
88
+ * - `^0.2.3` → `>=0.2.3 <0.3.0` (0.x: the minor axis is the API axis)
89
+ * - `^0.0.3` → `>=0.0.3 <0.0.4`
90
+ *
91
+ * Prerelease tags are ignored on both sides, consistent with
92
+ * {@link parseVersion}'s numeric-tuple contract.
93
+ *
94
+ * @param {string} version
95
+ * @param {{ operator: '^' | '~' | '', version: string }} spec
96
+ * @returns {boolean}
97
+ */
98
+ export function satisfiesPinSpec(version, spec) {
99
+ if (compareVersions(version, spec.version) < 0) return false;
100
+ const [vMajor, vMinor] = parseVersion(version);
101
+ const [pMajor, pMinor, pPatch] = parseVersion(spec.version);
102
+
103
+ if (spec.operator === '~') {
104
+ return vMajor === pMajor && vMinor === pMinor;
105
+ }
106
+ if (spec.operator === '^') {
107
+ if (pMajor > 0) return vMajor === pMajor;
108
+ if (pMinor > 0) return vMajor === 0 && vMinor === pMinor;
109
+ // ^0.0.x pins the patch axis exactly.
110
+ return compareVersions(version, `0.0.${pPatch}`) === 0;
111
+ }
112
+ return compareVersions(version, spec.version) === 0;
113
+ }
114
+
115
+ /**
116
+ * Read the consumer's declared `mandrel` dependency pin from
117
+ * `<consumerRoot>/package.json` (checking `dependencies` then
118
+ * `devDependencies`), returning both the range `operator` and the base
119
+ * `version` it decorates.
120
+ *
121
+ * Callers that only need to decide whether `npm install mandrel@<target>`
122
+ * would move the declared pin want {@link resolveConsumerPinVersion} (the
123
+ * base version alone). Callers that need to ask whether an installed
124
+ * version is *in range* — `mandrel doctor`'s `pin-current` check — need the
125
+ * operator too, because `^2.1.0` and `2.1.0` make very different claims
126
+ * about an installed 2.4.0.
127
+ *
128
+ * Returns `null` under exactly the same conditions as
129
+ * {@link resolveConsumerPinVersion} — see its contract.
130
+ *
131
+ * @param {string} consumerRoot
132
+ * @param {typeof nodeFs} [fsImpl]
133
+ * @returns {{ operator: '^' | '~' | '', version: string } | null}
134
+ */
135
+ export function resolveConsumerPinSpec(consumerRoot, fsImpl = nodeFs) {
136
+ let parsed;
137
+ try {
138
+ const raw = fsImpl.readFileSync(
139
+ path.join(consumerRoot, 'package.json'),
140
+ 'utf8',
141
+ );
142
+ parsed = JSON.parse(raw);
143
+ } catch {
144
+ return null;
145
+ }
146
+ const declared =
147
+ parsed?.dependencies?.[PACKAGE_NAME] ??
148
+ parsed?.devDependencies?.[PACKAGE_NAME];
149
+ if (typeof declared !== 'string') return null;
150
+ const match = /^([\^~]?)(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(
151
+ declared.trim(),
152
+ );
153
+ if (!match) return null;
154
+ return {
155
+ operator: /** @type {'^' | '~' | ''} */ (match[1]),
156
+ version: match[2],
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Read the consumer's declared `mandrel` dependency pin from
162
+ * `<consumerRoot>/package.json` (checking `dependencies` then
163
+ * `devDependencies`), stripping a leading `^`/`~` range operator to
164
+ * recover a base semver for comparison against the registry's newest
165
+ * published version.
166
+ *
167
+ * A base-version compare against the target is the right question here
168
+ * regardless of what the full range technically already matches: the
169
+ * declared pin is exactly what `npm install mandrel@<target>` (the
170
+ * `mandrel update` `npm-update` step) moves, so comparing its base version
171
+ * decides whether that step needs to run.
172
+ *
173
+ * Returns `null` — never throws — when there is no readable `package.json`,
174
+ * no `mandrel` entry in either dependency block, or the declared value
175
+ * isn't a plain exact/caret/tilde semver (a `workspace:`/`file:`/`git+`
176
+ * specifier, `latest`, `*`, or a comparator range). Callers fall back to
177
+ * resolving the actually-installed version in that case — see
178
+ * `lib/cli/update.js#resolveCurrentVersionForUpdate` and
179
+ * `lib/cli/registry.js`'s `pin-current` doctor check (both Story #4530),
180
+ * which is also how a project with no `mandrel` dependency at all —
181
+ * including mandrel's own repo, which carries no self-dependency —
182
+ * degrades to a clean skip rather than a false failure.
183
+ *
184
+ * @param {string} consumerRoot
185
+ * @param {typeof nodeFs} [fsImpl]
186
+ * @returns {string | null}
187
+ */
188
+ export function resolveConsumerPinVersion(consumerRoot, fsImpl = nodeFs) {
189
+ return resolveConsumerPinSpec(consumerRoot, fsImpl)?.version ?? null;
190
+ }
@@ -9,11 +9,13 @@ filtering, idempotency enforcement, and the actionable per-step log line. The
9
9
  `migrations` array is the single source of truth for which steps exist and in
10
10
  what order they run.
11
11
 
12
- > The registry currently ships **empty**. The project sits on the 1.x line
13
- > under release-please `always-bump-minor`, and no real contract break has
14
- > landed yet. The machinery is exercised by fixture steps in
15
- > [`__tests__/index.test.js`](./__tests__/index.test.js). Add the first real
16
- > step here when the first contract cutover lands.
12
+ > Story #4531 registered the first real step
13
+ > [`steps/2.1.0-retire-mi-drop-knobs.js`](./steps/2.1.0-retire-mi-drop-knobs.js),
14
+ > which strips the retired `codingGuardrails.miDropMustRefactor` and
15
+ > `autoRefresh.miDropCap` keys from a consumer's `.agentrc.json`. The
16
+ > machinery itself is still exercised independently by fixture steps in
17
+ > [`__tests__/index.test.js`](./__tests__/index.test.js). Add the next real
18
+ > step here (ascending by version) when the next contract cutover lands.
17
19
 
18
20
  ## Step shape
19
21
 
@@ -43,18 +43,22 @@
43
43
  * already in the tree (the consumer was on that version) and is skipped; a
44
44
  * step at exactly `toVersion` is the target and runs.
45
45
  *
46
- * The registry currently ships empty: v2.0.0's Story-collapse cutover did
47
- * not register a config migration step (consumers wipe/re-sync `.agents/`
48
- * and re-seed `.agentrc.json` from the starter). The machinery is proven
49
- * by the fixture steps in `__tests__/index.test.js`. When the next real
50
- * contract cutover lands, add its step here (ascending by version) with
51
- * an idempotent `detect`/`apply`.
46
+ * v2.0.0's Story-collapse cutover did not register a config migration step
47
+ * (consumers wipe/re-sync `.agents/` and re-seed `.agentrc.json` from the
48
+ * starter). Story #4531 registered the first real step see
49
+ * `steps/2.1.0-retire-mi-drop-knobs.js` alongside the fixture steps in
50
+ * `__tests__/index.test.js` that prove the machinery independent of any
51
+ * real step. When the next contract cutover lands, add its step here
52
+ * (ascending by version) with an idempotent `detect`/`apply`.
52
53
  */
53
54
 
55
+ import { retireMiDropKnobs } from './steps/2.1.0-retire-mi-drop-knobs.js';
56
+ import { retireVerifyConcurrencyCap } from './steps/2.1.0-retire-verify-concurrency-cap.js';
57
+ import { retireEpicAcTags } from './steps/2.2.0-retire-epic-ac-tags.js';
58
+
54
59
  /**
55
60
  * Ordered registry of migration steps. MUST stay sorted ascending by
56
- * `version`. Empty until the first real contract cutover graduates a step
57
- * here.
61
+ * `version`.
58
62
  *
59
63
  * @type {Array<{
60
64
  * version: string,
@@ -63,7 +67,11 @@
63
67
  * apply: (ctx: unknown) => void,
64
68
  * }>}
65
69
  */
66
- export const migrations = [];
70
+ export const migrations = [
71
+ retireMiDropKnobs,
72
+ retireVerifyConcurrencyCap,
73
+ retireEpicAcTags,
74
+ ];
67
75
 
68
76
  /**
69
77
  * Parse a dotted semver-ish string into a numeric tuple for comparison.