mandrel 1.94.0 → 2.1.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 (560) hide show
  1. package/.agents/README.md +116 -99
  2. package/.agents/agents/acceptance-critic.md +9 -7
  3. package/.agents/agents/story-worker.md +45 -51
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +498 -1287
  6. package/.agents/docs/agentrc-reference.json +185 -80
  7. package/.agents/docs/configuration.md +132 -190
  8. package/.agents/docs/execution-reference.md +51 -25
  9. package/.agents/docs/quality-gates.md +25 -33
  10. package/.agents/docs/workflows.md +8 -8
  11. package/.agents/instructions.md +113 -139
  12. package/.agents/rules/ci-remediation.md +11 -15
  13. package/.agents/rules/git-conventions-reference.md +48 -58
  14. package/.agents/rules/git-conventions.md +16 -22
  15. package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
  16. package/.agents/schemas/agentrc.schema.json +83 -254
  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/retro.end.schema.json +1 -1
  24. package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
  25. package/.agents/schemas/signal-event.schema.json +3 -3
  26. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  27. package/.agents/schemas/validation-evidence.schema.json +1 -1
  28. package/.agents/scripts/acceptance-eval.js +22 -66
  29. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  30. package/.agents/scripts/audit-to-stories.js +7 -7
  31. package/.agents/scripts/boot-sweep.js +1 -1
  32. package/.agents/scripts/bootstrap.js +3 -3
  33. package/.agents/scripts/check-dead-exports.js +43 -104
  34. package/.agents/scripts/check-doc-links.js +2 -2
  35. package/.agents/scripts/check-lifecycle-lint.js +7 -10
  36. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  37. package/.agents/scripts/cleanup-repo-test-temp.js +6 -1
  38. package/.agents/scripts/deliver-recover.js +122 -0
  39. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  40. package/.agents/scripts/evidence-gate.js +20 -50
  41. package/.agents/scripts/generate-skills-index.js +17 -1
  42. package/.agents/scripts/generate-workflows-doc.js +4 -4
  43. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  44. package/.agents/scripts/lib/Logger.js +6 -10
  45. package/.agents/scripts/lib/audit-suite/runner.js +2 -2
  46. package/.agents/scripts/lib/audit-suite/selector.js +328 -28
  47. package/.agents/scripts/lib/audit-to-stories/{seed-epic-from-findings.js → seed-from-findings.js} +9 -9
  48. package/.agents/scripts/lib/baselines/kernel.js +206 -18
  49. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -15
  50. package/.agents/scripts/lib/baselines/reader.js +1 -6
  51. package/.agents/scripts/lib/bdd-runner-detect.js +5 -9
  52. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  53. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +32 -33
  54. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  55. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  56. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  57. package/.agents/scripts/lib/checks/core-bare-clean.js +6 -3
  58. package/.agents/scripts/lib/checks/index.js +3 -2
  59. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  60. package/.agents/scripts/lib/checks/state.js +17 -248
  61. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +26 -24
  62. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  63. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  64. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  65. package/.agents/scripts/lib/cli/standard-args.js +13 -22
  66. package/.agents/scripts/lib/cli-args.js +39 -9
  67. package/.agents/scripts/lib/close-validation/gates.js +15 -15
  68. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  69. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  70. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  71. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  72. package/.agents/scripts/lib/config/ci.js +6 -31
  73. package/.agents/scripts/lib/config/delivery-routing.js +52 -35
  74. package/.agents/scripts/lib/config/explain.js +61 -48
  75. package/.agents/scripts/lib/config/github.js +7 -5
  76. package/.agents/scripts/lib/config/limits.js +29 -80
  77. package/.agents/scripts/lib/config/paths.js +0 -2
  78. package/.agents/scripts/lib/config/quality.js +12 -15
  79. package/.agents/scripts/lib/config/runners.js +20 -66
  80. package/.agents/scripts/lib/config/temp-paths.js +30 -63
  81. package/.agents/scripts/lib/config/worktree-isolation.js +0 -5
  82. package/.agents/scripts/lib/config-resolver.js +2 -7
  83. package/.agents/scripts/lib/config-settings-schema-delivery.js +55 -161
  84. package/.agents/scripts/lib/config-settings-schema-quality.js +17 -16
  85. package/.agents/scripts/lib/config-settings-schema.js +100 -60
  86. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  87. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  88. package/.agents/scripts/lib/dependency-parser.js +3 -2
  89. package/.agents/scripts/lib/doc-tiers.js +2 -2
  90. package/.agents/scripts/lib/duplicate-search.js +242 -41
  91. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
  92. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  93. package/.agents/scripts/lib/format-generated-json.js +97 -0
  94. package/.agents/scripts/lib/framework-version.js +19 -190
  95. package/.agents/scripts/lib/gh-exec.js +8 -0
  96. package/.agents/scripts/lib/git-branch-cleanup.js +1 -10
  97. package/.agents/scripts/lib/git-branch-lifecycle.js +17 -180
  98. package/.agents/scripts/lib/git-utils.js +32 -20
  99. package/.agents/scripts/lib/github/framework-repo.js +6 -0
  100. package/.agents/scripts/lib/json-utils.js +1 -2
  101. package/.agents/scripts/lib/label-constants.js +10 -38
  102. package/.agents/scripts/lib/label-taxonomy.js +10 -55
  103. package/.agents/scripts/lib/observability/active-story-env.js +44 -165
  104. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  105. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  106. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  107. package/.agents/scripts/lib/observability/source-classifier.js +5 -5
  108. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  109. package/.agents/scripts/lib/onboard/init-tail.js +1 -3
  110. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  111. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  112. package/.agents/scripts/lib/orchestration/ceremony-routing.js +105 -44
  113. package/.agents/scripts/lib/orchestration/code-review.js +78 -436
  114. package/.agents/scripts/lib/orchestration/column-sync.js +1 -1
  115. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +4 -4
  116. package/.agents/scripts/lib/orchestration/context-envelope.js +2 -5
  117. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  118. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  119. package/.agents/scripts/lib/orchestration/docs-digest.js +8 -8
  120. package/.agents/scripts/lib/orchestration/file-assumptions.js +7 -13
  121. package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
  122. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  123. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  124. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  125. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +17 -19
  126. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  127. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  128. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +6 -3
  129. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +21 -43
  130. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  131. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  132. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +4 -4
  133. package/.agents/scripts/lib/orchestration/merge-block-class.js +77 -21
  134. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  135. package/.agents/scripts/lib/orchestration/phase-runner.js +3 -2
  136. package/.agents/scripts/lib/orchestration/plan-context.js +354 -282
  137. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +11 -22
  138. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +4 -8
  139. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  140. package/.agents/scripts/lib/orchestration/plan-navigation.js +92 -0
  141. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +71 -0
  142. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +245 -0
  143. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  144. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +432 -858
  145. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +778 -0
  146. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +47 -115
  147. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  148. package/.agents/scripts/lib/orchestration/plan-reachability.js +9 -14
  149. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +1 -1
  150. package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/authoring-context.js +52 -51
  151. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +27 -0
  152. package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/spec-authoring-grounding.js +1 -1
  153. package/.agents/scripts/lib/orchestration/pr-base-guard.js +18 -28
  154. package/.agents/scripts/lib/orchestration/remote-verifier.js +1 -1
  155. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  156. package/.agents/scripts/lib/orchestration/resolves-token.js +1 -1
  157. package/.agents/scripts/lib/orchestration/retro-proposals.js +140 -79
  158. package/.agents/scripts/lib/orchestration/review-depth.js +126 -47
  159. package/.agents/scripts/lib/orchestration/review-providers/codex.js +2 -2
  160. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  161. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  162. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  163. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +21 -56
  164. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  165. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  166. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  167. package/.agents/scripts/lib/orchestration/run-epilogue.js +784 -0
  168. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +25 -1
  169. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  170. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  171. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +86 -41
  172. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  173. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +77 -32
  174. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  175. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  176. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +44 -0
  177. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  178. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  179. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +275 -46
  180. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  181. package/.agents/scripts/lib/orchestration/spec-freshness.js +14 -205
  182. package/.agents/scripts/lib/orchestration/spec-section-validator.js +4 -5
  183. package/.agents/scripts/lib/orchestration/spec-spill.js +60 -0
  184. package/.agents/scripts/lib/orchestration/split-policy-validator.js +188 -0
  185. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +49 -0
  186. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  187. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +28 -42
  188. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  189. package/.agents/scripts/lib/orchestration/story-follow-ups.js +298 -0
  190. package/.agents/scripts/lib/orchestration/story-init-remote.js +51 -0
  191. package/.agents/scripts/lib/orchestration/story-plan-state.js +33 -0
  192. package/.agents/scripts/lib/orchestration/structured-comment-parser.js +1 -1
  193. package/.agents/scripts/lib/orchestration/task-body-validator.js +60 -25
  194. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  195. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +130 -75
  196. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +192 -377
  197. package/.agents/scripts/lib/orchestration/ticket-validator.js +123 -25
  198. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -72
  199. package/.agents/scripts/lib/orchestration/ticketing/reads.js +45 -55
  200. package/.agents/scripts/lib/orchestration/ticketing/transition.js +66 -6
  201. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  202. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  203. package/.agents/scripts/lib/planning-corpus.js +12 -281
  204. package/.agents/scripts/lib/preflight-runner.js +4 -4
  205. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -5
  206. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  207. package/.agents/scripts/lib/signals/index.js +4 -17
  208. package/.agents/scripts/lib/signals/read.js +35 -35
  209. package/.agents/scripts/lib/signals/schema.js +8 -11
  210. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  211. package/.agents/scripts/lib/signals/write.js +0 -1
  212. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  213. package/.agents/scripts/lib/single-story-sweep/protection-ctx.js +1 -1
  214. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  215. package/.agents/scripts/lib/story-adjacency.js +17 -19
  216. package/.agents/scripts/lib/story-body/story-body.js +130 -75
  217. package/.agents/scripts/lib/story-plan.js +2 -4
  218. package/.agents/scripts/lib/templates/decomposer-prompts.js +51 -46
  219. package/.agents/scripts/lib/templates/spec-author-prompts.js +47 -45
  220. package/.agents/scripts/lib/test-env.js +14 -1
  221. package/.agents/scripts/lib/test-tiers.js +0 -3
  222. package/.agents/scripts/lib/{epic-body-sections.js → ticket-body-sections.js} +25 -39
  223. package/.agents/scripts/lib/validation-evidence.js +32 -60
  224. package/.agents/scripts/lib/wave-runner/ready-set.js +38 -12
  225. package/.agents/scripts/lib/workspace-provisioner.js +1 -1
  226. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  227. package/.agents/scripts/lib/worktree/lifecycle/reap.js +72 -25
  228. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  229. package/.agents/scripts/lint-issue-body.js +71 -40
  230. package/.agents/scripts/mandrel-update-preflight.js +1 -1
  231. package/.agents/scripts/notify.js +4 -3
  232. package/.agents/scripts/plan-context.js +102 -80
  233. package/.agents/scripts/plan-persist.js +230 -279
  234. package/.agents/scripts/plan-run-epilogue.js +142 -0
  235. package/.agents/scripts/post-structured-comment.js +0 -38
  236. package/.agents/scripts/pr-watch-with-update.js +43 -22
  237. package/.agents/scripts/providers/github/compose.js +0 -1
  238. package/.agents/scripts/providers/github/errors.js +0 -19
  239. package/.agents/scripts/providers/github/issues.js +13 -39
  240. package/.agents/scripts/providers/github/mappers.js +5 -12
  241. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  242. package/.agents/scripts/providers/github/tickets.js +33 -156
  243. package/.agents/scripts/providers/github.js +17 -6
  244. package/.agents/scripts/resolve-stories.js +236 -0
  245. package/.agents/scripts/run-coverage.js +4 -1
  246. package/.agents/scripts/run-lint.js +2 -2
  247. package/.agents/scripts/run-verify.js +31 -2
  248. package/.agents/scripts/signals-view.js +25 -21
  249. package/.agents/scripts/single-story-close.js +178 -26
  250. package/.agents/scripts/single-story-confirm-merge.js +313 -24
  251. package/.agents/scripts/single-story-init.js +35 -30
  252. package/.agents/scripts/stories-wave-tick.js +85 -10
  253. package/.agents/scripts/story-plan.js +28 -49
  254. package/.agents/scripts/update-ticket-state.js +14 -65
  255. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  256. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  257. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  258. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  259. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  260. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  261. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  262. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  263. package/.agents/skills/core/idea-refinement/SKILL.md +9 -9
  264. package/.agents/skills/core/scope-triage/SKILL.md +31 -172
  265. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  266. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  267. package/.agents/skills/skills.index.json +3 -53
  268. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  269. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  270. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  271. package/.agents/starter-agentrc.json +0 -5
  272. package/.agents/templates/agent-protocol.md +9 -10
  273. package/.agents/workflows/audit-architecture.md +6 -7
  274. package/.agents/workflows/audit-clean-code.md +7 -7
  275. package/.agents/workflows/audit-dependencies.md +3 -3
  276. package/.agents/workflows/audit-devops.md +3 -3
  277. package/.agents/workflows/audit-documentation.md +9 -10
  278. package/.agents/workflows/audit-lighthouse.md +11 -3
  279. package/.agents/workflows/audit-navigability.md +13 -2
  280. package/.agents/workflows/audit-performance.md +5 -6
  281. package/.agents/workflows/audit-privacy.md +3 -3
  282. package/.agents/workflows/audit-quality.md +11 -12
  283. package/.agents/workflows/audit-security.md +4 -5
  284. package/.agents/workflows/audit-seo.md +13 -3
  285. package/.agents/workflows/audit-sre.md +3 -3
  286. package/.agents/workflows/audit-to-stories.md +20 -20
  287. package/.agents/workflows/audit-ux-ui.md +10 -3
  288. package/.agents/workflows/deliver.md +177 -176
  289. package/.agents/workflows/git-cleanup.md +5 -6
  290. package/.agents/workflows/git-deliver.md +1 -1
  291. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  292. package/.agents/workflows/helpers/acceptance-self-eval.md +35 -40
  293. package/.agents/workflows/helpers/code-quality-guardrails.md +7 -7
  294. package/.agents/workflows/helpers/code-review.md +75 -196
  295. package/.agents/workflows/helpers/{single-story-deliver-reference.md → deliver-story-reference.md} +83 -44
  296. package/.agents/workflows/helpers/deliver-story.md +606 -0
  297. package/.agents/workflows/helpers/diagnose.md +10 -10
  298. package/.agents/workflows/helpers/parallel-tooling.md +3 -3
  299. package/.agents/workflows/helpers/signals.md +16 -16
  300. package/.agents/workflows/helpers/worktree-lifecycle.md +66 -86
  301. package/.agents/workflows/mandrel-update.md +2 -1
  302. package/.agents/workflows/plan.md +277 -145
  303. package/.agents/workflows/qa-assist.md +27 -33
  304. package/.agents/workflows/qa-explore.md +29 -38
  305. package/.agents/workflows/qa-run.md +2 -6
  306. package/README.md +9 -8
  307. package/bin/mandrel.js +12 -1
  308. package/docs/CHANGELOG.md +70 -0
  309. package/lib/cli/registry.js +262 -19
  310. package/lib/cli/sync-agents.js +157 -0
  311. package/lib/cli/sync-commands.js +115 -6
  312. package/lib/cli/sync.js +168 -6
  313. package/lib/cli/update.js +105 -8
  314. package/lib/cli/version-helpers.js +131 -0
  315. package/lib/migrations/README.md +7 -5
  316. package/lib/migrations/index.js +12 -8
  317. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  318. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  319. package/package.json +2 -2
  320. package/.agents/agents/retro.md +0 -42
  321. package/.agents/personas/architect.md +0 -113
  322. package/.agents/personas/devops-engineer.md +0 -38
  323. package/.agents/personas/engineer.md +0 -33
  324. package/.agents/personas/project-manager.md +0 -114
  325. package/.agents/personas/qa-engineer.md +0 -95
  326. package/.agents/personas/security-engineer.md +0 -111
  327. package/.agents/personas/technical-writer.md +0 -101
  328. package/.agents/schemas/dispatch-manifest.json +0 -232
  329. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  330. package/.agents/schemas/epic-spec.schema.json +0 -153
  331. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  332. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  333. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  334. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  335. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  336. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  337. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  338. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  339. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  340. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  341. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  342. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  343. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  344. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  345. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  346. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  347. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  348. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  349. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  350. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  351. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  352. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  353. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  354. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  355. package/.agents/schemas/lifecycle/slice.end.schema.json +0 -21
  356. package/.agents/schemas/lifecycle/slice.heartbeat.schema.json +0 -20
  357. package/.agents/schemas/lifecycle/slice.start.schema.json +0 -17
  358. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  359. package/.agents/schemas/risk-verdict.schema.json +0 -66
  360. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  361. package/.agents/scripts/acceptance-spec-reconciler.js +0 -642
  362. package/.agents/scripts/analyze-execution.js +0 -444
  363. package/.agents/scripts/bookkeeping-reconcile.js +0 -117
  364. package/.agents/scripts/check-prepush-recovery.js +0 -90
  365. package/.agents/scripts/dispatcher.js +0 -295
  366. package/.agents/scripts/epic-audit-prepare.js +0 -497
  367. package/.agents/scripts/epic-audit-recheck.js +0 -274
  368. package/.agents/scripts/epic-deliver-note-intervention.js +0 -192
  369. package/.agents/scripts/epic-deliver-preflight.js +0 -462
  370. package/.agents/scripts/epic-deliver-prepare.js +0 -852
  371. package/.agents/scripts/epic-execute-record-wave.js +0 -449
  372. package/.agents/scripts/epic-plan-clarity.js +0 -211
  373. package/.agents/scripts/epic-plan-healthcheck.js +0 -581
  374. package/.agents/scripts/epic-reconcile.js +0 -625
  375. package/.agents/scripts/lib/baseline-snapshot.js +0 -979
  376. package/.agents/scripts/lib/checks/epic-merge-lock-stale.js +0 -54
  377. package/.agents/scripts/lib/checks/stale-origin-epic.js +0 -49
  378. package/.agents/scripts/lib/config/lifecycle.js +0 -40
  379. package/.agents/scripts/lib/config/preflight.js +0 -58
  380. package/.agents/scripts/lib/config/retro.js +0 -77
  381. package/.agents/scripts/lib/epic-merge-lock.js +0 -322
  382. package/.agents/scripts/lib/epic-plan-clarity.js +0 -181
  383. package/.agents/scripts/lib/epic-plan-ideation.js +0 -261
  384. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  385. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  386. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -219
  387. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  388. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  389. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  390. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -270
  391. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +0 -539
  392. package/.agents/scripts/lib/orchestration/deliver-route.js +0 -173
  393. package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -134
  394. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +0 -183
  395. package/.agents/scripts/lib/orchestration/epic-cleanup.js +0 -801
  396. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +0 -310
  397. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +0 -163
  398. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +0 -140
  399. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +0 -64
  400. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +0 -72
  401. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +0 -156
  402. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +0 -345
  403. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +0 -41
  404. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/reconcile-spawn.js +0 -86
  405. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +0 -391
  406. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +0 -94
  407. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +0 -236
  408. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/risk-verdict.js +0 -105
  409. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +0 -307
  410. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +0 -117
  411. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +0 -117
  412. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +0 -621
  413. package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +0 -186
  414. package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +0 -50
  415. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +0 -129
  416. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +0 -103
  417. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +0 -267
  418. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -210
  419. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +0 -238
  420. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/_bullet-format.js +0 -32
  421. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +0 -203
  422. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +0 -227
  423. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +0 -117
  424. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/stalled-worktree.js +0 -37
  425. package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +0 -127
  426. package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +0 -400
  427. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +0 -276
  428. package/.agents/scripts/lib/orchestration/epic-runner/wave-scheduler.js +0 -66
  429. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +0 -789
  430. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +0 -676
  431. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-discriminator.js +0 -389
  432. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-format.js +0 -230
  433. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +0 -361
  434. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  435. package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +0 -306
  436. package/.agents/scripts/lib/orchestration/finalize/post-handoff-comment.js +0 -489
  437. package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +0 -88
  438. package/.agents/scripts/lib/orchestration/lifecycle/emit-slice-lifecycle.js +0 -270
  439. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +0 -147
  440. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  441. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  442. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +0 -465
  443. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +0 -501
  444. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +0 -984
  445. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +0 -264
  446. package/.agents/scripts/lib/orchestration/lifecycle/listeners/checkpoint-pointer-writer.js +0 -284
  447. package/.agents/scripts/lib/orchestration/lifecycle/listeners/cleaner.js +0 -355
  448. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +0 -673
  449. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +0 -378
  450. package/.agents/scripts/lib/orchestration/lifecycle/listeners/intervention-recorder.js +0 -140
  451. package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +0 -144
  452. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  453. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +0 -174
  454. package/.agents/scripts/lib/orchestration/manifest-builder.js +0 -222
  455. package/.agents/scripts/lib/orchestration/plan-persist/amend.js +0 -359
  456. package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +0 -127
  457. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  458. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  459. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  460. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  461. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -33
  462. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  463. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  464. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  465. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -396
  466. package/.agents/scripts/lib/orchestration/post-merge-pipeline.js +0 -205
  467. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  468. package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +0 -152
  469. package/.agents/scripts/lib/orchestration/retro/phases/checks.js +0 -94
  470. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +0 -571
  471. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +0 -450
  472. package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +0 -191
  473. package/.agents/scripts/lib/orchestration/retro-heuristics.js +0 -57
  474. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  475. package/.agents/scripts/lib/orchestration/retro-runner.js +0 -197
  476. package/.agents/scripts/lib/orchestration/spec-renderer.js +0 -447
  477. package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +0 -747
  478. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/gate-failure.js +0 -211
  479. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +0 -158
  480. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +0 -446
  481. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +0 -297
  482. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/scope-discovery.js +0 -48
  483. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution-wiring.js +0 -67
  484. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution.js +0 -161
  485. package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +0 -117
  486. package/.agents/scripts/lib/orchestration/story-close/cd-out-guard.js +0 -86
  487. package/.agents/scripts/lib/orchestration/story-close/cleanup-reconciler.js +0 -147
  488. package/.agents/scripts/lib/orchestration/story-close/close-inputs.js +0 -142
  489. package/.agents/scripts/lib/orchestration/story-close/comment-bodies.js +0 -62
  490. package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +0 -658
  491. package/.agents/scripts/lib/orchestration/story-close/merge-subject.js +0 -198
  492. package/.agents/scripts/lib/orchestration/story-close/phases/branch-restore.js +0 -105
  493. package/.agents/scripts/lib/orchestration/story-close/phases/close.js +0 -222
  494. package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +0 -292
  495. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +0 -270
  496. package/.agents/scripts/lib/orchestration/story-close/phases/preflight.js +0 -110
  497. package/.agents/scripts/lib/orchestration/story-close/phases/refresh.js +0 -86
  498. package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked-emitter.js +0 -112
  499. package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked.js +0 -157
  500. package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +0 -421
  501. package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +0 -302
  502. package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +0 -163
  503. package/.agents/scripts/lib/orchestration/story-close-recovery.js +0 -690
  504. package/.agents/scripts/lib/orchestration/wave-marker.js +0 -28
  505. package/.agents/scripts/lib/orchestration/wave-record-io.js +0 -218
  506. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +0 -145
  507. package/.agents/scripts/lib/orchestration/wave-record-projection.js +0 -212
  508. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +0 -111
  509. package/.agents/scripts/lib/presentation/manifest-builder.js +0 -239
  510. package/.agents/scripts/lib/presentation/manifest-formatter.js +0 -242
  511. package/.agents/scripts/lib/presentation/manifest-helpers.js +0 -213
  512. package/.agents/scripts/lib/presentation/manifest-persistence.js +0 -261
  513. package/.agents/scripts/lib/presentation/manifest-procedures.js +0 -55
  514. package/.agents/scripts/lib/presentation/manifest-render-waves.js +0 -306
  515. package/.agents/scripts/lib/presentation/manifest-renderer.js +0 -188
  516. package/.agents/scripts/lib/presentation/manifest-story-views.js +0 -110
  517. package/.agents/scripts/lib/push-epic-retry.js +0 -209
  518. package/.agents/scripts/lib/spec/index.js +0 -36
  519. package/.agents/scripts/lib/spec/loader.js +0 -425
  520. package/.agents/scripts/lib/spec/state.js +0 -208
  521. package/.agents/scripts/lib/story-init/blocker-validator.js +0 -68
  522. package/.agents/scripts/lib/story-init/branch-initializer.js +0 -408
  523. package/.agents/scripts/lib/story-init/context-resolver.js +0 -92
  524. package/.agents/scripts/lib/story-init/donor-precheck.js +0 -207
  525. package/.agents/scripts/lib/story-init/state-transitioner.js +0 -80
  526. package/.agents/scripts/lib/story-init/task-graph-builder.js +0 -124
  527. package/.agents/scripts/lib/story-init/transition-summary.js +0 -34
  528. package/.agents/scripts/lib/test-reserved-epic-temp-ids.js +0 -35
  529. package/.agents/scripts/lib/wave-runner/tick.js +0 -754
  530. package/.agents/scripts/lib/wave-runner/wave-runner-error.js +0 -20
  531. package/.agents/scripts/lifecycle-emit-story-dispatch.js +0 -194
  532. package/.agents/scripts/lifecycle-emit.js +0 -510
  533. package/.agents/scripts/retro-run.js +0 -218
  534. package/.agents/scripts/slice-phase.js +0 -361
  535. package/.agents/scripts/standalone-feedback-rollup.js +0 -188
  536. package/.agents/scripts/story-close.js +0 -294
  537. package/.agents/scripts/story-init.js +0 -599
  538. package/.agents/scripts/story-phase.js +0 -369
  539. package/.agents/scripts/wave-tick.js +0 -464
  540. package/.agents/skills/core/analyze-execution/SKILL.md +0 -98
  541. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +0 -172
  542. package/.agents/skills/core/epic-plan-consolidate/examples.md +0 -51
  543. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +0 -441
  544. package/.agents/skills/core/epic-plan-decompose-author/examples.md +0 -47
  545. package/.agents/skills/core/epic-plan-premortem/SKILL.md +0 -146
  546. package/.agents/skills/core/epic-plan-premortem/examples.md +0 -53
  547. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +0 -383
  548. package/.agents/skills/core/epic-plan-spec-author/examples.md +0 -91
  549. package/.agents/workflows/helpers/deliver-epic-reference.md +0 -547
  550. package/.agents/workflows/helpers/deliver-epic-single.md +0 -331
  551. package/.agents/workflows/helpers/deliver-epic.md +0 -998
  552. package/.agents/workflows/helpers/deliver-stories.md +0 -450
  553. package/.agents/workflows/helpers/epic-audit.md +0 -189
  554. package/.agents/workflows/helpers/epic-deliver-story.md +0 -436
  555. package/.agents/workflows/helpers/epic-testing.md +0 -125
  556. package/.agents/workflows/helpers/plan-epic-reference.md +0 -160
  557. package/.agents/workflows/helpers/plan-epic.md +0 -353
  558. package/.agents/workflows/helpers/plan-story.md +0 -251
  559. package/.agents/workflows/helpers/scope-triage-gate.md +0 -108
  560. package/.agents/workflows/helpers/single-story-deliver.md +0 -557
