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
@@ -0,0 +1,784 @@
1
+ /**
2
+ * run-epilogue.js — real per-run closeout for `/deliver --run`.
3
+ *
4
+ * After the last Story in a multi-Story plan-run lands, this module:
5
+ * 1. Selects the cross-Story audit lens roster over the combined landed
6
+ * tip vs base (deterministic `selectAudits` — host walks lenses).
7
+ * 2. Rolls up friction follow-ups across every Story in the run and
8
+ * files/posts them on the primary Story.
9
+ * 3. Checks sibling Spec/acceptance coherence across Story bodies.
10
+ *
11
+ * There is no inert planner-only path: `planRunEpilogue` enumerates steps
12
+ * and `runPlanRunEpilogue` executes them. Single-Story runs skip the
13
+ * epilogue (`applicable: false`).
14
+ *
15
+ * @module lib/orchestration/run-epilogue
16
+ */
17
+
18
+ import { selectAudits } from '../audit-suite/index.js';
19
+ import { graduateRetroProposals } from '../feedback-loop/retro-proposals-graduator.js';
20
+ import { gitSpawn } from '../git-utils.js';
21
+ import { Logger } from '../Logger.js';
22
+ import { forEachLine } from '../observability/signals-writer.js';
23
+ import { composeRoutedProposals } from './retro-proposals.js';
24
+ import {
25
+ buildFollowUpsCommentBody,
26
+ resolveFollowUpRepos,
27
+ } from './story-follow-ups.js';
28
+ import { upsertStructuredComment } from './ticketing.js';
29
+
30
+ /**
31
+ * Canonical epilogue step kinds, in execution order.
32
+ * @type {readonly ['audit-roster', 'follow-up-rollup', 'sibling-coherence']}
33
+ */
34
+ export const RUN_EPILOGUE_STEP_KINDS = Object.freeze([
35
+ 'audit-roster',
36
+ 'follow-up-rollup',
37
+ 'sibling-coherence',
38
+ ]);
39
+
40
+ /**
41
+ * @param {string|number|{ id?: string|number, slug?: string }} entry
42
+ * @returns {string|null}
43
+ */
44
+ function normalizeStoryId(entry) {
45
+ if (typeof entry === 'string') return entry.trim() || null;
46
+ if (typeof entry === 'number' && Number.isInteger(entry)) {
47
+ return String(entry);
48
+ }
49
+ if (!entry || typeof entry !== 'object') return null;
50
+ if (typeof entry.id === 'string' || Number.isInteger(entry.id)) {
51
+ return String(entry.id).trim() || null;
52
+ }
53
+ return typeof entry.slug === 'string' ? entry.slug.trim() || null : null;
54
+ }
55
+
56
+ /**
57
+ * @param {Array<string|number|{ id?: string|number, slug?: string }>} stories
58
+ * @returns {string[]}
59
+ */
60
+ function normalizeStoryIds(stories) {
61
+ const list = Array.isArray(stories) ? stories : [];
62
+ const seen = new Set();
63
+ const ids = [];
64
+ for (const entry of list) {
65
+ const id = normalizeStoryId(entry);
66
+ if (!id || seen.has(id)) continue;
67
+ seen.add(id);
68
+ ids.push(id);
69
+ }
70
+ return ids;
71
+ }
72
+
73
+ /**
74
+ * Enumerate the ordered epilogue steps for a completed run.
75
+ *
76
+ * @param {object} args
77
+ * @param {string} args.planRunId
78
+ * @param {Array<string|number|{ id?: string|number, slug?: string }>} args.stories
79
+ * @returns {object}
80
+ */
81
+ export function planRunEpilogue({ planRunId, stories } = {}) {
82
+ const ids = normalizeStoryIds(stories);
83
+ const runId =
84
+ typeof planRunId === 'string' && planRunId.trim() !== ''
85
+ ? planRunId.trim()
86
+ : null;
87
+
88
+ if (ids.length <= 1) {
89
+ return {
90
+ applicable: false,
91
+ planRunId: runId,
92
+ stories: ids,
93
+ steps: [],
94
+ reason:
95
+ ids.length === 0
96
+ ? 'no Stories in run'
97
+ : 'single-Story run — per-Story close is the end; no run-scoped epilogue',
98
+ };
99
+ }
100
+
101
+ // Positional `/deliver 101 102` has no plan-run label. Synthesize a
102
+ // stable adhoc id from the sorted Story set so the epilogue still
103
+ // anchors comments / audit roster without requiring `--run`.
104
+ const effectiveRunId =
105
+ runId ??
106
+ `adhoc-${[...ids].sort((a, b) => Number(a) - Number(b)).join('-')}`;
107
+
108
+ const steps = [
109
+ {
110
+ kind: 'audit-roster',
111
+ description: `Select cross-Story audit lenses for run ${effectiveRunId}`,
112
+ stories: ids,
113
+ },
114
+ {
115
+ kind: 'follow-up-rollup',
116
+ description: `Friction follow-up roll-up for run ${effectiveRunId}`,
117
+ stories: ids,
118
+ },
119
+ {
120
+ kind: 'sibling-coherence',
121
+ description: `Sibling-coherence check across the ${ids.length} Story specs of run ${effectiveRunId}`,
122
+ stories: ids,
123
+ },
124
+ ];
125
+
126
+ return {
127
+ applicable: true,
128
+ planRunId: effectiveRunId,
129
+ stories: ids,
130
+ steps,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * How many first-parent commits of the base ref to scan when looking for the
136
+ * run's landed squash-merges. The epilogue fires immediately after the run's
137
+ * last Story lands, so the run's merges sit within the first handful of
138
+ * commits; the limit only bounds the pathological case.
139
+ * @type {number}
140
+ */
141
+ const BASE_SCAN_LIMIT = 500;
142
+
143
+ /** ASCII unit separator — cannot occur in a git commit subject. */
144
+ const FIELD_SEP = '\x1f';
145
+
146
+ /**
147
+ * Read the base ref's first-parent history as `{ sha, parents, subject }`
148
+ * records, newest-first.
149
+ *
150
+ * @param {object} args
151
+ * @param {string} args.cwd
152
+ * @param {string} args.baseRef
153
+ * @param {number} args.scanLimit
154
+ * @param {{ gitSpawn: Function }} args.git
155
+ * @returns {{ ok: true, commits: Array<{sha: string, parents: string[], subject: string}> }
156
+ * | { ok: false, reason: string }}
157
+ */
158
+ function readFirstParentHistory({ cwd, baseRef, scanLimit, git }) {
159
+ let result;
160
+ try {
161
+ result = git.gitSpawn(
162
+ cwd,
163
+ 'log',
164
+ '--first-parent',
165
+ baseRef,
166
+ `--max-count=${scanLimit}`,
167
+ `--format=%H${FIELD_SEP}%P${FIELD_SEP}%s`,
168
+ );
169
+ } catch (err) {
170
+ return {
171
+ ok: false,
172
+ reason: `\`git log ${baseRef}\` could not be spawned: ${err?.message ?? String(err)}`,
173
+ };
174
+ }
175
+ if (result?.status !== 0) {
176
+ const detail =
177
+ String(result?.stderr ?? '').split('\n')[0] || 'unknown error';
178
+ return {
179
+ ok: false,
180
+ reason: `\`git log ${baseRef}\` failed (is \`${baseRef}\` fetched?): ${detail}`,
181
+ };
182
+ }
183
+ const commits = String(result.stdout ?? '')
184
+ .split('\n')
185
+ .map((line) => line.trim())
186
+ .filter(Boolean)
187
+ .map((line) => {
188
+ const [sha, parents, ...rest] = line.split(FIELD_SEP);
189
+ return {
190
+ sha,
191
+ parents: parents ? parents.split(' ').filter(Boolean) : [],
192
+ subject: rest.join(FIELD_SEP),
193
+ };
194
+ });
195
+ return { ok: true, commits };
196
+ }
197
+
198
+ /**
199
+ * One trailing `(#<n>)` / `(refs #<n>)` marker, anchored at the end of what
200
+ * is left of a subject after the markers to its right have been peeled off.
201
+ */
202
+ const TRAILING_MARKER_RE = /\s*\((?:refs\s+)?#(\d+)\)$/;
203
+
204
+ /**
205
+ * Peel the **trailing run** of `(#<n>)` markers off a squash subject.
206
+ *
207
+ * The shape this parses is fixed by the close pipeline plus GitHub: the PR
208
+ * title `normalizePrTitle` writes ends with `(#<storyId>)`, and GitHub's
209
+ * squash appends ` (#<prNumber>)` to it — so a landed Story merge reads
210
+ * `<subject> (#<storyId>) (#<prNumber>)` and the Story's own marker is the
211
+ * *second-to-last* marker, not the last one. Both are returned; the caller
212
+ * decides which ids it cares about.
213
+ *
214
+ * Why the trailing run rather than a substring scan (the bug this replaces):
215
+ * `subject.includes('(#101)')` matched the id **anywhere**, so an unrelated
216
+ * later commit quoting an old marker — canonically a revert, whose subject
217
+ * embeds the reverted title verbatim: `revert: "fix: x (#101) (#900)" (#950)`
218
+ * — anchored the run on a far-older commit and inflated the roster diff with
219
+ * everything in between. Peeling from the right stops at the first character
220
+ * that is not part of a marker (the closing quote, above), so a quoted marker
221
+ * is structurally out of reach.
222
+ *
223
+ * The `refs #` form is accepted because it is the other PR-title shape this
224
+ * repo's own history carries (`feat(x): … (refs #4575) (#4582)`) — the
225
+ * `refs #<id>` convention from `rules/git-conventions.md`. A plain substring
226
+ * scan for `(#4575)` never matched those at all.
227
+ *
228
+ * @param {string} subject
229
+ * @returns {number[]} Marker ids, right-to-left (PR number first).
230
+ */
231
+ function trailingMarkerIds(subject) {
232
+ const ids = [];
233
+ let rest = typeof subject === 'string' ? subject.trimEnd() : '';
234
+ for (;;) {
235
+ const match = TRAILING_MARKER_RE.exec(rest);
236
+ if (!match) return ids;
237
+ ids.push(Number(match[1]));
238
+ rest = rest.slice(0, match.index).trimEnd();
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Resolve the **pre-run base sha**: the commit the base branch pointed at
244
+ * before the run's first Story landed.
245
+ *
246
+ * Why not `origin/main...HEAD` (the bug this replaces): the epilogue runs
247
+ * *after* the run's last Story lands, so HEAD in the main checkout is either
248
+ * `origin/main` itself or an ancestor of it. The three-dot merge-base is then
249
+ * HEAD, and the diff is empty **by construction** — it never reported the
250
+ * run's real diff. Rolling HEAD back does not help, so branch-reaping /
251
+ * cleanup ordering is not the cause; the refs being compared are.
252
+ *
253
+ * The derivation walks the base ref's first-parent history for the run's
254
+ * landed squash-merges. Every Story PR title carries a `(#<storyId>)` suffix
255
+ * (guaranteed by `normalizePrTitle` in the close pipeline) and GitHub uses the
256
+ * PR title as the squash subject, so the marker is a reliable, offline handle
257
+ * that does **not** depend on the Story branches still existing. The earliest
258
+ * such merge's first parent is the pre-run base.
259
+ *
260
+ * @param {object} args
261
+ * @param {Array<string|number>} args.stories - Story ids in the run.
262
+ * @param {string} args.cwd
263
+ * @param {string} [args.baseRef] - Remote-tracking base ref, e.g. `origin/main`.
264
+ * @param {number} [args.scanLimit]
265
+ * @param {{ gitSpawn: Function }} [args.git]
266
+ * @returns {{ resolved: true, baseSha: string, mergeSha: string, storyId: number, baseRef: string }
267
+ * | { resolved: false, reason: string, baseRef: string }}
268
+ */
269
+ export function resolveRunBaseSha({
270
+ stories,
271
+ cwd,
272
+ baseRef = 'origin/main',
273
+ scanLimit = BASE_SCAN_LIMIT,
274
+ git = { gitSpawn },
275
+ } = {}) {
276
+ const ids = (Array.isArray(stories) ? stories : [])
277
+ .map(Number)
278
+ .filter((n) => Number.isInteger(n) && n > 0);
279
+ if (ids.length === 0) {
280
+ return {
281
+ resolved: false,
282
+ baseRef,
283
+ reason:
284
+ 'the run carries no numeric Story ids to match landed merges against',
285
+ };
286
+ }
287
+
288
+ const history = readFirstParentHistory({ cwd, baseRef, scanLimit, git });
289
+ if (!history.ok) {
290
+ return { resolved: false, baseRef, reason: history.reason };
291
+ }
292
+
293
+ // `git log` is newest-first; walk backwards so the first hit is the
294
+ // *earliest* merge belonging to the run.
295
+ const wanted = new Set(ids);
296
+ for (let i = history.commits.length - 1; i >= 0; i -= 1) {
297
+ const commit = history.commits[i];
298
+ const hit = trailingMarkerIds(commit.subject).find((id) => wanted.has(id));
299
+ if (hit === undefined) continue;
300
+ const baseSha = commit.parents[0];
301
+ if (!baseSha) {
302
+ return {
303
+ resolved: false,
304
+ baseRef,
305
+ reason: `the earliest landed merge for the run (${commit.sha}, Story #${hit}) is a root commit — it has no first parent to use as the pre-run base`,
306
+ };
307
+ }
308
+ return {
309
+ resolved: true,
310
+ baseRef,
311
+ baseSha,
312
+ mergeSha: commit.sha,
313
+ storyId: hit,
314
+ };
315
+ }
316
+
317
+ return {
318
+ resolved: false,
319
+ baseRef,
320
+ reason: `no landed squash-merge carrying a \`(#<storyId>)\` marker for ${ids
321
+ .map((id) => `#${id}`)
322
+ .join(
323
+ ', ',
324
+ )} was found in the last ${scanLimit} first-parent commits of \`${baseRef}\` — has the run landed?`,
325
+ };
326
+ }
327
+
328
+ /**
329
+ * List the files the run changed: `<pre-run base>...<baseRef>`.
330
+ *
331
+ * @returns {{ ok: true, files: string[] } | { ok: false, reason: string }}
332
+ */
333
+ function listChangedFiles({ cwd, baseSha, headRef, git }) {
334
+ const range = `${baseSha}...${headRef}`;
335
+ let result;
336
+ try {
337
+ result = git.gitSpawn(cwd, 'diff', '--name-only', range);
338
+ } catch (err) {
339
+ return {
340
+ ok: false,
341
+ reason: `\`git diff ${range}\` could not be spawned: ${err?.message ?? String(err)}`,
342
+ };
343
+ }
344
+ if (result?.status !== 0) {
345
+ const detail =
346
+ String(result?.stderr ?? '').split('\n')[0] || 'unknown error';
347
+ return { ok: false, reason: `\`git diff ${range}\` failed: ${detail}` };
348
+ }
349
+ return {
350
+ ok: true,
351
+ files: String(result.stdout ?? '')
352
+ .split('\n')
353
+ .map((line) => line.trim())
354
+ .filter(Boolean),
355
+ };
356
+ }
357
+
358
+ /**
359
+ * Resolve the run's combined landed diff, or an explicit reason it could not
360
+ * be computed. Never conflates "could not compute" with "zero files changed".
361
+ *
362
+ * @returns {{ resolved: boolean, changedFiles: string[], baseSha: string|null,
363
+ * mergeSha: string|null, baseRef: string, reason: string|null }}
364
+ */
365
+ function resolveCombinedDiff({ stories, cwd, baseRef, git }) {
366
+ const base = resolveRunBaseSha({ stories, cwd, baseRef, git });
367
+ if (!base.resolved) {
368
+ return {
369
+ resolved: false,
370
+ changedFiles: [],
371
+ baseSha: null,
372
+ mergeSha: null,
373
+ baseRef,
374
+ reason: base.reason,
375
+ };
376
+ }
377
+ const diff = listChangedFiles({
378
+ cwd,
379
+ baseSha: base.baseSha,
380
+ headRef: baseRef,
381
+ git,
382
+ });
383
+ if (!diff.ok) {
384
+ return {
385
+ resolved: false,
386
+ changedFiles: [],
387
+ baseSha: base.baseSha,
388
+ mergeSha: base.mergeSha,
389
+ baseRef,
390
+ reason: diff.reason,
391
+ };
392
+ }
393
+ return {
394
+ resolved: true,
395
+ changedFiles: diff.files,
396
+ baseSha: base.baseSha,
397
+ mergeSha: base.mergeSha,
398
+ baseRef,
399
+ reason: null,
400
+ };
401
+ }
402
+
403
+ /**
404
+ * The diff line of the roster comment. An unresolved base MUST read as a
405
+ * loud failure, never as `Changed files considered: 0`.
406
+ *
407
+ * @param {ReturnType<typeof resolveCombinedDiff>} diff
408
+ * @returns {string[]}
409
+ */
410
+ function renderDiffLines(diff) {
411
+ if (!diff.resolved) {
412
+ return [
413
+ '> ⚠️ **Combined landed diff unavailable — this is NOT "zero files changed".**',
414
+ `> The pre-run base sha could not be resolved: ${diff.reason}`,
415
+ '> Walk the lenses below against the run diff determined by hand.',
416
+ '> Lens selection was **keyword-only**: with no change set, no lens',
417
+ '> `filePatterns` trigger could fire, so the roster below reflects the',
418
+ "> primary Story's prose rather than what the run touched. Treat it as a",
419
+ '> starting point, not a roster.',
420
+ ];
421
+ }
422
+ return [
423
+ `Combined landed diff \`${diff.baseSha}...${diff.baseRef}\` — ` +
424
+ `**${diff.changedFiles.length}** changed file(s).`,
425
+ ];
426
+ }
427
+
428
+ /**
429
+ * @param {object} config
430
+ * @returns {string} the remote-tracking base ref, e.g. `origin/main`.
431
+ */
432
+ function resolveBaseRef(config) {
433
+ const baseBranch = config?.project?.baseBranch;
434
+ const branch =
435
+ typeof baseBranch === 'string' && baseBranch.trim() !== ''
436
+ ? baseBranch.trim()
437
+ : 'main';
438
+ return `origin/${branch}`;
439
+ }
440
+
441
+ async function executeAuditRoster({
442
+ planRunId,
443
+ stories,
444
+ cwd,
445
+ provider,
446
+ config,
447
+ git,
448
+ selectAuditsFn,
449
+ }) {
450
+ const primaryId = Number(stories[0]);
451
+ const diff = resolveCombinedDiff({
452
+ stories,
453
+ cwd,
454
+ baseRef: resolveBaseRef(config),
455
+ git,
456
+ });
457
+ // Hand `selectAudits` the change set we just resolved — never a git range for
458
+ // it to re-derive. This function runs in the main checkout *after* the run's
459
+ // Stories merged, so every range it could name (`main...HEAD`) is empty by
460
+ // construction; asking for one is how the roster came to select lenses from
461
+ // zero files while printing the correct file list beside them (Story #4571).
462
+ const lensGrounding = diff.resolved ? 'diff' : 'keyword-only';
463
+ let selectedAudits = [];
464
+ if (Number.isInteger(primaryId) && primaryId > 0) {
465
+ const selected = await selectAuditsFn({
466
+ ticketId: primaryId,
467
+ gate: 'gate3',
468
+ provider,
469
+ changedFiles: diff.resolved ? diff.changedFiles : [],
470
+ });
471
+ selectedAudits = Array.isArray(selected?.selectedAudits)
472
+ ? selected.selectedAudits
473
+ : Array.isArray(selected)
474
+ ? selected
475
+ : [];
476
+ }
477
+ if (!diff.resolved) {
478
+ Logger.warn(
479
+ `[run-epilogue] plan-run ${planRunId}: combined landed diff unavailable — ${diff.reason}`,
480
+ );
481
+ }
482
+ const body = [
483
+ '### plan-run-audit-roster',
484
+ '',
485
+ `Cross-Story audit roster for plan-run \`${planRunId}\`.`,
486
+ '',
487
+ ...renderDiffLines(diff),
488
+ '',
489
+ `**Selected lenses** (host MUST walk each against the combined landed diff) — ` +
490
+ `grounding: \`${lensGrounding}\`:`,
491
+ ...(selectedAudits.length > 0
492
+ ? selectedAudits.map((lens) => `- \`${lens}\``)
493
+ : ['- _(none — docs-only or no matching change-set lenses)_']),
494
+ '',
495
+ '```json',
496
+ JSON.stringify(
497
+ {
498
+ planRunId,
499
+ stories: stories.map(Number),
500
+ baseResolution: {
501
+ resolved: diff.resolved,
502
+ baseRef: diff.baseRef,
503
+ baseSha: diff.baseSha,
504
+ mergeSha: diff.mergeSha,
505
+ reason: diff.reason,
506
+ },
507
+ changedFiles: diff.resolved ? diff.changedFiles : null,
508
+ lensGrounding,
509
+ selectedAudits,
510
+ },
511
+ null,
512
+ 2,
513
+ ),
514
+ '```',
515
+ ].join('\n');
516
+ if (Number.isInteger(primaryId) && primaryId > 0) {
517
+ await upsertStructuredComment(
518
+ provider,
519
+ primaryId,
520
+ 'plan-run-audit-roster',
521
+ body,
522
+ );
523
+ }
524
+ return {
525
+ kind: 'audit-roster',
526
+ selectedAudits,
527
+ // Whether the lenses above were chosen from the run's landed files
528
+ // (`diff`) or, with no resolvable base, from the primary Story's prose
529
+ // alone (`keyword-only`). The two are not interchangeable: a keyword-only
530
+ // roster cannot select a lens that declares no keywords, so its silence
531
+ // about a lens says nothing about the code.
532
+ lensGrounding,
533
+ // `null` — not `0` — when unresolved: a zero count must only ever mean
534
+ // "the run genuinely changed nothing".
535
+ changedFileCount: diff.resolved ? diff.changedFiles.length : null,
536
+ changedFiles: diff.resolved ? diff.changedFiles : null,
537
+ baseResolution: {
538
+ resolved: diff.resolved,
539
+ baseRef: diff.baseRef,
540
+ baseSha: diff.baseSha,
541
+ mergeSha: diff.mergeSha,
542
+ reason: diff.reason,
543
+ },
544
+ };
545
+ }
546
+
547
+ async function executeFollowUpRollup({
548
+ planRunId,
549
+ stories,
550
+ provider,
551
+ config,
552
+ cwd,
553
+ }) {
554
+ const signals = [];
555
+ for (const raw of stories) {
556
+ const sid = Number(raw);
557
+ if (!Number.isInteger(sid) || sid <= 0) continue;
558
+ await forEachLine(
559
+ null,
560
+ sid,
561
+ (parsed) => {
562
+ if (!parsed || typeof parsed !== 'object') return;
563
+ const category =
564
+ typeof parsed.category === 'string' ? parsed.category.trim() : '';
565
+ if (!category) return;
566
+ const source = parsed.source === 'framework' ? 'framework' : 'consumer';
567
+ signals.push({ category, source });
568
+ },
569
+ config,
570
+ );
571
+ }
572
+ const repos = resolveFollowUpRepos(config);
573
+ const primaryId = Number(stories[0]);
574
+ const proposals = composeRoutedProposals({
575
+ anchorId: Number.isInteger(primaryId) ? primaryId : 1,
576
+ anchorKind: 'run',
577
+ frameworkRepo: repos.frameworkRepo,
578
+ consumerRepo: repos.consumerRepo,
579
+ signals,
580
+ unresolvedBlockedEvents: [],
581
+ });
582
+ // Patch titles to mention the plan-run token (anchorKind run uses numeric id).
583
+ for (const item of [...proposals.framework, ...proposals.consumer]) {
584
+ item.title = item.title.replace(/plan-run \d+/, `plan-run ${planRunId}`);
585
+ item.body = item.body.replace(/plan-run \d+/g, `plan-run ${planRunId}`);
586
+ }
587
+ const graduated = await graduateRetroProposals({
588
+ epicId: primaryId,
589
+ provider,
590
+ config,
591
+ currentRepo: repos.currentRepo,
592
+ frameworkRepo: (() => {
593
+ const [owner, repo] = repos.frameworkRepo.split('/');
594
+ return { owner, repo };
595
+ })(),
596
+ routedProposals: proposals,
597
+ cwd,
598
+ });
599
+ if (Number.isInteger(primaryId) && primaryId > 0) {
600
+ const body = buildFollowUpsCommentBody({
601
+ storyId: primaryId,
602
+ proposals,
603
+ graduated,
604
+ // Story #4578 — the run's Story count is what lets an empty roll-up
605
+ // render as a flagged claim ("0 signals across N Stories") rather than
606
+ // as "nothing to follow up".
607
+ storyCount: stories.length,
608
+ }).replace(
609
+ `from Story #${primaryId}`,
610
+ `from plan-run \`${planRunId}\` (primary Story #${primaryId})`,
611
+ );
612
+ await upsertStructuredComment(provider, primaryId, 'follow-ups', body);
613
+ }
614
+ return {
615
+ kind: 'follow-up-rollup',
616
+ signalCount: signals.length,
617
+ storyCount: stories.length,
618
+ filed: graduated.filed?.length ?? 0,
619
+ // Story #4578 — zero signals across a multi-Story run is a claim, not a
620
+ // clean bill of health. Surfaced on the step result so the CLI can warn
621
+ // the operator without re-deriving it from the comment prose.
622
+ emptyRollupSuspect: signals.length === 0 && stories.length > 1,
623
+ };
624
+ }
625
+
626
+ function extractSection(body, heading) {
627
+ if (typeof body !== 'string') return '';
628
+ const re = new RegExp(
629
+ `(?:^|\\n)## ${heading}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`,
630
+ 'i',
631
+ );
632
+ const match = body.match(re);
633
+ return match ? match[1].trim() : '';
634
+ }
635
+
636
+ async function executeSiblingCoherence({ planRunId, stories, provider }) {
637
+ const findings = [];
638
+ const bodies = [];
639
+ for (const raw of stories) {
640
+ const sid = Number(raw);
641
+ if (!Number.isInteger(sid) || sid <= 0) continue;
642
+ const ticket = await provider.getTicket(sid);
643
+ bodies.push({
644
+ id: sid,
645
+ title: ticket?.title ?? '',
646
+ acceptance: extractSection(ticket?.body ?? '', 'Acceptance'),
647
+ spec: extractSection(ticket?.body ?? '', 'Spec'),
648
+ });
649
+ }
650
+ const withAcceptance = bodies.filter((b) => b.acceptance.length > 0);
651
+ if (withAcceptance.length > 0 && withAcceptance.length < bodies.length) {
652
+ const missing = bodies
653
+ .filter((b) => b.acceptance.length === 0)
654
+ .map((b) => `#${b.id}`);
655
+ findings.push(
656
+ `Stories missing ## Acceptance while siblings declare ACs: ${missing.join(', ')}`,
657
+ );
658
+ }
659
+ // Detect identical non-empty Spec blobs (likely copy-paste drift).
660
+ const specMap = new Map();
661
+ for (const b of bodies) {
662
+ if (!b.spec) continue;
663
+ const key = b.spec.replace(/\s+/g, ' ').slice(0, 400);
664
+ if (!specMap.has(key)) specMap.set(key, []);
665
+ specMap.get(key).push(b.id);
666
+ }
667
+ for (const ids of specMap.values()) {
668
+ if (ids.length > 1) {
669
+ findings.push(
670
+ `Duplicate ## Spec prose across Stories ${ids.map((id) => `#${id}`).join(', ')} — split or dedupe.`,
671
+ );
672
+ }
673
+ }
674
+ const primaryId = Number(stories[0]);
675
+ const body = [
676
+ '### plan-run-sibling-coherence',
677
+ '',
678
+ `Sibling-coherence check for plan-run \`${planRunId}\`.`,
679
+ '',
680
+ findings.length === 0
681
+ ? '_No coherence findings._'
682
+ : findings.map((f) => `- ${f}`).join('\n'),
683
+ '',
684
+ '```json',
685
+ JSON.stringify(
686
+ { planRunId, stories: stories.map(Number), findings },
687
+ null,
688
+ 2,
689
+ ),
690
+ '```',
691
+ ].join('\n');
692
+ if (Number.isInteger(primaryId) && primaryId > 0) {
693
+ await upsertStructuredComment(
694
+ provider,
695
+ primaryId,
696
+ 'plan-run-sibling-coherence',
697
+ body,
698
+ );
699
+ }
700
+ return { kind: 'sibling-coherence', findings };
701
+ }
702
+
703
+ /**
704
+ * Execute the per-run epilogue. Throws only on programmer misuse; step
705
+ * failures are collected into `errors[]`.
706
+ *
707
+ * @param {object} args
708
+ * @param {string} args.planRunId
709
+ * @param {Array<string|number>} args.stories
710
+ * @param {object} args.provider
711
+ * @param {object} [args.config]
712
+ * @param {string} [args.cwd]
713
+ * @param {{ gitSpawn: Function }} [args.git] - Injection seam for tests.
714
+ * @param {typeof selectAudits} [args.selectAuditsFn] - Injection seam for tests.
715
+ * @returns {Promise<object>}
716
+ */
717
+ export async function runPlanRunEpilogue({
718
+ planRunId,
719
+ stories,
720
+ provider,
721
+ config,
722
+ cwd = process.cwd(),
723
+ git = { gitSpawn },
724
+ selectAuditsFn = selectAudits,
725
+ } = {}) {
726
+ const plan = planRunEpilogue({ planRunId, stories });
727
+ if (!plan.applicable) {
728
+ return { ...plan, results: [], errors: [] };
729
+ }
730
+ if (!provider || typeof provider.getTicket !== 'function') {
731
+ throw new TypeError('runPlanRunEpilogue requires a ticketing provider');
732
+ }
733
+
734
+ const results = [];
735
+ const errors = [];
736
+ for (const step of plan.steps) {
737
+ try {
738
+ if (step.kind === 'audit-roster') {
739
+ results.push(
740
+ await executeAuditRoster({
741
+ planRunId: plan.planRunId,
742
+ stories: plan.stories,
743
+ cwd,
744
+ provider,
745
+ config,
746
+ git,
747
+ selectAuditsFn,
748
+ }),
749
+ );
750
+ } else if (step.kind === 'follow-up-rollup') {
751
+ results.push(
752
+ await executeFollowUpRollup({
753
+ planRunId: plan.planRunId,
754
+ stories: plan.stories,
755
+ provider,
756
+ config,
757
+ cwd,
758
+ }),
759
+ );
760
+ } else if (step.kind === 'sibling-coherence') {
761
+ results.push(
762
+ await executeSiblingCoherence({
763
+ planRunId: plan.planRunId,
764
+ stories: plan.stories,
765
+ provider,
766
+ }),
767
+ );
768
+ }
769
+ } catch (err) {
770
+ const message = err?.message ?? String(err);
771
+ Logger.warn(`[run-epilogue] step ${step.kind} failed: ${message}`);
772
+ errors.push({ kind: step.kind, message });
773
+ }
774
+ }
775
+
776
+ return {
777
+ applicable: true,
778
+ planRunId: plan.planRunId,
779
+ stories: plan.stories,
780
+ steps: plan.steps,
781
+ results,
782
+ errors,
783
+ };
784
+ }