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
@@ -31,12 +31,17 @@ import {
31
31
  satisfiesNodeEngine,
32
32
  } from '../../.agents/scripts/lib/bootstrap/project-bootstrap.js';
33
33
  import { isCommandExcluded } from '../../.agents/scripts/lib/command-header.js';
34
+ import { getDeliveryRouting } from '../../.agents/scripts/lib/config/delivery-routing.js';
34
35
  import {
35
36
  defaultResolvePackageRoot,
36
37
  listFiles as listPayloadFiles,
37
38
  } from './sync.js';
38
39
  import { readCache } from './version-check.js';
39
- import { compareVersions as compareSemver } from './version-helpers.js';
40
+ import {
41
+ compareVersions as compareSemver,
42
+ resolveConsumerPinSpec,
43
+ satisfiesPinSpec,
44
+ } from './version-helpers.js';
40
45
 
41
46
  // ---------------------------------------------------------------------------
42
47
  // Internal helpers
@@ -309,6 +314,60 @@ function runCommandsInSync({ projectRoot, cwd, readDir, readFile } = {}) {
309
314
  // check: agents-in-sync
310
315
  // ---------------------------------------------------------------------------
311
316
 
317
+ /**
318
+ * Best-effort raw JSON read — no AJV, no merge validation. Absent, unreadable,
319
+ * or malformed files degrade to `null` so a config problem never crashes a
320
+ * doctor check; the caller applies framework defaults on `null`.
321
+ *
322
+ * @param {string} absPath
323
+ * @param {typeof fs} fsImpl
324
+ * @returns {object | null}
325
+ */
326
+ function readJsonSafe(absPath, fsImpl) {
327
+ try {
328
+ return JSON.parse(fsImpl.readFileSync(absPath, 'utf8'));
329
+ } catch {
330
+ return null;
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Resolve `delivery.routing.roleScopedAgents` (default `true`) for
336
+ * `agents-in-sync`'s fatal/advisory branch below, WITHOUT importing the full
337
+ * `resolveConfig()` chain — that chain pulls in AJV (`config-schema.js`),
338
+ * which would violate this module's own "Node built-ins only" contract (see
339
+ * the module doc: registry.js must load inside the preflight guard, before
340
+ * any third-party package is guaranteed present).
341
+ *
342
+ * Reads `.agentrc.json` and `.agentrc.local.json` directly and shallow-merges
343
+ * only the `delivery.routing` block (local wins per-key) — narrower than the
344
+ * resolver's full deep-merge, but sufficient for this one boolean, and it
345
+ * still honours a local override rather than silently ignoring it. Delegates
346
+ * the actual default/coercion to `getDeliveryRouting` (`config/delivery-
347
+ * routing.js`), which has zero imports of its own and is already the single
348
+ * source of truth for this default elsewhere in the framework.
349
+ *
350
+ * @param {string} projectRoot
351
+ * @param {typeof fs} fsImpl
352
+ * @returns {boolean}
353
+ */
354
+ function resolveRoleScopedAgentsFlag(projectRoot, fsImpl) {
355
+ const base = readJsonSafe(path.join(projectRoot, '.agentrc.json'), fsImpl);
356
+ const local = readJsonSafe(
357
+ path.join(projectRoot, '.agentrc.local.json'),
358
+ fsImpl,
359
+ );
360
+ const merged = {
361
+ delivery: {
362
+ routing: {
363
+ ...(base?.delivery?.routing ?? {}),
364
+ ...(local?.delivery?.routing ?? {}),
365
+ },
366
+ },
367
+ };
368
+ return getDeliveryRouting(merged).roleScopedAgents;
369
+ }
370
+
312
371
  /**
313
372
  * Dry-run the sync-claude-agents logic: compare `.agents/agents/*.md` sources
314
373
  * to the generated `.claude/agents/*.md` role-agent tree and report parity.
@@ -326,30 +385,52 @@ function runCommandsInSync({ projectRoot, cwd, readDir, readFile } = {}) {
326
385
  * A repo with no `.agents/agents/` sources and no `.claude/agents/` tree is a
327
386
  * clean no-op (0 sources, 0 dests → "up to date").
328
387
  *
329
- * **Inert-scaffolding divergence from `commands-in-sync` (#4478 PR-2).** The
330
- * role agents are inert no workflow spawns them yet — so a **never
331
- * materialized** agent tree (sources present, but the `.claude/agents/` tree
332
- * empty) is NOT a readiness failure: it is reported as advisory, not fatal.
333
- * Once the tree HAS been materialized, drift (a source missing from the dest,
334
- * or a stale dest file) fails the check exactly like `commands-in-sync`. M7-B
335
- * (the spawn flip) is where the sync is wired into the delivery flows and this
336
- * tolerance is tightened.
388
+ * **Tightened from advisory to fatal (Story #4530, M7-B).** Prior to this
389
+ * Story, a **never materialized** agent tree (sources present, `.claude/
390
+ * agents/` empty) was reported as advisory — "inert", because no workflow
391
+ * spawned the role agents yet. M7-B has landed: `helpers/deliver-story` Step
392
+ * 1a dispatches `subagent_type: acceptance-critic` on the **default** path
393
+ * (`roleScopedAgents` defaults `true`). An unmaterialized tree under that
394
+ * default is no longer a benign scaffolding gap it is the acceptance
395
+ * ceremony's fresh-context critic silently failing to spawn and falling back
396
+ * to a weaker inline critic, with doctor reporting green throughout. This
397
+ * check now fails in exactly that case. When `roleScopedAgents` is `false`
398
+ * (the operator kill-switch), an unmaterialized tree is expected — the check
399
+ * stays advisory. Once the tree HAS been materialized, drift (a source
400
+ * missing from the dest, or a stale dest file) fails the check exactly like
401
+ * `commands-in-sync`, unconditionally, exactly as before this Story.
337
402
  *
338
403
  * Injectable seams (used by tests so no real filesystem is touched):
339
404
  * - `cwd()` replaces `process.cwd` so tests can pin the consumer root.
340
405
  * - `readDir` replaces `fs.readdirSync`.
406
+ * - `fsImpl` replaces the `node:fs` surface used for the `.agentrc.json` /
407
+ * `.agentrc.local.json` reads.
408
+ * - `roleScopedAgents` overrides the resolved flag directly, bypassing the
409
+ * config reads entirely.
341
410
  *
342
- * @param {{ projectRoot?: string, cwd?: () => string, readDir?: (dir: string) => string[] }} [opts]
411
+ * @param {{
412
+ * projectRoot?: string,
413
+ * cwd?: () => string,
414
+ * readDir?: (dir: string) => string[],
415
+ * fsImpl?: typeof fs,
416
+ * roleScopedAgents?: boolean,
417
+ * }} [opts]
343
418
  * @returns {{ ok: boolean, detail: string, remedy?: string }}
344
419
  */
345
- function runAgentsInSync({ projectRoot, cwd, readDir } = {}) {
420
+ function runAgentsInSync({
421
+ projectRoot,
422
+ cwd,
423
+ readDir,
424
+ fsImpl = fs,
425
+ roleScopedAgents,
426
+ } = {}) {
346
427
  const getCwd = cwd ?? (() => process.cwd());
347
428
  const root = projectRoot ?? getCwd();
348
429
  const listDir =
349
430
  readDir ??
350
431
  ((dir) => {
351
432
  try {
352
- return fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
433
+ return fsImpl.readdirSync(dir).filter((f) => f.endsWith('.md'));
353
434
  } catch {
354
435
  return [];
355
436
  }
@@ -365,14 +446,26 @@ function runAgentsInSync({ projectRoot, cwd, readDir } = {}) {
365
446
  .filter((f) => !f.startsWith('.'))
366
447
  .sort();
367
448
 
368
- // Never-materialized agent tree: inert, so advisory (ok:true), not fatal.
369
449
  if (dests.length === 0) {
450
+ if (sources.length === 0) {
451
+ return { ok: true, detail: '0 agents up to date' };
452
+ }
453
+ const resolvedFlag =
454
+ typeof roleScopedAgents === 'boolean'
455
+ ? roleScopedAgents
456
+ : resolveRoleScopedAgentsFlag(root, fsImpl);
457
+ if (!resolvedFlag) {
458
+ // Kill-switch off: an unmaterialized tree is expected, not a defect.
459
+ return {
460
+ ok: true,
461
+ detail: `${sources.length} agent def(s) not materialized (roleScopedAgents is disabled)`,
462
+ };
463
+ }
370
464
  return {
371
- ok: true,
372
- detail:
373
- sources.length === 0
374
- ? '0 agents up to date'
375
- : `${sources.length} agent def(s) not yet materialized (inert)`,
465
+ ok: false,
466
+ detail: `${sources.length} agent def(s) not yet materialized in .claude/agents/`,
467
+ remedy:
468
+ 'Run `mandrel sync-agents` to regenerate the `.claude/agents/` tree.',
376
469
  };
377
470
  }
378
471
 
@@ -392,7 +485,7 @@ function runAgentsInSync({ projectRoot, cwd, readDir } = {}) {
392
485
  ok: false,
393
486
  detail: parts.join(', '),
394
487
  remedy:
395
- 'Run `npm run sync:agents` to regenerate the `.claude/agents/` tree.',
488
+ 'Run `mandrel sync-agents` to regenerate the `.claude/agents/` tree.',
396
489
  };
397
490
  }
398
491
 
@@ -705,6 +798,149 @@ export function runAgentsDrift({ cwd, fsImpl = fs, resolvePackageRoot } = {}) {
705
798
  };
706
799
  }
707
800
 
801
+ // ---------------------------------------------------------------------------
802
+ // check: pin-current
803
+ // ---------------------------------------------------------------------------
804
+
805
+ /**
806
+ * Fatal doctor check (Story #4525 / #4530): is the version actually
807
+ * resolvable in `node_modules` **consistent with** the consumer's declared
808
+ * `mandrel` dependency range (`package.json`)?
809
+ *
810
+ * This is a **different** signal from `agents-drift` (are the materialized
811
+ * `.agents/` file *contents* byte-identical to the installed payload) and
812
+ * from `version-current` below (is a *newer* version *published*). Neither
813
+ * of those catches the shape of bug #4525 reported: a `package.json` pin
814
+ * that has fallen out of agreement with what `node_modules` actually
815
+ * resolves to — e.g. an out-of-band symlink, or `npm install mandrel@latest
816
+ * --no-save` — which `mandrel doctor` graded as fully green before this
817
+ * check existed, because the only version `defaultCurrentVersion` ever read
818
+ * WAS the inflated `node_modules` one.
819
+ *
820
+ * **Range-aware.** The declared pin is a *range*, not an equality, so the
821
+ * question this check asks is satisfaction, not identity. A consumer who
822
+ * declares `^2.1.0` and runs a routine `npm update` gets 2.4.0 in
823
+ * `node_modules` with `package.json` untouched — that is npm behaving
824
+ * exactly as documented, and under this project's `always-bump-minor`
825
+ * release cadence it is the *normal* steady state, not a defect. Grading it
826
+ * fatal (as this check originally did, comparing only the pin's base
827
+ * version) made `mandrel doctor` cry wolf on healthy installs.
828
+ *
829
+ * The graded contract:
830
+ * - installed **satisfies** the declared range and equals its base version
831
+ * → pass, quietly.
832
+ * - installed **satisfies** the declared range but is newer than its base
833
+ * (`^2.1.0` + 2.4.0) → pass with an **advisory** `detail`. Re-pinning via
834
+ * `mandrel update` is tidier, but nothing is broken. This mirrors how
835
+ * `version-current` stays non-fatal: `ok: true` with the advisory carried
836
+ * in `detail` (the registry's `advisory: true` flag documents an entry
837
+ * whose `run()` *never* fails, which is not true of this check).
838
+ * - installed does **not** satisfy the declared range (`^2.1.0` + 3.0.0, or
839
+ * an exact `2.1.0` pin + 2.4.0) → **fail**. The declared dependency does
840
+ * not describe the code that is running.
841
+ * - installed is **behind** the pin's base version (`^2.4.0` + 2.1.0) →
842
+ * **fail**, and note that `<` the base never satisfies any of the ranges
843
+ * `resolveConsumerPinSpec` yields, so this is a sub-case of the above
844
+ * given a distinct, more actionable message.
845
+ *
846
+ * Skips cleanly (not a failure) when there is no resolvable pin at all — no
847
+ * `package.json`, no `mandrel` entry in `dependencies`/`devDependencies`, or
848
+ * a non-semver specifier (`workspace:`, `git+`, `latest`, a comparator
849
+ * range) — via {@link resolveConsumerPinSpec}'s own `null` contract. This
850
+ * is also how mandrel's own repo (no self-dependency) and workspace/npx
851
+ * consumers degrade to a clean pass rather than a false failure.
852
+ *
853
+ * Injectable seams (used by tests so no real filesystem or package is
854
+ * needed):
855
+ * - `cwd()` — replaces `process.cwd`, anchoring both the pin read and the
856
+ * `node_modules` resolution at the same consumer root (mirrors
857
+ * `agents-drift` / `agents-in-sync`).
858
+ * - `fsImpl` — replaces the `node:fs` surface.
859
+ * - `resolvePackageRoot(fromDir)` — replaces `mandrel` resolution; throws
860
+ * when the package is not installed.
861
+ *
862
+ * @param {{
863
+ * cwd?: () => string,
864
+ * fsImpl?: typeof fs,
865
+ * resolvePackageRoot?: (fromDir: string) => string,
866
+ * }} [opts]
867
+ * @returns {{ ok: boolean, detail: string, remedy?: string }}
868
+ */
869
+ export function runPinCurrent({ cwd, fsImpl = fs, resolvePackageRoot } = {}) {
870
+ const getCwd = cwd ?? (() => process.cwd());
871
+ const resolveRoot = resolvePackageRoot ?? defaultResolvePackageRoot;
872
+ const projectRoot = getCwd();
873
+
874
+ const spec = resolveConsumerPinSpec(projectRoot, fsImpl);
875
+ if (!spec) {
876
+ return {
877
+ ok: true,
878
+ detail: 'no resolvable mandrel dependency pin — skipped',
879
+ };
880
+ }
881
+
882
+ let installedRoot;
883
+ try {
884
+ installedRoot = resolveRoot(projectRoot);
885
+ } catch {
886
+ // No baseline to compare the pin against; agents-materialized /
887
+ // runtime-deps own the "not installed" remedy.
888
+ return {
889
+ ok: true,
890
+ detail: 'mandrel not installed — pin check skipped',
891
+ };
892
+ }
893
+
894
+ let installed;
895
+ try {
896
+ const parsed = JSON.parse(
897
+ fsImpl.readFileSync(path.join(installedRoot, 'package.json'), 'utf8'),
898
+ );
899
+ installed = String(parsed.version);
900
+ } catch {
901
+ return {
902
+ ok: true,
903
+ detail: 'installed version unreadable — pin check skipped',
904
+ };
905
+ }
906
+
907
+ const declared = `${spec.operator}${spec.version}`;
908
+
909
+ if (compareSemver(installed, spec.version) < 0) {
910
+ return {
911
+ ok: false,
912
+ detail: `package.json pins ${declared} but an older v${installed} is installed`,
913
+ remedy:
914
+ 'Run `npm install` to install a version satisfying the package.json pin.',
915
+ };
916
+ }
917
+
918
+ if (!satisfiesPinSpec(installed, spec)) {
919
+ return {
920
+ ok: false,
921
+ detail: `package.json pins ${declared} but v${installed} is installed — outside the declared range`,
922
+ remedy:
923
+ 'Run `mandrel update` to reconcile the package.json pin with the installed version.',
924
+ };
925
+ }
926
+
927
+ if (compareSemver(installed, spec.version) > 0) {
928
+ // In range, just newer than the declared base — the ordinary result of an
929
+ // `npm update` under a caret/tilde pin. Healthy, so `ok: true`; the
930
+ // advisory rides in `detail` (the doctor runner only prints `remedy` for
931
+ // failures).
932
+ return {
933
+ ok: true,
934
+ detail: `v${installed} satisfies the ${declared} pin but is newer than it — run \`mandrel update\` to re-pin (advisory)`,
935
+ };
936
+ }
937
+
938
+ return {
939
+ ok: true,
940
+ detail: `pin ${declared} matches the installed version`,
941
+ };
942
+ }
943
+
708
944
  // ---------------------------------------------------------------------------
709
945
  // check: version-current
710
946
  // ---------------------------------------------------------------------------
@@ -870,6 +1106,13 @@ export const registry = [
870
1106
  name: 'agents-drift',
871
1107
  run: (opts) => runAgentsDrift(opts),
872
1108
  },
1109
+ {
1110
+ name: 'pin-current',
1111
+ // Fatal, unlike version-current below (Story #4525/#4530): a pin/install
1112
+ // disagreement means the declared dependency does not describe the code
1113
+ // that is running.
1114
+ run: (opts) => runPinCurrent(opts),
1115
+ },
873
1116
  {
874
1117
  name: 'version-current',
875
1118
  // Non-fatal: surfaces a cache-only stale-version advisory. `run()` always
@@ -0,0 +1,157 @@
1
+ // lib/cli/sync-agents.js
2
+ /**
3
+ * `mandrel sync-agents` subcommand (Story #4528 / #4530).
4
+ *
5
+ * Thin wrapper that delegates to the canonical sync script
6
+ * (.agents/scripts/sync-claude-agents.js), projecting `.agents/agents/` into
7
+ * a flat `.claude/agents/` tree. Exact sibling of `sync-commands.js` —
8
+ * same delegation shape, same marker-gated refusal check, same seams —
9
+ * targeting the role-agent tree instead of the command tree. The sync
10
+ * script owns all sync logic; this module exists only to expose it through
11
+ * the mandrel CLI surface without reimplementing it.
12
+ *
13
+ * `sync-claude-agents.js` uses top-level await and has no exported `main()`
14
+ * function (marked `cli-opt-out`), so delegation runs via a child process
15
+ * rather than a direct import. Exit code and all output are forwarded
16
+ * verbatim to the caller so `mandrel sync-agents` is transparent to scripts
17
+ * that check $?.
18
+ *
19
+ * ## Prior to this Story
20
+ *
21
+ * `sync-claude-agents.js` was invoked on the **bootstrap** path
22
+ * (`project-bootstrap.js`'s `sync:agents` script, `prepare`, and
23
+ * `runSyncCommands`) but had no `lib/cli/` subcommand counterpart and was
24
+ * never run by `mandrel sync` / `mandrel update` on the CLI path — the gap
25
+ * #4528 reported. See `update.js`'s `driftHealSteps()` /
26
+ * `fullUpgradeSteps()`, which now spawn a `sync-agents` phase alongside
27
+ * `sync-commands` so `.claude/agents/` materializes on that path too.
28
+ *
29
+ * ## Marker-gated refusal (Story #4526 / #4530) — same shape as sync-commands
30
+ *
31
+ * Before delegating, this wrapper checks whether the `.agents/` tree it is
32
+ * about to project agents FROM actually matches the running CLI's own
33
+ * payload version, exactly as `sync-commands.js` does for the command tree.
34
+ * See that module's doc comment for the full rationale and the anchor-trap
35
+ * warning — it applies identically here (`PROJECT_ROOT` is deliberately the
36
+ * package root, used only to read the running CLI's OWN version; the marker
37
+ * read is anchored at `cwd()`, never at `PROJECT_ROOT`).
38
+ *
39
+ * Gate order:
40
+ * 1. Marker present (`.agents/.mandrel-version`) → compare it to the
41
+ * running CLI's own package version. Mismatch → refuse.
42
+ * 2. Marker absent (a pre-marker install) → fall back to the existing
43
+ * `agents-drift` content-hash check. Drift detected → refuse.
44
+ * 3. Clean either way → project as before.
45
+ */
46
+
47
+ import { spawnSync } from 'node:child_process';
48
+ import nodeFs from 'node:fs';
49
+ import path from 'node:path';
50
+ import { fileURLToPath } from 'node:url';
51
+
52
+ import { runAgentsDrift } from './registry.js';
53
+ import { readVersionMarker } from './sync.js';
54
+
55
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
56
+ // lib/cli/ → lib/ → project root
57
+ const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
58
+ const SYNC_SCRIPT = path.join(
59
+ PROJECT_ROOT,
60
+ '.agents',
61
+ 'scripts',
62
+ 'sync-claude-agents.js',
63
+ );
64
+
65
+ /**
66
+ * Resolve the running CLI's own package version from `PROJECT_ROOT`'s
67
+ * `package.json`. See `sync-commands.js`'s "anchor trap" module doc: this is
68
+ * the ONE place `PROJECT_ROOT` (the package's own two-dirs-up root) is the
69
+ * correct anchor.
70
+ *
71
+ * @param {typeof nodeFs} fsImpl
72
+ * @returns {string}
73
+ */
74
+ function resolveOwnPackageVersion(fsImpl) {
75
+ const parsed = JSON.parse(
76
+ fsImpl.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8'),
77
+ );
78
+ return String(parsed.version);
79
+ }
80
+
81
+ /**
82
+ * Run the sync-claude-agents script and forward its output + exit code —
83
+ * unless the marker-gated refusal check (see module doc) finds the
84
+ * `.agents/` tree mismatched against the running CLI, in which case it
85
+ * refuses and exits non-zero before spawning anything.
86
+ *
87
+ * Injectable seams — identical shape to `sync-commands.js`:
88
+ * - `runner` — replaces `spawnSync` for the real delegation.
89
+ * - `cwd()` — replaces `process.cwd`; anchors the marker read at the
90
+ * consumer project root.
91
+ * - `fs` — replaces the `node:fs` surface for both the marker read and the
92
+ * own-version read.
93
+ * - `ownVersion` — overrides the running CLI's own resolved version
94
+ * directly, bypassing `resolveOwnPackageVersion`.
95
+ * - `checkAgentsDrift` — overrides the fallback drift probe used when the
96
+ * marker is absent; defaults to `runAgentsDrift({ cwd })`.
97
+ * - `writeErr` / `exit` — replace the corresponding process surfaces.
98
+ *
99
+ * @param {string[]} _argv - Unused; reserved for future flags.
100
+ * @param {{
101
+ * runner?: typeof spawnSync,
102
+ * cwd?: () => string,
103
+ * fs?: typeof nodeFs,
104
+ * ownVersion?: string,
105
+ * checkAgentsDrift?: () => { ok: boolean, detail: string },
106
+ * writeErr?: (s: string) => void,
107
+ * exit?: (code: number) => void,
108
+ * }} [opts]
109
+ * @returns {void}
110
+ */
111
+ export default function run(
112
+ _argv = [],
113
+ {
114
+ runner = spawnSync,
115
+ cwd = () => process.cwd(),
116
+ fs = nodeFs,
117
+ ownVersion,
118
+ checkAgentsDrift,
119
+ writeErr = (s) => process.stderr.write(s),
120
+ exit = (code) => process.exit(code),
121
+ } = {},
122
+ ) {
123
+ const projectRoot = cwd();
124
+ const resolvedOwnVersion = ownVersion ?? resolveOwnPackageVersion(fs);
125
+ const marker = readVersionMarker(projectRoot, fs);
126
+
127
+ if (marker) {
128
+ if (marker !== resolvedOwnVersion) {
129
+ writeErr(
130
+ `mandrel sync-agents: the materialized .agents/ tree is v${marker} but the running CLI is v${resolvedOwnVersion} — refusing to project .claude/agents/ from a mismatched tree.\n` +
131
+ ' → Run `mandrel sync` to re-materialize .agents/ to the current version, then re-run.\n',
132
+ );
133
+ exit(1);
134
+ return;
135
+ }
136
+ } else {
137
+ const drift = (checkAgentsDrift ?? (() => runAgentsDrift({ cwd })))();
138
+ if (!drift.ok) {
139
+ writeErr(
140
+ `mandrel sync-agents: .agents/ appears to have drifted from the installed package payload (${drift.detail}) — refusing to project .claude/agents/ from a mismatched tree.\n` +
141
+ ' → Run `mandrel sync` to restore the materialized .agents/ payload, then re-run.\n',
142
+ );
143
+ exit(1);
144
+ return;
145
+ }
146
+ }
147
+
148
+ const result = runner(process.execPath, [SYNC_SCRIPT], {
149
+ stdio: 'inherit',
150
+ env: process.env,
151
+ });
152
+
153
+ const exitCode = result.status ?? 1;
154
+ if (exitCode !== 0) {
155
+ exit(exitCode);
156
+ }
157
+ }
@@ -11,12 +11,49 @@
11
11
  * (marked `cli-opt-out`), so delegation runs via a child process rather than
12
12
  * a direct import. Exit code and all output are forwarded verbatim to the
13
13
  * caller so `mandrel sync-commands` is transparent to scripts that check $?.
14
+ *
15
+ * ## Marker-gated refusal (Story #4526 / #4530)
16
+ *
17
+ * Before delegating, this wrapper checks whether the `.agents/` tree it is
18
+ * about to project commands FROM actually matches the running CLI's own
19
+ * payload version. Without this gate, a consumer whose `.agents/` tree lags
20
+ * the installed package (see #4525's pin-skew scenario) gets commands
21
+ * faithfully projected from a **stale** tree — describing a workflow whose
22
+ * backing scripts are not installed. Because `.claude/*` is conventionally
23
+ * gitignored, that wrong projection leaves no diff and no signal until an
24
+ * agent hits a missing-module error mid-task, far from the actual cause.
25
+ *
26
+ * Gate order:
27
+ * 1. Marker present (`.agents/.mandrel-version`, Story #4530) → compare it
28
+ * to the running CLI's own package version. Mismatch → refuse.
29
+ * 2. Marker absent (a pre-marker install with no baseline to compare) →
30
+ * fall back to the existing `agents-drift` content-hash check
31
+ * (`registry.js`). Drift detected → refuse.
32
+ * 3. Clean either way → project as before.
33
+ *
34
+ * ## Anchor trap (do not repeat)
35
+ *
36
+ * `PROJECT_ROOT` below is deliberately the **package** root — resolved from
37
+ * this module's own two-dirs-up location — because it is used ONLY to read
38
+ * the running CLI's OWN version, which is exactly what that self-referential
39
+ * pattern correctly answers (contrast with the #4525 bug in `update.js`,
40
+ * where the same pattern was wrongly used to answer a *consumer*-pin
41
+ * question). The marker read is a SEPARATE resolution, anchored at `cwd()`
42
+ * (the consumer project root) via `readVersionMarker` — never at
43
+ * `PROJECT_ROOT`. Comparing the marker to `PROJECT_ROOT`'s own version would
44
+ * make the check compare the package against itself and could never fire —
45
+ * the exact trap `registry.js`'s `agents-in-sync` check documents for the
46
+ * same reason.
14
47
  */
15
48
 
16
49
  import { spawnSync } from 'node:child_process';
50
+ import nodeFs from 'node:fs';
17
51
  import path from 'node:path';
18
52
  import { fileURLToPath } from 'node:url';
19
53
 
54
+ import { runAgentsDrift } from './registry.js';
55
+ import { readVersionMarker } from './sync.js';
56
+
20
57
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
58
  // lib/cli/ → lib/ → project root
22
59
  const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
@@ -28,16 +65,88 @@ const SYNC_SCRIPT = path.join(
28
65
  );
29
66
 
30
67
  /**
31
- * Run the sync-claude-commands script and forward its output + exit code.
68
+ * Resolve the running CLI's own package version from `PROJECT_ROOT`'s
69
+ * `package.json`. See the "anchor trap" module doc: this is the ONE place
70
+ * `PROJECT_ROOT` (the package's own two-dirs-up root) is the correct anchor.
71
+ *
72
+ * @param {typeof nodeFs} fsImpl
73
+ * @returns {string}
74
+ */
75
+ function resolveOwnPackageVersion(fsImpl) {
76
+ const parsed = JSON.parse(
77
+ fsImpl.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8'),
78
+ );
79
+ return String(parsed.version);
80
+ }
81
+
82
+ /**
83
+ * Run the sync-claude-commands script and forward its output + exit code —
84
+ * unless the marker-gated refusal check (see module doc) finds the
85
+ * `.agents/` tree mismatched against the running CLI, in which case it
86
+ * refuses and exits non-zero before spawning anything.
32
87
  *
33
- * Injectable seam: `runner` replaces `spawnSync` so tests can drive every
34
- * branch without spawning a real child process.
88
+ * Injectable seams (used by tests so no real filesystem, child process, or
89
+ * package resolution occurs on the gate path):
90
+ * - `runner` — replaces `spawnSync` for the real delegation.
91
+ * - `cwd()` — replaces `process.cwd`; anchors the marker read at the
92
+ * consumer project root.
93
+ * - `fs` — replaces the `node:fs` surface for both the marker read and the
94
+ * own-version read.
95
+ * - `ownVersion` — overrides the running CLI's own resolved version
96
+ * directly, bypassing `resolveOwnPackageVersion`.
97
+ * - `checkAgentsDrift` — overrides the fallback drift probe used when the
98
+ * marker is absent; defaults to `runAgentsDrift({ cwd })`.
99
+ * - `writeErr` / `exit` — replace the corresponding process surfaces.
35
100
  *
36
101
  * @param {string[]} _argv - Unused; reserved for future flags.
37
- * @param {{ runner?: typeof spawnSync }} [opts]
102
+ * @param {{
103
+ * runner?: typeof spawnSync,
104
+ * cwd?: () => string,
105
+ * fs?: typeof nodeFs,
106
+ * ownVersion?: string,
107
+ * checkAgentsDrift?: () => { ok: boolean, detail: string },
108
+ * writeErr?: (s: string) => void,
109
+ * exit?: (code: number) => void,
110
+ * }} [opts]
38
111
  * @returns {void}
39
112
  */
40
- export default function run(_argv = [], { runner = spawnSync } = {}) {
113
+ export default function run(
114
+ _argv = [],
115
+ {
116
+ runner = spawnSync,
117
+ cwd = () => process.cwd(),
118
+ fs = nodeFs,
119
+ ownVersion,
120
+ checkAgentsDrift,
121
+ writeErr = (s) => process.stderr.write(s),
122
+ exit = (code) => process.exit(code),
123
+ } = {},
124
+ ) {
125
+ const projectRoot = cwd();
126
+ const resolvedOwnVersion = ownVersion ?? resolveOwnPackageVersion(fs);
127
+ const marker = readVersionMarker(projectRoot, fs);
128
+
129
+ if (marker) {
130
+ if (marker !== resolvedOwnVersion) {
131
+ writeErr(
132
+ `mandrel sync-commands: the materialized .agents/ tree is v${marker} but the running CLI is v${resolvedOwnVersion} — refusing to project .claude/commands/ from a mismatched tree.\n` +
133
+ ' → Run `mandrel sync` to re-materialize .agents/ to the current version, then re-run.\n',
134
+ );
135
+ exit(1);
136
+ return;
137
+ }
138
+ } else {
139
+ const drift = (checkAgentsDrift ?? (() => runAgentsDrift({ cwd })))();
140
+ if (!drift.ok) {
141
+ writeErr(
142
+ `mandrel sync-commands: .agents/ appears to have drifted from the installed package payload (${drift.detail}) — refusing to project .claude/commands/ from a mismatched tree.\n` +
143
+ ' → Run `mandrel sync` to restore the materialized .agents/ payload, then re-run.\n',
144
+ );
145
+ exit(1);
146
+ return;
147
+ }
148
+ }
149
+
41
150
  const result = runner(process.execPath, [SYNC_SCRIPT], {
42
151
  stdio: 'inherit',
43
152
  env: process.env,
@@ -45,6 +154,6 @@ export default function run(_argv = [], { runner = spawnSync } = {}) {
45
154
 
46
155
  const exitCode = result.status ?? 1;
47
156
  if (exitCode !== 0) {
48
- process.exit(exitCode);
157
+ exit(exitCode);
49
158
  }
50
159
  }