@@ -1,135 +1,49 @@
1
1
  /**
2
- * run-plan-persist.js — single GitHub-write surface for the /plan collapse
3
- * (Epic #4474, PR3 + PR4 modes).
2
+ * run-plan-persist.js — flat Story persist for the v2 `/plan` collapse
3
+ * (Stage 3 `docs/roadmap.md`).
4
4
  *
5
- * Implements the ordered, fail-closed superset persist that replaced the
6
- * retired 12-phase pipeline's separate persist halves
7
- * (design §1 Step 3 + §2 mode matrix, issue #4474):
5
+ * Ordered, fail-closed pipeline:
8
6
  *
9
- * 1. args (owned by the `plan-persist.js` CLI shell)
10
- * 2. section gate `validateSpecSections` runs BEFORE the lease and
11
- * BEFORE any provider call; a rejection makes zero GitHub calls.
12
- * 3. risk-verdict validation (CLI-owned `loadRiskVerdict`) +
13
- * mode-coherence hard error (`resolveDeliveryMode`): fan-out requires
14
- * tickets; `deliveryShape: "single"` refuses any tickets payload
15
- * the single mode's validator/DAG skip is fenced by construction,
16
- * unreachable when tickets are present; `--amend` requires tickets
17
- * carrying `op` fields and refuses the single shape.
18
- * 4. ticket validator + file-assumption gate + DAG + sizing + budget
19
- * (fan-out and amend; amend validates the MERGED set — existing
20
- * keeps + adds + modifies, closes excluded; all git-local, still
21
- * zero provider calls). Skipped entirely in single mode (no tickets
22
- * exist to validate).
23
- * 4.5. deterministic draft reachability (Epic #4474 PR6, design §4 —
24
- * the 8.4 critic demoted into persist): route-glob scan of the
25
- * draft set vs `planning.navigation.navRegistry`, mirroring the
26
- * `--paranoid` F7 healthcheck mechanics. Orphan surfaces are a
27
- * NAMED SOFT FAILURE (`code: PLAN_REACHABILITY_ORPHANS`, CLI exit
28
- * 3) raised before any provider call — the author appends the
29
- * single reachability Story in one targeted amend and re-runs the
30
- * persist once. Silent no-op when `planning.navigation` is
31
- * unconfigured; skip decisions are appended to the plan-metrics
32
- * ledger (`kind: critic-skip`) for audit.
33
- * 5. ideation fold — `renderEpicBody` / `openEpicFromOnePager` create
34
- * the Epic when the run starts from a one-pager (the first provider
35
- * call of the run). Amend additionally resolves every
36
- * modify/keep/close slug to its live issue and enforces the close-op
37
- * confirmation gate (exit 2 without `--explicit-delete`) BEFORE the
38
- * lease and before any mutation.
39
- * 6. Epic lease (KEEP — documented double-create at ~80 creations).
40
- * From here the lease is released on EVERY exit path (success, gate
41
- * failure, throw) via try/finally.
42
- * 7. managed Tech Spec / Acceptance Table sections + risk-verdict
43
- * structured comment + spec-freshness advisory.
44
- * 8. mode-split mutation:
45
- * - fan-out: story creation via the structural reconciler
46
- * (idempotent per-slug creation; the reconciler's state file is
47
- * the per-slug resume ledger), bracketed by checkpoint-v2 writes
48
- * so a rate-limit crash resumes losslessly with `--resume`;
49
- * - single: NO story tree — the `delivery::single` routing marker is
50
- * applied instead (inert until #4475 lands the deliver-side
51
- * reader), and `decompose = { ticketCount: 0, shape: "single" }`
52
- * is checkpointed so delivery-time consumers never misread absence
53
- * as unplanned;
54
- * - amend: op-mapped delta — close ops close, modify ops
55
- * close-and-recreate, add ops create, keep ops are untouched by
56
- * construction; the state ledger and blocked-by edges are rebuilt
57
- * over the merged set.
58
- * 9. inline post-plan healthcheck (the `agent::ready` exit condition,
59
- * Story #2921).
60
- * 10. single terminal `agent::ready` flip — the intermediate
61
- * `agent::review-spec` flip is retired on this surface (its readers
62
- * were visibility-only; the /deliver start gate needs only
63
- * `agent::ready`).
64
- * 11. checkpoint v2 + single `plan-summary` comment carrying the dry-run
65
- * wave table as closing text (replaces the Phase 9 dispatcher
66
- * round-trip and the Phase 12 notify). The single-mode summary
67
- * records `{ deliveryShape: "single", sliceCount, routingReasons }`.
68
- * 12. temp cleanup ONLY at terminal success — a failed run leaves
69
- * techspec/acceptance/risk-verdict/tickets artifacts on disk so a
70
- * `--force`/`--resume` re-persist reuses them (fixes the
71
- * `plan-phase-cleanup.js` mid-pipeline deletion defect).
7
+ * 1. Ticket validator + file-assumption + DAG + capacity + budget
8
+ * 2. Draft reachability (named soft failure, exit 3)
9
+ * 3. Split-policy partition (`assertAcceptancePartition`) + spec fold/spill
10
+ * 4. Create Story issues (`type::story` + sanitized authored labels —
11
+ * deliberately NOT `agent::ready`), resumably via a plan fingerprint
12
+ * 5. Upsert `story-plan-state` on every created Story; upsert `plan-summary`
13
+ * on the primary Story
14
+ * 6. Flip every Story to `agent::ready` the terminal step, so `ready`
15
+ * always implies "checkpoints written"
16
+ * 7. Comment on + close the superseded `--tickets` source issues
17
+ * (Story #4535) — bookkeeping only; never fails the run
18
+ * 8. Temp cleanup at terminal success + a stale-plan-dir reap
72
19
  *
73
- * Checkpoint v2: same `epic-plan-state` structured comment, `version: 2`,
74
- * with the `planningRisk` / `riskVerdict` / `reviewRouting` / `spec` /
75
- * `decompose` fields byte-compatible with v1 so the four delivery-time
76
- * consumers — `lib/orchestration/code-review.js` (review depth),
77
- * `epic-audit-prepare.js` (audit-lens routing),
78
- * `story-close/phases/locked-pipeline.js` (parent-risk inheritance), and
79
- * the decompose context reader — read it without modification. The only
80
- * additions are the `version` bump and the additive `persist` progress
81
- * block; consumers key on field presence, never on `version`.
20
+ * **Why `agent::ready` moved to the end (Story #4541).** Issues used to be
21
+ * born `agent::ready` in the creating POST while the checkpoints were
22
+ * written afterwards. Anything picking a Story up in that window — or after
23
+ * a comment failure aborted the loop read the checkpoint as `null`
24
+ * (`story-plan-state.js` degrades missing/malformed to `null`). Creating
25
+ * unlabelled, writing checkpoints, then flipping closes that race.
26
+ *
27
+ * **No authored risk artifact (Story #4542).** Persist neither requires nor
28
+ * accepts a risk verdict, derives no envelope from one, and computes no
29
+ * review routing. Review depth and the acceptance-critic mode are derived from
30
+ * the diff at close time (`review-depth.js#deriveChangeLevel`); `--force-review`
31
+ * is an explicit operator flag, recorded here as a receipt and never inferred.
32
+ *
33
+ * Hard cutover: no Epic parent, no reconciler, no `deliveryShape`, no
34
+ * `--amend` tree cascades. Those surfaces die with Stages 4–5 for any
35
+ * remaining epic-delivery readers.
82
36
  *
83
37
  * @module lib/orchestration/plan-persist/run-plan-persist
84
38
  */
85
39
 
86
- import { spawnSync as defaultSpawnSync } from 'node:child_process';
40
+ import { readdir, rm, stat } from 'node:fs/promises';
87
41
  import path from 'node:path';
88
42
 
89
- import { runPlanHealthcheck as defaultRunPlanHealthcheck } from '../../../epic-plan-healthcheck.js';
90
- import { verifyBddRunnerPendingTag } from '../../bdd-runner-detect.js';
43
+ import { anchorTempRoot, tempRootFrom } from '../../config/temp-paths.js';
91
44
  import { getLimits, PROJECT_ROOT } from '../../config-resolver.js';
92
- import { openEpicFromOnePager } from '../../epic-plan-ideation.js';
93
45
  import { gitSpawn } from '../../git-utils.js';
94
46
  import { Logger } from '../../Logger.js';
95
- import {
96
- AGENT_LABELS,
97
- DELIVERY_LABELS,
98
- TYPE_LABELS,
99
- } from '../../label-constants.js';
100
- import { cleanupPhaseTempFiles } from '../../plan-phase-cleanup.js';
101
- import { loadState, writeSpec, writeState } from '../../spec/index.js';
102
- import {
103
- reconcileSubIssueLinks,
104
- setBlockedByDependencies,
105
- setEpicLabel,
106
- warnTicketCapNearLimit,
107
- } from '../epic-plan-decompose/phases/creation.js';
108
- import {
109
- enforceFanOutGate,
110
- runHealthcheckGate,
111
- surfaceSoftConflictFindings,
112
- } from '../epic-plan-decompose/phases/persist.js';
113
- import {
114
- buildEpicSpecInput,
115
- validateTickets,
116
- } from '../epic-plan-decompose/phases/persist-helpers.js';
117
- import {
118
- RECONCILE_CLI,
119
- spawnReconcilerApply,
120
- } from '../epic-plan-decompose/phases/reconcile-spawn.js';
121
- import {
122
- acquireEpicPlanLease,
123
- assertNoOpenPlanChildren,
124
- releaseEpicPlanLease,
125
- } from '../epic-plan-lease-guard.js';
126
- import { planEpic } from '../epic-plan-spec/phases/plan-epic.js';
127
- import { runSpecFreshnessCheck } from '../epic-plan-spec/phases/spec-freshness.js';
128
- import {
129
- initialize as initializePlanState,
130
- read as readPlanState,
131
- write as writePlanState,
132
- } from '../epic-plan-state-store.js';
133
47
  import { evaluatePlanCritics } from '../plan-critics-evaluate.js';
134
48
  import {
135
49
  appendCriticSkip,
@@ -141,184 +55,299 @@ import {
141
55
  evaluateDraftReachability,
142
56
  renderReachabilityOrphans,
143
57
  } from '../plan-reachability.js';
144
- import { resolveReviewRouting } from '../plan-review-routing.js';
145
- import { deriveRiskEnvelope } from '../planning-risk.js';
146
- import { renderSpec } from '../spec-renderer.js';
147
- import {
148
- formatMissingSectionMessage,
149
- validateSpecSections,
150
- } from '../spec-section-validator.js';
151
58
  import { upsertStructuredComment } from '../ticketing.js';
152
59
  import {
153
- applyAmendOps,
154
- buildMergedTicketSet,
155
- enforceAmendCloseGate,
156
- partitionAmendTickets,
157
- renderAmendPlanDiff,
158
- resolveAmendTargets,
159
- } from './amend.js';
160
- import { countDeliverySlices, resolveDeliveryMode } from './delivery-mode.js';
60
+ enforceFanOutGate,
61
+ surfaceSoftConflictFindings,
62
+ } from './fan-out-gate.js';
63
+ import { resolveBaseBranchRef, validateTickets } from './persist-helpers.js';
64
+ import {
65
+ assemblePlanStories,
66
+ createStoryIssues,
67
+ markStoriesReady,
68
+ } from './story-ops.js';
161
69
  import {
162
70
  buildPlanSummaryCommentBody,
163
71
  buildWaveTable,
164
72
  PLAN_SUMMARY_COMMENT_TYPE,
165
73
  } from './summary.js';
74
+ import { closeSupersededTickets } from './supersede-ops.js';
166
75
 
167
- /** Checkpoint schema version written by this surface. */
168
- export const PLAN_CHECKPOINT_SCHEMA_VERSION_V2 = 2;
76
+ /** Checkpoint schema version written on each Story's story-plan-state. */
77
+ const PLAN_CHECKPOINT_SCHEMA_VERSION_V2 = 2;
169
78
 
170
- // Mode-coherence resolution (design §1 Step 3 item 3 + §2 mode matrix)
171
- // lives in `delivery-mode.js`; re-exported here so the CLI's stable public
172
- // API keeps a single import root for the persist surface.
173
- export { resolveDeliveryMode };
79
+ /** Structured-comment type for the per-plan Story checkpoint. */
80
+ const STORY_PLAN_STATE_TYPE = 'story-plan-state';
174
81
 
175
82
  /**
176
- * Merge-write the epic-plan-state checkpoint at schema v2. Reads the
177
- * current checkpoint (or initializes a fresh skeleton), shallow-merges
178
- * `patch`, and stamps `version: 2`. Field shapes for `planningRisk`,
179
- * `riskVerdict`, `reviewRouting`, `spec`, and `decompose` are byte-compatible
180
- * with v1 — v2 is additive only.
83
+ * Write the `story-plan-state` checkpoint on a Story.
181
84
  *
182
85
  * @param {object} provider
183
- * @param {number} epicId
184
- * @param {object} patch
185
- * @returns {Promise<object>} the written checkpoint payload
86
+ * @param {number} storyId
87
+ * @param {object} state
186
88
  */
187
- export async function writeCheckpointV2(provider, epicId, patch) {
188
- const current =
189
- (await readPlanState({ provider, epicId })) ??
190
- (await initializePlanState({ provider, epicId }));
191
- // One-level deep merge for object-valued blocks (`spec`, `decompose`,
192
- // `persist`, …) so a partial patch (e.g. `persist: { completedAt }`)
193
- // refines rather than replaces the block — same discipline the v1
194
- // writers applied by hand with `...currentState.decompose`.
195
- const merged = { ...current };
196
- for (const [key, value] of Object.entries(patch ?? {})) {
197
- const existing = merged[key];
198
- if (
199
- value !== null &&
200
- typeof value === 'object' &&
201
- !Array.isArray(value) &&
202
- existing !== null &&
203
- typeof existing === 'object' &&
204
- !Array.isArray(existing)
205
- ) {
206
- merged[key] = { ...existing, ...value };
207
- } else {
208
- merged[key] = value;
209
- }
89
+ export async function writeCheckpointV2(provider, storyId, state) {
90
+ if (!Number.isInteger(storyId)) {
91
+ throw new TypeError('writeCheckpointV2 requires a numeric storyId');
210
92
  }
211
- return writePlanState({
212
- provider,
213
- epicId,
214
- state: {
215
- ...merged,
216
- version: PLAN_CHECKPOINT_SCHEMA_VERSION_V2,
217
- },
218
- });
93
+ const body = [
94
+ '### story-plan-state',
95
+ '',
96
+ '```json',
97
+ JSON.stringify(
98
+ {
99
+ version: PLAN_CHECKPOINT_SCHEMA_VERSION_V2,
100
+ storyId,
101
+ ...state,
102
+ },
103
+ null,
104
+ 2,
105
+ ),
106
+ '```',
107
+ ].join('\n');
108
+ await upsertStructuredComment(provider, storyId, STORY_PLAN_STATE_TYPE, body);
109
+ return state;
219
110
  }
220
111
 
221
112
  /**
222
- * Resolve (or create) the Epic this persist run targets.
113
+ * Enforce the ticket validator's findings and report what the file-assumption
114
+ * gate actually concluded.
223
115
  *
224
- * Ideation mode (`onePagerContent` present): folds the former Phase 3/4
225
- * ideation steps in `openEpicFromOnePager` renders the Epic body from the
226
- * one-pager via the canonical template and opens the Issue with the
227
- * `type::epic` label. This is deliberately the FIRST provider call of the
228
- * run (after every deterministic gate), so a gate rejection never leaves an
229
- * orphaned Epic behind.
116
+ * The return value feeds the posted `plan-summary`'s freshness line, which
117
+ * used to be hard-coded `{ stale: 0, ambiguous: 0 }` — so the comment read
118
+ * "Spec freshness: clean" even on the one run where the gate had *given up*
119
+ * (Story #4541's unresolvable-base-ref downgrade). The summary asserted a
120
+ * clean result precisely when it had the least evidence for one.
230
121
  *
231
- * Existing-Epic mode: fetches and type-asserts the Epic.
122
+ * The mapping is deliberate. A confirmed mismatch is never reported here at
123
+ * all — it throws, and the run posts no summary. The only findings that
124
+ * survive to the summary are ones the gate could not *verify*, because the
125
+ * base ref they were computed against does not resolve; unverifiable is
126
+ * `ambiguous`, not `stale`.
232
127
  *
233
- * @returns {Promise<{ epicId: number, epic: object, created: boolean }>}
128
+ * @returns {{ stale: number, ambiguous: number }} Freshness counts for the
129
+ * posted summary.
234
130
  */
235
- async function resolveTargetEpic({
236
- epicId,
237
- onePagerContent,
238
- templateContent,
239
- provider,
131
+ function enforceTicketValidation(validated, { config, settings, cwd }) {
132
+ const validationErrors = validated.errors ?? [];
133
+ const assumptionFailures = validationErrors.filter((error) =>
134
+ error.startsWith('File assumption mismatch:'),
135
+ );
136
+ const blockingErrors = validationErrors.filter(
137
+ (error) => !error.startsWith('File assumption mismatch:'),
138
+ );
139
+ if (blockingErrors.length > 0) {
140
+ throw new Error(
141
+ `[plan-persist] ticket validation failed with ${blockingErrors.length} ` +
142
+ `hard error(s):\n${blockingErrors.map((error) => ` - ${error}`).join('\n')}`,
143
+ );
144
+ }
145
+ if (assumptionFailures.length === 0) return { stale: 0, ambiguous: 0 };
146
+ // Story #4541: resolve through the canonical `project.baseBranch` (the
147
+ // shape `config-resolver` actually emits) with the legacy settings bag as
148
+ // a fallback — reading a bare `config.baseBranch` meant this probe always
149
+ // targeted the literal `main`.
150
+ const gateBaseRef =
151
+ config?.project?.baseBranch ??
152
+ settings?.baseBranch ??
153
+ resolveBaseBranchRef(config);
154
+ const refResolves =
155
+ gitSpawn(
156
+ cwd ?? process.cwd(),
157
+ 'rev-parse',
158
+ '--verify',
159
+ '--quiet',
160
+ `${gateBaseRef}^{commit}`,
161
+ ).status === 0;
162
+ if (refResolves) {
163
+ throw new Error(
164
+ `[plan-persist] file-assumption gate: ${assumptionFailures.length} ` +
165
+ `mismatch(es):\n${assumptionFailures.map((error) => ` - ${error}`).join('\n')}`,
166
+ );
167
+ }
168
+ Logger.warn(
169
+ `[plan-persist] file-assumption gate skipped: base ref '${gateBaseRef}' ` +
170
+ `does not resolve — ${assumptionFailures.length} finding(s) downgraded.`,
171
+ );
172
+ return { stale: 0, ambiguous: assumptionFailures.length };
173
+ }
174
+
175
+ /**
176
+ * Run the supersede close phase behind a belt-and-braces guard.
177
+ *
178
+ * `closeSupersededTickets` already swallows per-ticket failures, but this
179
+ * phase runs *after* `createIssue`, so an unexpected throw anywhere in it
180
+ * would leave the run half-done with Stories already live. Degrade to a
181
+ * reported failure instead.
182
+ *
183
+ * @returns {Promise<import('./supersede-ops.js').SupersedeReport>}
184
+ */
185
+ async function runSupersedePhase(args) {
186
+ try {
187
+ return await closeSupersededTickets(args);
188
+ } catch (err) {
189
+ Logger.warn(
190
+ `[plan-persist] supersede close phase failed: ${err.message} — ` +
191
+ 'Stories were created; close the source tickets by hand.',
192
+ );
193
+ return {
194
+ enabled: true,
195
+ dryRun: args.dryRun === true,
196
+ reason: `phase-error: ${err.message}`,
197
+ closed: [],
198
+ planned: [],
199
+ skipped: [],
200
+ failed: (args.sourceTicketIds ?? []).map((ticket) => ({
201
+ ticket,
202
+ reason: err.message,
203
+ })),
204
+ };
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Render the plan-metrics line for the **posted** `plan-summary` comment,
210
+ * scoped to this invocation.
211
+ *
212
+ * The ordering hazard this closes: the ledger record for the current run is
213
+ * appended by `recordPlanInvocation`'s `finally`, which by construction only
214
+ * fires once `runPlanPersist` has *resolved* — long after this comment body
215
+ * is composed. A plain `since`-filtered ledger read therefore summarized
216
+ * every run except the one being summarized, and on an otherwise-quiet
217
+ * ledger it rendered "plan-metrics: no invocations recorded" onto the very
218
+ * comment reporting the run. (The stdout envelope was always correct: its
219
+ * `attachPlanMetrics` read runs after the wrapper returns.)
220
+ *
221
+ * **The `finally` stays where it is.** It is what guarantees a persist that
222
+ * throws is still recorded, so moving the append earlier — or moving this
223
+ * read later, past the GitHub writes it feeds — would trade a cosmetic bug
224
+ * for a real one. Instead of racing it, fold in a synthetic record standing
225
+ * for the in-flight invocation. It is the same record the wrapper is about to
226
+ * write, minus a final duration; it cannot double-count, because the real one
227
+ * does not exist yet at this point in the pipeline.
228
+ *
229
+ * `ok: true` is honest here: this line is only ever composed on the success
230
+ * path, after `createStoryIssues` has returned.
231
+ *
232
+ * @param {{ config: object, since: string, startedAt: string, mode: string }} args
233
+ * @returns {Promise<string|null>}
234
+ */
235
+ async function renderRunScopedPlanMetricsLine({
236
+ config,
237
+ since,
238
+ startedAt,
239
+ mode,
240
240
  }) {
241
- if (onePagerContent) {
242
- if (typeof provider.createIssue !== 'function') {
243
- throw new Error(
244
- '[plan-persist] provider does not expose createIssue; cannot open ' +
245
- 'an Epic from a one-pager.',
246
- );
247
- }
248
- const created = await openEpicFromOnePager({
249
- onePager: onePagerContent,
250
- template: templateContent,
251
- createIssue: (payload) => provider.createIssue(payload),
252
- });
253
- Logger.info(
254
- `[plan-persist] Opened Epic #${created.id} from one-pager ("${created.title}").`,
241
+ try {
242
+ const ledger = await readPlanMetrics(null, config);
243
+ const endedAt = new Date().toISOString();
244
+ const inFlight = {
245
+ v: 1,
246
+ cli: 'plan-persist',
247
+ mode,
248
+ epicId: null,
249
+ startedAt,
250
+ endedAt,
251
+ durationMs: Math.max(0, Date.parse(endedAt) - Date.parse(startedAt)) || 0,
252
+ ok: true,
253
+ };
254
+ const summary = summarizePlanMetrics(
255
+ { ...ledger, entries: [...(ledger.entries ?? []), inFlight] },
256
+ { since },
255
257
  );
256
- const epic = await provider.getEpic(created.id);
257
- if (!epic) {
258
- throw new Error(
259
- `[plan-persist] Epic #${created.id} was created but could not be re-fetched.`,
260
- );
261
- }
262
- return { epicId: created.id, epic, created: true };
258
+ return renderPlanMetricsSummaryLine(summary);
259
+ } catch (err) {
260
+ Logger.warn(`[plan-persist] plan-metrics summary skipped: ${err.message}`);
261
+ return null;
263
262
  }
263
+ }
264
+
265
+ /**
266
+ * Age after which an abandoned `temp/plan-*` directory is reaped. A plan run
267
+ * that is still being authored is minutes-to-hours old; a week is far past
268
+ * any live run and comfortably past an operator returning to a paused one.
269
+ */
270
+ const STALE_PLAN_DIR_MS = 7 * 24 * 60 * 60 * 1000;
264
271
 
265
- const epic = await provider.getEpic(epicId);
266
- if (!epic) {
267
- throw new Error(`[plan-persist] Epic #${epicId} not found.`);
272
+ /**
273
+ * Reap abandoned `plan-*` directories under the temp root (Story #4541).
274
+ *
275
+ * Terminal-success cleanup only ever removed the *current* run's `planDir`,
276
+ * so every plan that failed a gate, was abandoned mid-authoring, or ran
277
+ * `--dry-run` left its directory behind forever. This sweeps the stragglers
278
+ * on each persist.
279
+ *
280
+ * Best-effort throughout: this is hygiene, never a reason to fail a run that
281
+ * has already created Stories. The current run's own `planDir` is always
282
+ * excluded — its cleanup is the caller's decision.
283
+ *
284
+ * @param {{ config?: object, keepDir?: string|null, now?: number }} args
285
+ * @returns {Promise<{ reaped: string[] }>}
286
+ */
287
+ export async function reapStalePlanDirs({
288
+ config = {},
289
+ keepDir = null,
290
+ now = Date.now(),
291
+ } = {}) {
292
+ const reaped = [];
293
+ const tempRoot = anchorTempRoot(tempRootFrom(config));
294
+ let entries;
295
+ try {
296
+ entries = await readdir(tempRoot, { withFileTypes: true });
297
+ } catch {
298
+ return { reaped }; // No temp root yet — nothing to reap.
268
299
  }
269
- if (!epic.labels?.includes(TYPE_LABELS.EPIC)) {
270
- throw new Error(
271
- `[plan-persist] Ticket #${epicId} is not a ${TYPE_LABELS.EPIC}.`,
300
+ const keep = keepDir ? path.resolve(keepDir) : null;
301
+ for (const entry of entries) {
302
+ if (!entry.isDirectory() || !entry.name.startsWith('plan-')) continue;
303
+ const dir = path.resolve(tempRoot, entry.name);
304
+ if (keep !== null && dir === keep) continue;
305
+ try {
306
+ const { mtimeMs } = await stat(dir);
307
+ if (now - mtimeMs < STALE_PLAN_DIR_MS) continue;
308
+ await rm(dir, { recursive: true, force: true });
309
+ reaped.push(dir);
310
+ } catch {
311
+ // A racing writer or a permission error: leave it for the next run.
312
+ }
313
+ }
314
+ if (reaped.length > 0) {
315
+ Logger.info(
316
+ `[plan-persist] reaped ${reaped.length} abandoned plan director(ies) ` +
317
+ `older than 7d under ${tempRoot}.`,
272
318
  );
273
319
  }
274
- return { epicId, epic, created: false };
320
+ return { reaped };
275
321
  }
276
322
 
277
323
  /**
278
- * Execute the collapsed persist end to end (module doc has the 12-step
279
- * order). Modes: fan-out (default), single (`deliveryShape: "single"`),
280
- * amend (`--amend`).
324
+ * Execute the flat Story persist end to end.
281
325
  *
282
326
  * @param {{
283
- * epicId?: number|null,
284
- * provider: import('../../ITicketingProvider.js').ITicketingProvider,
327
+ * provider: object,
285
328
  * artifacts: {
286
- * techSpecContent: string,
287
- * acceptanceSpecContent?: string|null,
288
- * riskVerdict: import('../planning-risk.js').RiskVerdict,
289
- * tickets?: Array<object>|null,
290
- * onePagerContent?: string|null,
291
- * templateContent?: string|null,
329
+ * stories: Array<object>,
330
+ * techSpecContent?: string|null,
331
+ * planAcceptance?: string[]|null,
292
332
  * },
293
333
  * config?: object,
294
- * settings?: { baseBranch?: string, paths?: { tempRoot?: string } },
334
+ * settings?: object,
295
335
  * opts?: {
296
- * force?: boolean,
297
- * resume?: boolean,
298
- * amend?: boolean,
299
- * explicitDelete?: boolean,
300
- * steal?: boolean,
301
336
  * forceReview?: boolean,
302
337
  * allowOverBudget?: boolean,
303
338
  * allowLargeFanOut?: boolean,
304
- * // test seams (production callers must not set these)
305
- * skipHealthcheck?: boolean,
306
339
  * skipCleanup?: boolean,
307
- * spawnSync?: typeof defaultSpawnSync,
308
- * reconcileCli?: string,
309
- * writeSpecFn?: typeof writeSpec,
310
- * renderSpecFn?: typeof renderSpec,
311
- * loadStateFn?: typeof loadState,
312
- * writeStateFn?: typeof writeState,
313
- * runHealthcheckFn?: typeof defaultRunPlanHealthcheck,
314
- * bddProbeFn?: typeof verifyBddRunnerPendingTag,
315
- * fanOutCounter?: (arg: { path: string }) => number,
340
+ * dryRun?: boolean,
341
+ * planDir?: string,
342
+ * fanOutCounter?: Function,
316
343
  * cwd?: string,
344
+ * sourceTicketIds?: number[],
345
+ * sourceTicketOrigin?: 'flag'|'envelope'|'none',
346
+ * closeSuperseded?: boolean,
317
347
  * },
318
348
  * }} input
319
349
  */
320
350
  export async function runPlanPersist({
321
- epicId: requestedEpicId = null,
322
351
  provider,
323
352
  artifacts,
324
353
  config = {},
@@ -326,227 +355,92 @@ export async function runPlanPersist({
326
355
  opts = {},
327
356
  }) {
328
357
  const {
329
- techSpecContent,
330
- acceptanceSpecContent = null,
331
- riskVerdict,
332
- tickets = null,
333
- onePagerContent = null,
334
- templateContent = null,
358
+ stories: rawStories = null,
359
+ techSpecContent = null,
360
+ planAcceptance = null,
335
361
  } = artifacts ?? {};
336
362
  const {
337
- force = false,
338
- resume = false,
339
- amend = false,
340
- explicitDelete = false,
341
- steal = false,
342
363
  forceReview = false,
343
364
  allowOverBudget = false,
344
365
  allowLargeFanOut = false,
345
- skipHealthcheck = false,
346
366
  skipCleanup = false,
347
- spawnSync = defaultSpawnSync,
348
- reconcileCli = RECONCILE_CLI,
349
- writeSpecFn = writeSpec,
350
- renderSpecFn = renderSpec,
351
- loadStateFn = loadState,
352
- writeStateFn = writeState,
353
- runHealthcheckFn = defaultRunPlanHealthcheck,
354
- bddProbeFn = verifyBddRunnerPendingTag,
367
+ dryRun = false,
368
+ planDir = null,
355
369
  fanOutCounter = undefined,
356
370
  cwd = PROJECT_ROOT,
371
+ sourceTicketIds = [],
372
+ sourceTicketOrigin = 'none',
373
+ closeSuperseded = true,
357
374
  } = opts;
358
375
 
359
- // ---- Step 1: argument coherence (flag parsing itself is CLI-owned). ----
360
- if (force && resume) {
361
- throw new Error(
362
- '[plan-persist] --force and --resume are mutually exclusive.',
363
- );
364
- }
365
- if (amend && (force || resume)) {
366
- throw new Error(
367
- '[plan-persist] --amend is mutually exclusive with --force/--resume ' +
368
- '— the amend delta is already an incremental re-persist.',
369
- );
370
- }
371
- if (amend && onePagerContent) {
372
- throw new Error(
373
- '[plan-persist] --amend requires --epic <id> — there is no existing ' +
374
- 'plan to amend in ideation mode.',
375
- );
376
- }
377
- if (onePagerContent && resume) {
378
- throw new Error(
379
- '[plan-persist] --resume requires --epic <id> — the Epic already ' +
380
- "exists after the first attempt (its number is in the failed run's " +
381
- 'output); an ideation --resume would open a duplicate.',
382
- );
383
- }
384
- if (onePagerContent && !templateContent) {
385
- throw new Error(
386
- '[plan-persist] ideation mode requires the epic-from-idea template ' +
387
- 'content (templateContent).',
388
- );
389
- }
390
- if (!onePagerContent && !Number.isInteger(requestedEpicId)) {
391
- throw new Error(
392
- '[plan-persist] either --epic <id> or --one-pager <path> is required.',
393
- );
394
- }
376
+ // Boundary for the plan-metrics summary below: everything this invocation
377
+ // appends to the ledger is stamped at or after this instant, so filtering
378
+ // on it scopes the counts to *this* run rather than every plan ever run
379
+ // through the shared standalone ledger (Story #4541).
380
+ const runStartedAt = opts.metricsSince ?? new Date().toISOString();
395
381
 
396
- // ---- Step 2: section gate — BEFORE the lease, BEFORE any provider call.
397
- // A rejection here has made zero GitHub calls (locked in by the
398
- // fail-closed-ordering test).
399
- const sectionCheck = validateSpecSections({ body: techSpecContent });
400
- if (!sectionCheck.ok) {
382
+ if (!Array.isArray(rawStories) || rawStories.length === 0) {
401
383
  throw new Error(
402
- formatMissingSectionMessage({
403
- techspecPath: 'authored Tech Spec (--tech-spec)',
404
- missing: sectionCheck.missing,
405
- }),
384
+ '[plan-persist] stories payload must be a non-empty array ' +
385
+ '(--stories <file>). Default is one Story.',
406
386
  );
407
387
  }
408
388
 
409
- // ---- Step 3: risk-verdict presence (schema validation is CLI-owned via
410
- // loadRiskVerdict) + mode-coherence hard error. ----
411
- if (!riskVerdict || !Array.isArray(riskVerdict.axes)) {
389
+ const maxTickets = getLimits(config).maxTickets;
390
+ if (rawStories.length > maxTickets && !allowOverBudget) {
412
391
  throw new Error(
413
- '[plan-persist] risk verdict is required author risk-verdict.json ' +
414
- 'and pass it with --risk-verdict.',
392
+ `[plan-persist] Stories (${rawStories.length}) exceed the reviewability ` +
393
+ `budget (${maxTickets}). Re-scope, or rerun with --allow-over-budget.`,
415
394
  );
416
395
  }
417
- const mode = resolveDeliveryMode(riskVerdict, tickets, { amend });
418
-
419
- // ---- Step 4: ticket validator + file-assumption gate + DAG + sizing +
420
- // budget (fan-out and amend; git-local — still no provider call). In
421
- // amend mode every gate runs over the MERGED set — keeps + adds +
422
- // modifies, closes excluded — so the DAG is validated against the tree
423
- // that will actually exist post-amend. The skip branch below is fenced
424
- // by construction: `resolveDeliveryMode` hard-refuses `deliveryShape:
425
- // "single"` with any tickets payload, so single mode can only reach here
426
- // with no tickets to validate. ----
427
- let validated = null;
428
- let amendPartition = null;
429
- let reachability = null;
430
- let gateSet = null;
431
- if (mode !== 'single') {
432
- amendPartition = mode === 'amend' ? partitionAmendTickets(tickets) : null;
433
- gateSet = mode === 'amend' ? buildMergedTicketSet(tickets) : tickets;
434
- const maxTickets = getLimits(config).maxTickets;
435
- if (gateSet.length > maxTickets && !allowOverBudget) {
436
- throw new Error(
437
- `[plan-persist] Tickets (${gateSet.length}) exceed the reviewability ` +
438
- `budget (${maxTickets}). Re-scope the Epic into a smaller plan, or ` +
439
- 'rerun with --allow-over-budget after confirming the over-budget ' +
440
- 'rationale on the Epic.',
441
- );
442
- }
443
- warnTicketCapNearLimit(gateSet, maxTickets, 'plan-persist');
444
- if (gateSet.length > maxTickets && allowOverBudget) {
445
- Logger.warn(
446
- `[plan-persist] Persisting an over-budget decomposition: ${gateSet.length} ` +
447
- `tickets vs. budget ${maxTickets} (operator override --allow-over-budget).`,
448
- );
449
- }
450
- Logger.info(
451
- `[plan-persist] Running cross-validation on ${gateSet.length} tickets` +
452
- `${mode === 'amend' ? ' (merged amend set)' : ''}...`,
396
+ if (rawStories.length > maxTickets && allowOverBudget) {
397
+ Logger.warn(
398
+ `[plan-persist] Persisting an over-budget plan: ${rawStories.length} ` +
399
+ `Stories vs. budget ${maxTickets} (--allow-over-budget).`,
453
400
  );
454
- validated = validateTickets(gateSet, config, { fanOutCounter, cwd });
455
- enforceFanOutGate(validated.findings, allowLargeFanOut, 'plan-persist');
456
- surfaceSoftConflictFindings(validated.findings, 'plan-persist');
401
+ }
457
402
 
458
- // File-assumption gate (#4474 PR7 — coverage regression fix). The
459
- // validator batches per-Story `{ path, assumption }` mismatches against
460
- // the base branch onto `validated.errors`; the retired 12-phase flow
461
- // gated that channel in the workflow's re-prompt loop, so the collapsed
462
- // CLI must gate it here or the check is silently advisory. Fan-out and
463
- // shared-editor findings keep their own policy channels above — this
464
- // rejects only the deterministic assumption mismatches, still before
465
- // any provider call. Guarded on ref resolvability: in a checkout where
466
- // the base branch ref does not resolve (shallow CI fetches, detached
467
- // test sandboxes) every path probes "absent" and the findings are
468
- // noise, so they downgrade to warnings instead of hard-failing.
469
- const assumptionFailures = (validated.errors ?? []).filter((e) =>
470
- e.startsWith('File assumption mismatch:'),
471
- );
472
- if (assumptionFailures.length > 0) {
473
- const gateBaseRef = config?.baseBranch ?? 'main';
474
- const refResolves =
475
- gitSpawn(
476
- cwd ?? process.cwd(),
477
- 'rev-parse',
478
- '--verify',
479
- '--quiet',
480
- `${gateBaseRef}^{commit}`,
481
- ).status === 0;
482
- if (refResolves) {
483
- throw new Error(
484
- `[plan-persist] file-assumption gate: ${assumptionFailures.length} ` +
485
- `mismatch(es) between declared assumptions and the base branch:\n` +
486
- `${assumptionFailures.map((e) => ` - ${e}`).join('\n')}\n` +
487
- 'Fix the Story change declarations (or the plan) and re-run the persist.',
488
- );
489
- }
490
- Logger.warn(
491
- `[plan-persist] file-assumption gate skipped: base ref '${gateBaseRef}' ` +
492
- `does not resolve in this checkout — ${assumptionFailures.length} ` +
493
- 'finding(s) downgraded to warnings.',
494
- );
495
- for (const e of assumptionFailures) {
496
- Logger.warn(`[plan-persist] ${e}`);
497
- }
498
- }
403
+ Logger.info(
404
+ `[plan-persist] Running cross-validation on ${rawStories.length} Story ticket(s)...`,
405
+ );
406
+ const validated = validateTickets(rawStories, config, {
407
+ fanOutCounter,
408
+ cwd,
409
+ modelCapacity: opts.modelCapacity,
410
+ });
411
+ enforceFanOutGate(validated.findings, allowLargeFanOut, 'plan-persist');
412
+ surfaceSoftConflictFindings(validated.findings, 'plan-persist');
413
+ const freshness = enforceTicketValidation(validated, {
414
+ config,
415
+ settings,
416
+ cwd,
417
+ });
499
418
 
500
- // ---- Step 4.5: deterministic draft reachability (#4474 PR6 — the 8.4
501
- // critic demoted into persist). Still git-local, zero provider calls,
502
- // so the one-targeted-amend recovery re-runs a clean persist. ----
503
- reachability = evaluateDraftReachability({ tickets: gateSet, config });
504
- if (reachability.status === 'orphans') {
505
- const err = new Error(renderReachabilityOrphans(reachability));
506
- err.code = 'PLAN_REACHABILITY_ORPHANS';
507
- err.orphans = reachability.orphans;
508
- throw err;
509
- }
510
- Logger.info(`[plan-persist] reachability: ${reachability.reasons[0]}`);
511
- } else {
512
- reachability = {
513
- status: 'skipped',
514
- reasons: [
515
- 'single-delivery shape — no draft story tree to scan for orphan surfaces.',
516
- ],
517
- orphans: [],
518
- scanned: 0,
519
- };
419
+ const reachability = evaluateDraftReachability({
420
+ tickets: rawStories,
421
+ config,
422
+ });
423
+ if (reachability.status === 'orphans') {
424
+ const err = new Error(renderReachabilityOrphans(reachability));
425
+ err.code = 'PLAN_REACHABILITY_ORPHANS';
426
+ err.orphans = reachability.orphans;
427
+ throw err;
520
428
  }
429
+ Logger.info(`[plan-persist] reachability: ${reachability.reasons[0]}`);
521
430
  if (reachability.status === 'skipped') {
522
- // Audit trail for the skip decision (#4474 PR6). Best-effort by
523
- // contract — a failed append never fails the persist. Logged before
524
- // the first provider call so even an ideation run that later fails
525
- // still records the decision (ideation has no Epic id yet, so the
526
- // record lands on the standalone stream).
527
431
  await appendCriticSkip(
528
432
  {
529
433
  critic: 'reachability',
530
434
  reasons: reachability.reasons,
531
435
  cli: 'plan-persist',
532
- epicId: requestedEpicId,
533
436
  },
534
437
  config,
535
438
  );
536
439
  }
537
440
 
538
- // ---- Step 4.7: folded critic dispatch evaluation (#4496 fix 6 — the
539
- // former standalone `plan-critics.js` turn). Deterministic and git-local,
540
- // still zero provider calls: the verdicts are printed (and returned on
541
- // the result) as part of the pre-write phase, and every skip decision is
542
- // appended to the plan-metrics ledger exactly as the standalone CLI did,
543
- // so under-firing stays auditable without a separate invocation on the
544
- // headless path. Advisory by construction — the deterministic validators
545
- // above remain the unchanged hard gates. ----
546
441
  const critics = evaluatePlanCritics({
547
- techSpecContent,
548
- riskVerdict,
549
- tickets: gateSet,
442
+ techSpecContent: techSpecContent ?? '',
443
+ tickets: rawStories,
550
444
  config,
551
445
  });
552
446
  for (const decision of [critics.consolidation, critics.premortem]) {
@@ -556,460 +450,140 @@ export async function runPlanPersist({
556
450
  decision.reasons.join('; '),
557
451
  );
558
452
  if (!decision.dispatch) {
559
- // Best-effort by contract — a failed append never fails the persist.
560
453
  await appendCriticSkip(
561
454
  {
562
455
  critic: decision.critic,
563
456
  reasons: decision.reasons,
564
457
  cli: 'plan-persist',
565
- epicId: requestedEpicId,
566
458
  },
567
459
  config,
568
460
  );
569
461
  }
570
462
  }
571
463
 
572
- // ---- Step 5: ideation fold / Epic resolution (first provider call). ----
573
- const { epicId, epic, created } = await resolveTargetEpic({
574
- epicId: requestedEpicId,
575
- onePagerContent,
576
- templateContent,
577
- provider,
464
+ // Split policy + inline Spec fold (over-budget Specs fail closed no docs/).
465
+ const { stories } = assemblePlanStories(rawStories, {
466
+ sharedSpec: techSpecContent,
467
+ planAcceptance: planAcceptance ?? undefined,
468
+ sourceTicketIds,
578
469
  });
579
470
 
580
- // Amend pre-mutation resolution: every modify/keep/close slug must
581
- // resolve to a live issue, and close ops require --explicit-delete
582
- // (exit 2 with the dry-run diff otherwise — the epic-reconcile.js
583
- // contract). Runs BEFORE the lease and before any mutation.
584
- let amendTargets = null;
585
- if (mode === 'amend') {
586
- const priorState = loadStateFn(epicId);
587
- amendTargets = await resolveAmendTargets({
588
- partition: amendPartition,
589
- stateMapping: priorState.mapping,
590
- provider,
591
- });
592
- enforceAmendCloseGate({
593
- epicId,
594
- targets: amendTargets,
595
- adds: amendPartition.add,
596
- explicitDelete,
597
- });
598
- Logger.info(
599
- renderAmendPlanDiff({
600
- epicId,
601
- targets: amendTargets,
602
- adds: amendPartition.add,
603
- }),
604
- );
605
- }
606
-
607
- // ---- Step 6: Epic lease. Every path after a successful acquire runs
608
- // through the finally below, so the lease is released on success, on a
609
- // gate failure, and on a throw alike. ----
610
- await acquireEpicPlanLease({ provider, epicId, config, steal });
611
-
612
- try {
613
- // Refuse a duplicate story tree unless this is a deliberate re-persist
614
- // (`--force` closes + recreates via the reconciler's close ops;
615
- // `--resume` continues a partial persist). Amend is exempt by
616
- // definition — its whole purpose is mutating the existing open tree.
617
- if (mode !== 'amend') {
618
- await assertNoOpenPlanChildren({
619
- provider,
620
- epicId,
621
- force: force || resume,
622
- });
623
- }
624
-
625
- await initializePlanState({ provider, epicId });
626
-
627
- // ---- Step 7: managed sections + risk comment + freshness advisory. ----
628
- // BDD-runner probe (Story #4145): best-effort; a probe failure degrades
629
- // to "runner present" and never blocks the persist.
630
- let bddRunner = null;
631
- try {
632
- bddRunner = await bddProbeFn({ cwd: PROJECT_ROOT });
633
- } catch (err) {
634
- Logger.warn(
635
- `[plan-persist] BDD runner probe skipped (${err.message}); ` +
636
- 'acceptance disposition derived from risk axes only.',
637
- );
638
- }
639
- const planningRisk = deriveRiskEnvelope(riskVerdict, { bddRunner });
640
- if (planningRisk.acceptanceWaivedReason) {
641
- Logger.info(
642
- `[plan-persist] Acceptance disposition forced to not-applicable for ` +
643
- `Epic #${epicId}: ${planningRisk.acceptanceWaivedReason}`,
644
- );
645
- }
646
-
647
- // Amend always overwrites the managed sections — the amended Tech Spec
648
- // IS the delta's spec half (planEpic would otherwise short-circuit
649
- // `already-planned` on the pre-amend sections).
650
- const planResult = await planEpic(
651
- epicId,
652
- provider,
653
- { techSpecContent, acceptanceSpecContent },
654
- settings,
655
- { force: force || mode === 'amend', planningRisk },
656
- );
657
-
658
- const reviewRouting = resolveReviewRouting({ planningRisk, forceReview });
659
- Logger.info(`[plan-persist] Review routing: ${reviewRouting.decision}.`);
660
-
661
- await upsertStructuredComment(
662
- provider,
663
- epicId,
664
- 'risk-verdict',
665
- buildRiskVerdictCommentBody({ epicId, riskVerdict, planningRisk }),
666
- );
667
-
668
- const baseBranchRef = settings?.baseBranch ?? 'main';
669
- const tempRoot = path.resolve(
670
- PROJECT_ROOT,
671
- settings?.paths?.tempRoot ?? 'temp',
672
- );
673
- const freshness = await runSpecFreshnessCheck({
674
- epicId,
675
- techSpecContent,
676
- baseBranchRef,
677
- tempRoot,
678
- provider,
679
- });
680
-
681
- // Spec-half checkpoint (v2). A crash after this point resumes with the
682
- // sections already folded (planEpic short-circuits `already-planned`).
683
- await writeCheckpointV2(provider, epicId, {
684
- planningRisk,
685
- riskVerdict,
686
- reviewRouting: {
687
- decision: reviewRouting.decision,
688
- requiresStop: reviewRouting.requiresStop,
689
- forceReviewApplied: reviewRouting.forceReviewApplied,
690
- },
691
- spec: {
692
- techSpecPersisted:
693
- planResult?.techSpecPersisted === true ||
694
- planResult?.reason === 'already-planned',
695
- acceptanceTable: planResult?.acceptanceTable ?? 'none',
696
- completedAt: new Date().toISOString(),
697
- },
698
- persist: {
699
- mode,
700
- cli: 'plan-persist',
701
- startedAt: new Date().toISOString(),
702
- completedAt: null,
703
- },
704
- });
705
-
706
- // ---- Step 8: mode-split mutation. ----
707
- let reconcile = null;
708
- let specFilePath = null;
709
- let ticketCount = 0;
710
- let single = null;
711
- let amendSummary = null;
712
-
713
- if (mode === 'fan-out') {
714
- // Story creation via the structural reconciler.
715
- Logger.info(
716
- `[plan-persist] Rendering spec for Epic #${epicId} (${validated.length} tickets)...`,
717
- );
718
- const spec = renderSpecFn(validated, {
719
- epic: buildEpicSpecInput(epic, epicId),
720
- });
721
- specFilePath = writeSpecFn(epicId, spec, { epicsDir: undefined });
722
- Logger.info(`[plan-persist] Wrote spec → ${specFilePath}`);
723
-
724
- // Pre-creation checkpoint: marks creation in flight so a rate-limit
725
- // crash mid-creation leaves a checkpoint pointing at the spec + the
726
- // reconciler's per-slug state file (the resume ledger). `--resume`
727
- // re-runs the reconciler, which creates only the missing slugs.
728
- await writeCheckpointV2(provider, epicId, {
729
- decompose: { ticketCount: null, completedAt: null },
730
- });
731
-
732
- Logger.info(
733
- `[plan-persist] Spawning epic-reconcile.js --apply --yes for Epic #${epicId}...`,
734
- );
735
- reconcile = spawnReconcilerApply({
736
- spawnSync,
737
- reconcileCli,
738
- epicId,
739
- cwd,
740
- explicitDelete: force,
741
- });
742
-
743
- await reconcileSubIssueLinks(epicId, provider);
744
-
745
- const postReconcileState = loadStateFn(epicId);
746
- await setBlockedByDependencies(
747
- epicId,
748
- provider,
749
- spec,
750
- postReconcileState.mapping,
751
- );
752
-
753
- ticketCount = tickets.length;
754
- // Post-creation checkpoint (the former recordCheckpoint half).
755
- await writeCheckpointV2(provider, epicId, {
756
- decompose: {
757
- ticketCount,
758
- shape: 'fan-out',
759
- completedAt: new Date().toISOString(),
760
- },
761
- });
762
-
763
- // A force re-persist over a former single-delivery plan flips the
764
- // routing shape — drop the stale marker so #4475's reader never sees
765
- // a fan-out tree labelled single.
766
- await removeSingleDeliveryMarker(provider, epicId, epic);
767
- } else if (mode === 'single') {
768
- // Single-delivery: NO story tree. The delivery::single routing
769
- // marker (inert until #4475's deliver-side reader) plus the Delivery
770
- // Slicing table of the persisted Tech Spec are the plan.
771
- single = {
772
- deliveryShape: 'single',
773
- sliceCount: countDeliverySlices(techSpecContent),
774
- routingReasons: riskVerdict.deliveryShapeRationale
775
- ? [riskVerdict.deliveryShapeRationale]
776
- : [],
777
- };
778
- Logger.info(
779
- `[plan-persist] Single-delivery mode: applying ${DELIVERY_LABELS.SINGLE} ` +
780
- `to Epic #${epicId} (no story tree).`,
781
- );
782
- await provider.updateTicket(epicId, {
783
- labels: { add: [DELIVERY_LABELS.SINGLE], remove: [] },
784
- });
785
- // Explicit zero-ticket checkpoint so delivery-time consumers read a
786
- // deliberate single-shape plan, never an unplanned absence.
787
- await writeCheckpointV2(provider, epicId, {
788
- decompose: {
789
- ticketCount: 0,
790
- shape: 'single',
791
- completedAt: new Date().toISOString(),
792
- },
793
- });
794
- } else {
795
- // Amend delta: close-and-recreate is scoped to modify/close slugs
796
- // only; keeps are untouched by construction (no code path receives
797
- // them); adds are created fresh. The state ledger and blocked-by
798
- // edges are rebuilt over the merged set.
799
- const spec = renderSpecFn(validated, {
800
- epic: buildEpicSpecInput(epic, epicId),
801
- });
802
- specFilePath = writeSpecFn(epicId, spec, { epicsDir: undefined });
803
- Logger.info(`[plan-persist] Wrote amended spec → ${specFilePath}`);
804
-
805
- await writeCheckpointV2(provider, epicId, {
806
- decompose: { ticketCount: null, completedAt: null },
807
- });
471
+ const { created } = await createStoryIssues({
472
+ provider,
473
+ stories,
474
+ opts: { dryRun },
475
+ });
808
476
 
809
- const validatedBySlug = new Map(validated.map((t) => [t.slug, t]));
810
- const applied = await applyAmendOps({
811
- epicId,
812
- provider,
813
- targets: amendTargets,
814
- validatedBySlug,
815
- });
477
+ const primary = created[0];
478
+ const waveTable = buildWaveTable(
479
+ stories.map((s) => ({
480
+ slug: s.slug,
481
+ title: s.title,
482
+ depends_on: s.depends_on,
483
+ })),
484
+ );
816
485
 
817
- // Rebuild the state ledger over the merged set: prior mapping minus
818
- // closed/replaced slugs, plus the fresh create/recreate numbers.
819
- const priorState = loadStateFn(epicId);
820
- const mergedMapping = { ...(priorState.mapping ?? {}) };
821
- for (const slug of applied.closedSlugs) delete mergedMapping[slug];
822
- Object.assign(mergedMapping, applied.mapping);
823
- writeStateFn(epicId, {
824
- epicId,
825
- mapping: mergedMapping,
826
- lastReconciledAt: new Date().toISOString(),
827
- });
486
+ // Story #4541: `readPlanMetrics` is declared `(epicId, config)` but was
487
+ // called with `config` first, so the ledger path resolver received the
488
+ // config object as an `epicId` and threw its guard on every single run —
489
+ // a throw this try/catch then swallowed into a silently absent summary.
490
+ // v2 persist is always Epic-less, hence the explicit `null`. The `since`
491
+ // filter keeps the counts about this invocation, and the in-flight record
492
+ // is folded in because the wrapper has not written it yet — see
493
+ // `renderRunScopedPlanMetricsLine`.
494
+ const planMetricsLine = await renderRunScopedPlanMetricsLine({
495
+ config,
496
+ since: runStartedAt,
497
+ startedAt: runStartedAt,
498
+ mode: dryRun ? 'dry-run' : 'persist',
499
+ });
828
500
 
829
- await reconcileSubIssueLinks(epicId, provider);
830
- await setBlockedByDependencies(epicId, provider, spec, mergedMapping);
501
+ const summaryBody = buildPlanSummaryCommentBody({
502
+ epicId: primary.id,
503
+ ticketCount: created.length,
504
+ forceReview,
505
+ freshness,
506
+ healthcheck: { skipped: true },
507
+ waveTable,
508
+ mode: 'stories',
509
+ planMetricsLine,
510
+ stories: created,
511
+ });
831
512
 
832
- ticketCount = validated.length;
833
- amendSummary = {
834
- closed: applied.closed,
835
- recreated: applied.recreated,
836
- created: applied.created,
837
- keptCount: amendTargets.keep.length,
838
- };
839
- await writeCheckpointV2(provider, epicId, {
840
- decompose: {
841
- ticketCount,
842
- shape: 'fan-out',
513
+ if (!dryRun) {
514
+ for (const story of created) {
515
+ await writeCheckpointV2(provider, story.id, {
516
+ persist: {
843
517
  completedAt: new Date().toISOString(),
518
+ storyCount: created.length,
519
+ primaryStoryId: primary.id,
520
+ stories: created.map((createdStory) => ({
521
+ slug: createdStory.slug,
522
+ id: createdStory.id,
523
+ })),
844
524
  },
845
525
  });
846
-
847
- // An amended plan is fan-out-shaped; drop a stale single marker.
848
- await removeSingleDeliveryMarker(provider, epicId, epic);
849
526
  }
527
+ await upsertStructuredComment(
528
+ provider,
529
+ primary.id,
530
+ PLAN_SUMMARY_COMMENT_TYPE,
531
+ summaryBody,
532
+ );
850
533
 
851
- // ---- Step 9: inline healthcheck the agent::ready exit condition. ----
852
- const healthcheck = skipHealthcheck
853
- ? { ok: true, skipped: true }
854
- : await runHealthcheckGate({
855
- epicId,
856
- epic,
857
- runHealthcheckFn,
858
- tag: 'plan-persist',
859
- });
534
+ // Terminal step: every checkpoint above is now on every Story, so
535
+ // `agent::ready` can honestly mean "fully persisted" (Story #4541).
536
+ // Anything that picks a Story up from here reads a real checkpoint.
537
+ await markStoriesReady({ provider, created });
538
+ }
860
539
 
861
- // ---- Step 10: single terminal agent::ready flip. This surface never
862
- // writes agent::review-spec — the HITL review gate sits BEFORE persist
863
- // in the collapsed flow, so the intermediate label has no reader. ----
864
- Logger.info(
865
- `[plan-persist] Flipping Epic #${epicId} to ${AGENT_LABELS.READY}...`,
866
- );
867
- await setEpicLabel(provider, epicId, AGENT_LABELS.READY);
540
+ const supersede = await runSupersedePhase({
541
+ provider,
542
+ stories,
543
+ created,
544
+ sourceTicketIds,
545
+ dryRun,
546
+ closeSuperseded,
547
+ });
548
+ // Record which channel the ids came from so a run that superseded nothing
549
+ // says *why* (`none` = neither the envelope nor --source-tickets carried
550
+ // any) rather than reading as a clean no-op — Story #4554.
551
+ supersede.sourceTicketOrigin = sourceTicketOrigin;
868
552
 
869
- // ---- Step 11: final checkpoint v2 + single plan-summary comment with
870
- // the dry-run wave table as closing text (single mode records the
871
- // { deliveryShape, sliceCount, routingReasons } routing record
872
- // instead). ----
873
- const waveTable = mode === 'single' ? [] : buildWaveTable(validated);
874
- const checkpoint = await writeCheckpointV2(provider, epicId, {
875
- persist: { completedAt: new Date().toISOString() },
876
- });
877
- // G2 measurement receipt (Epic #4474 PR7): roll the plan-metrics ledger
878
- // into the summary comment so turns-per-plan / per-mode counts / critic
879
- // skips are readable off the Epic. Best-effort — a missing ledger
880
- // yields no line. The in-flight persist invocation itself is stamped by
881
- // the CLI wrapper *after* this function returns, so it appears in the
882
- // stdout JSON (and any later re-persist), not in this comment.
883
- let planMetricsLine = null;
553
+ if (!skipCleanup && planDir) {
884
554
  try {
885
- const metricsSummary = summarizePlanMetrics(
886
- await readPlanMetrics(epicId, config),
887
- );
888
- if (metricsSummary) {
889
- planMetricsLine = renderPlanMetricsSummaryLine(metricsSummary);
890
- }
555
+ await rm(planDir, { recursive: true, force: true });
891
556
  } catch (err) {
892
- Logger.warn(
893
- `[plan-persist] plan-metrics summary line skipped: ${err.message}`,
894
- );
557
+ Logger.warn(`[plan-persist] temp cleanup skipped: ${err.message}`);
895
558
  }
896
- await upsertStructuredComment(
897
- provider,
898
- epicId,
899
- PLAN_SUMMARY_COMMENT_TYPE,
900
- buildPlanSummaryCommentBody({
901
- epicId,
902
- ticketCount,
903
- planningRisk,
904
- reviewRouting,
905
- freshness,
906
- healthcheck,
907
- waveTable,
908
- mode,
909
- planMetricsLine,
910
- single,
911
- amend: amendSummary,
912
- }),
913
- );
559
+ }
560
+ // Terminal-success cleanup only ever removes *this* run's planDir, so
561
+ // abandoned ones accumulated forever. Sweep them (Story #4541).
562
+ await reapStalePlanDirs({ config, keepDir: skipCleanup ? planDir : null });
914
563
 
915
- // ---- Step 12: temp cleanup ONLY at terminal success. A failed run
916
- // leaves every authored artifact on disk for --force/--resume reuse. ----
917
- const cleanup = skipCleanup
918
- ? { deleted: [], missing: [], failed: [], skipped: true }
919
- : await cleanupPhaseTempFiles({ phase: 'persist', epicId });
564
+ const adopted = created.filter((story) => story.adopted);
565
+ if (adopted.length > 0) {
920
566
  Logger.info(
921
- `[plan-persist] Persist complete for Epic #${epicId} (${mode}). ` +
922
- `${ticketCount} ticket(s) persisted; Epic is ${AGENT_LABELS.READY}.`,
567
+ `[plan-persist] resumed ${adopted.length} of ${created.length} Story(ies) ` +
568
+ `from a previous persist: ${adopted.map((s2) => `#${s2.id}`).join(', ')}.`,
923
569
  );
924
- if (cleanup.deleted.length > 0) {
925
- Logger.info(
926
- `[plan-persist] 🧹 Cleaned up ${cleanup.deleted.length} temp file(s).`,
927
- );
928
- }
929
-
930
- return {
931
- epicId,
932
- epicCreated: created,
933
- mode,
934
- ticketCount,
935
- checkpoint,
936
- planningRisk,
937
- reviewRouting,
938
- freshness,
939
- healthcheck,
940
- reachability,
941
- critics,
942
- reconcile,
943
- specPath: specFilePath,
944
- waveTable,
945
- single,
946
- amend: amendSummary,
947
- cleanup,
948
- labelTransition: 'ready',
949
- };
950
- } finally {
951
- // Lease release on EVERY exit path (success, gate failure, throw).
952
- // Best-effort by contract — releaseEpicPlanLease never throws.
953
- await releaseEpicPlanLease({ provider, epicId, config });
954
570
  }
955
- }
956
-
957
- /**
958
- * Drop a stale `delivery::single` marker when a fan-out-shaped persist
959
- * (full re-persist or amend) lands over a formerly single-delivery plan.
960
- * No-op — and no API call — when the fetched Epic never carried it.
961
- *
962
- * @param {object} provider
963
- * @param {number} epicId
964
- * @param {{ labels?: string[] }} epic the Epic as fetched at step 5
965
- */
966
- async function removeSingleDeliveryMarker(provider, epicId, epic) {
967
- if (!epic?.labels?.includes(DELIVERY_LABELS.SINGLE)) return;
968
571
  Logger.info(
969
- `[plan-persist] Removing stale ${DELIVERY_LABELS.SINGLE} marker from ` +
970
- `Epic #${epicId} (plan is fan-out-shaped now).`,
572
+ `[plan-persist] Persisted ${created.length} Story(ies)` +
573
+ `; primary #${primary.id} is agent::ready.`,
971
574
  );
972
- await provider.updateTicket(epicId, {
973
- labels: { add: [], remove: [DELIVERY_LABELS.SINGLE] },
974
- });
975
- }
976
-
977
- /**
978
- * Render the `risk-verdict` structured-comment body. Lifted from
979
- * `epic-plan-spec/phases/run-spec-phase.js` so the collapsed surface posts
980
- * the byte-identical audit-trail comment (axis table + fenced-JSON record)
981
- * downstream tooling parses.
982
- *
983
- * @param {{ epicId: number, riskVerdict: import('../planning-risk.js').RiskVerdict, planningRisk: import('../planning-risk.js').PlanningRiskEnvelope }} input
984
- * @returns {string}
985
- */
986
- function buildRiskVerdictCommentBody({ epicId, riskVerdict, planningRisk }) {
987
- const axisRows = planningRisk.axes.map(
988
- (entry) => `| ${entry.axis} | ${entry.level} | ${entry.rationale} |`,
575
+ Logger.info(
576
+ `[plan-persist] Deliver with: /deliver ${created.map((s2) => s2.id).join(' ')}`,
989
577
  );
990
- const axisTable =
991
- axisRows.length > 0
992
- ? ['| Axis | Level | Rationale |', '| --- | --- | --- |', ...axisRows]
993
- : ['_No risk axes apply (planner-asserted)._'];
994
- const record = {
995
- kind: 'risk-verdict',
996
- epicId,
997
- verdict: riskVerdict,
998
- planningRisk,
578
+
579
+ return {
580
+ stories: created,
581
+ primaryStoryId: primary.id,
582
+ forceReview,
583
+ critics,
584
+ reachability,
585
+ freshness,
586
+ waveTable,
587
+ supersede,
999
588
  };
1000
- const waiverNote = planningRisk.acceptanceWaivedReason
1001
- ? ['', `> ⚠️ **Acceptance waived** — ${planningRisk.acceptanceWaivedReason}`]
1002
- : [];
1003
- return [
1004
- `### 🧭 Planning Risk Verdict — ${planningRisk.overallLevel} · ${planningRisk.gateDecision}`,
1005
- '',
1006
- riskVerdict.summary,
1007
- '',
1008
- ...axisTable,
1009
- ...waiverNote,
1010
- '',
1011
- '```json',
1012
- JSON.stringify(record, null, 2),
1013
- '```',
1014
- ].join('\n');
1015
589
  }