mandrel 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. package/.agents/README.md +59 -28
  2. package/.agents/agents/acceptance-critic.md +20 -9
  3. package/.agents/agents/story-worker.md +45 -48
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +60 -46
  6. package/.agents/docs/agentrc-reference.json +8 -13
  7. package/.agents/docs/configuration.md +33 -57
  8. package/.agents/docs/execution-reference.md +39 -10
  9. package/.agents/docs/quality-gates.md +17 -19
  10. package/.agents/docs/workflows.md +6 -6
  11. package/.agents/instructions.md +64 -79
  12. package/.agents/rules/ci-remediation.md +3 -3
  13. package/.agents/rules/gherkin-standards.md +10 -0
  14. package/.agents/rules/git-conventions-reference.md +42 -51
  15. package/.agents/schemas/acceptance-eval-verdict.schema.json +2 -2
  16. package/.agents/schemas/agentrc.schema.json +35 -46
  17. package/.agents/schemas/audit-rules.json +59 -1
  18. package/.agents/schemas/audit-rules.schema.json +33 -1
  19. package/.agents/schemas/lifecycle/README.md +1 -2
  20. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  21. package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
  22. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
  23. package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
  24. package/.agents/schemas/signal-event.schema.json +3 -3
  25. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  26. package/.agents/schemas/validation-evidence.schema.json +1 -1
  27. package/.agents/scripts/acceptance-eval.js +24 -68
  28. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  29. package/.agents/scripts/bootstrap.js +3 -3
  30. package/.agents/scripts/check-dead-exports.js +43 -104
  31. package/.agents/scripts/check-doc-links.js +2 -2
  32. package/.agents/scripts/check-lifecycle-lint.js +1 -1
  33. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  34. package/.agents/scripts/deliver-recover.js +122 -0
  35. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  36. package/.agents/scripts/evidence-gate.js +20 -50
  37. package/.agents/scripts/generate-skills-index.js +17 -1
  38. package/.agents/scripts/generate-workflows-doc.js +4 -4
  39. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  40. package/.agents/scripts/lib/audit-suite/selector.js +323 -23
  41. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
  42. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  43. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  44. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  45. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  46. package/.agents/scripts/lib/checks/core-bare-clean.js +4 -1
  47. package/.agents/scripts/lib/checks/index.js +1 -1
  48. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  49. package/.agents/scripts/lib/checks/state.js +17 -248
  50. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +3 -3
  51. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  52. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  53. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  54. package/.agents/scripts/lib/cli-args.js +23 -2
  55. package/.agents/scripts/lib/close-validation/gates.js +13 -13
  56. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  57. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  58. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  59. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  60. package/.agents/scripts/lib/config/acceptance-eval.js +2 -2
  61. package/.agents/scripts/lib/config/delivery-routing.js +7 -6
  62. package/.agents/scripts/lib/config/explain.js +10 -16
  63. package/.agents/scripts/lib/config/github.js +7 -5
  64. package/.agents/scripts/lib/config/limits.js +15 -25
  65. package/.agents/scripts/lib/config/quality.js +11 -14
  66. package/.agents/scripts/lib/config/runners.js +8 -21
  67. package/.agents/scripts/lib/config/temp-paths.js +18 -56
  68. package/.agents/scripts/lib/config-settings-schema-delivery.js +34 -16
  69. package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
  70. package/.agents/scripts/lib/config-settings-schema.js +48 -22
  71. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  72. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  73. package/.agents/scripts/lib/duplicate-search.js +38 -7
  74. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  75. package/.agents/scripts/lib/format-generated-json.js +97 -0
  76. package/.agents/scripts/lib/framework-version.js +19 -189
  77. package/.agents/scripts/lib/gh-exec.js +8 -0
  78. package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
  79. package/.agents/scripts/lib/git-utils.js +0 -14
  80. package/.agents/scripts/lib/json-utils.js +1 -2
  81. package/.agents/scripts/lib/label-constants.js +0 -15
  82. package/.agents/scripts/lib/label-taxonomy.js +1 -12
  83. package/.agents/scripts/lib/observability/active-story-env.js +42 -163
  84. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  85. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  86. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  87. package/.agents/scripts/lib/observability/source-classifier.js +2 -2
  88. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  89. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  90. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  91. package/.agents/scripts/lib/orchestration/ceremony-routing.js +43 -45
  92. package/.agents/scripts/lib/orchestration/change-set.js +103 -0
  93. package/.agents/scripts/lib/orchestration/code-review.js +70 -191
  94. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
  95. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  96. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  97. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  98. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  99. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  100. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +9 -11
  101. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  102. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  103. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +7 -3
  104. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  105. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  106. package/.agents/scripts/lib/orchestration/merge-block-class.js +76 -20
  107. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  108. package/.agents/scripts/lib/orchestration/plan-context.js +116 -33
  109. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +26 -36
  110. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +31 -22
  111. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  112. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
  113. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
  114. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  115. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -100
  116. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
  117. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
  118. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  119. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +230 -0
  120. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
  121. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -2
  122. package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
  123. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  124. package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
  125. package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
  126. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  127. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  128. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  129. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  130. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  131. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  132. package/.agents/scripts/lib/orchestration/run-epilogue.js +374 -16
  133. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
  134. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  135. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  136. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +4 -13
  137. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  138. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +72 -30
  139. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  140. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  141. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +12 -8
  142. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  143. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  144. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +264 -43
  145. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  146. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  147. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +104 -279
  148. package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +191 -0
  149. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +120 -0
  150. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  151. package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
  152. package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
  153. package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
  154. package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
  155. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  156. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
  157. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
  158. package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
  159. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
  160. package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
  161. package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
  162. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  163. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  164. package/.agents/scripts/lib/planning-corpus.js +12 -286
  165. package/.agents/scripts/lib/preflight-runner.js +2 -2
  166. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  167. package/.agents/scripts/lib/signals/index.js +4 -17
  168. package/.agents/scripts/lib/signals/read.js +35 -35
  169. package/.agents/scripts/lib/signals/schema.js +8 -11
  170. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  171. package/.agents/scripts/lib/signals/write.js +0 -1
  172. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  173. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  174. package/.agents/scripts/lib/story-adjacency.js +8 -7
  175. package/.agents/scripts/lib/story-body/story-body.js +81 -13
  176. package/.agents/scripts/lib/templates/decomposer-prompts.js +15 -16
  177. package/.agents/scripts/lib/test-env.js +14 -1
  178. package/.agents/scripts/lib/test-tiers.js +0 -3
  179. package/.agents/scripts/lib/ticket-body-sections.js +0 -14
  180. package/.agents/scripts/lib/validation-evidence.js +31 -59
  181. package/.agents/scripts/lib/wave-runner/live-probe.js +315 -0
  182. package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
  183. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  184. package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
  185. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  186. package/.agents/scripts/plan-context.js +38 -7
  187. package/.agents/scripts/plan-critics.js +203 -0
  188. package/.agents/scripts/plan-persist.js +145 -35
  189. package/.agents/scripts/plan-run-epilogue.js +83 -38
  190. package/.agents/scripts/post-structured-comment.js +0 -38
  191. package/.agents/scripts/pr-watch-with-update.js +43 -22
  192. package/.agents/scripts/providers/github/compose.js +0 -1
  193. package/.agents/scripts/providers/github/errors.js +0 -19
  194. package/.agents/scripts/providers/github/issues.js +1 -11
  195. package/.agents/scripts/providers/github/mappers.js +5 -0
  196. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  197. package/.agents/scripts/providers/github/tickets.js +33 -153
  198. package/.agents/scripts/providers/github.js +17 -6
  199. package/.agents/scripts/quality-preview.js +13 -6
  200. package/.agents/scripts/resolve-stories.js +236 -0
  201. package/.agents/scripts/run-coverage.js +4 -1
  202. package/.agents/scripts/run-lint.js +2 -2
  203. package/.agents/scripts/run-verify.js +31 -2
  204. package/.agents/scripts/signals-view.js +9 -10
  205. package/.agents/scripts/single-story-close.js +173 -18
  206. package/.agents/scripts/single-story-confirm-merge.js +288 -15
  207. package/.agents/scripts/single-story-init.js +6 -10
  208. package/.agents/scripts/stories-wave-tick.js +380 -53
  209. package/.agents/scripts/story-plan.js +3 -3
  210. package/.agents/scripts/update-ticket-state.js +8 -50
  211. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  212. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  213. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  214. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  215. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  216. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  217. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  218. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  219. package/.agents/skills/core/idea-refinement/SKILL.md +3 -3
  220. package/.agents/skills/core/scope-triage/SKILL.md +3 -0
  221. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  222. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  223. package/.agents/skills/skills.index.json +2 -12
  224. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  225. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  226. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  227. package/.agents/workflows/audit-architecture.md +3 -4
  228. package/.agents/workflows/audit-clean-code.md +4 -4
  229. package/.agents/workflows/audit-documentation.md +4 -5
  230. package/.agents/workflows/audit-lighthouse.md +8 -0
  231. package/.agents/workflows/audit-navigability.md +10 -0
  232. package/.agents/workflows/audit-performance.md +2 -3
  233. package/.agents/workflows/audit-quality.md +8 -9
  234. package/.agents/workflows/audit-security.md +1 -2
  235. package/.agents/workflows/audit-seo.md +10 -0
  236. package/.agents/workflows/audit-ux-ui.md +7 -0
  237. package/.agents/workflows/deliver.md +133 -45
  238. package/.agents/workflows/git-cleanup.md +2 -2
  239. package/.agents/workflows/git-deliver.md +1 -1
  240. package/.agents/workflows/helpers/acceptance-self-eval.md +34 -17
  241. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -12
  242. package/.agents/workflows/helpers/code-review.md +14 -12
  243. package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
  244. package/.agents/workflows/helpers/deliver-story.md +209 -118
  245. package/.agents/workflows/helpers/parallel-tooling.md +2 -2
  246. package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
  247. package/.agents/workflows/plan.md +239 -19
  248. package/.agents/workflows/qa-assist.md +6 -6
  249. package/.agents/workflows/qa-explore.md +3 -3
  250. package/.agents/workflows/qa-run.md +1 -5
  251. package/bin/mandrel.js +12 -1
  252. package/docs/CHANGELOG.md +62 -0
  253. package/lib/cli/registry.js +262 -19
  254. package/lib/cli/sync-agents.js +157 -0
  255. package/lib/cli/sync-commands.js +115 -6
  256. package/lib/cli/sync.js +168 -6
  257. package/lib/cli/update.js +105 -8
  258. package/lib/cli/version-helpers.js +131 -0
  259. package/lib/migrations/README.md +7 -5
  260. package/lib/migrations/index.js +17 -9
  261. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  262. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  263. package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +154 -0
  264. package/package.json +2 -2
  265. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  266. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  267. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  268. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  269. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  270. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  271. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  272. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  273. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  274. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  275. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  276. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  277. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  278. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  279. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  280. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  281. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  282. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  283. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  284. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  285. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  286. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  287. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  288. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  289. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  290. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  291. package/.agents/schemas/risk-verdict.schema.json +0 -53
  292. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  293. package/.agents/scripts/analyze-execution.js +0 -444
  294. package/.agents/scripts/check-prepush-recovery.js +0 -90
  295. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  296. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  297. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -187
  298. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  299. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  300. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  301. package/.agents/scripts/lib/orchestration/audit-lens-routing.js +0 -128
  302. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
  303. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  304. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  305. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  306. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  307. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  308. package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
  309. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  310. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  311. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  312. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -21
  313. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  314. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  315. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  316. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -397
  317. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  318. package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
  319. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  320. package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
  321. package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
  322. package/.agents/scripts/resolve-plan-run.js +0 -117
  323. package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * resolve-stories.js — resolve a list of Story ids into the
5
+ * `{ stories, dag, done }` envelope `/deliver` sequences from.
6
+ *
7
+ * This is the ONE resolution step for multi-Story delivery. `/deliver` takes
8
+ * only Story ids; the graph is discovered here, from live state, rather than
9
+ * hand-transcribed by the host or implied by a batch label.
10
+ *
11
+ * What it resolves, per Story:
12
+ * - the issue itself, fetched with **state=all** so an already-landed
13
+ * sibling is present rather than silently dropped;
14
+ * - its dependency edges: the union of body-parsed `blocked by #N` /
15
+ * `depends on #N` and native GitHub `blocked_by` edges;
16
+ * - its declared file footprint, as plain path strings, so the scheduler's
17
+ * co-dispatch overlap guard has something to work with.
18
+ *
19
+ * And across the set: every dependency id — inside the requested set or
20
+ * foreign to it — is checked against live issue state, and a blocker that is
21
+ * closed or `agent::done` lands in `done[]`. That is what makes "deliver
22
+ * Stories across plan runs and over time" work: a Story whose blocker merged
23
+ * weeks ago in a different run is simply ready.
24
+ *
25
+ * Usage:
26
+ * node .agents/scripts/resolve-stories.js --ids 101,102
27
+ * node .agents/scripts/resolve-stories.js --ids 101,102 --pretty
28
+ * node .agents/scripts/resolve-stories.js --ids 101 --no-native # skip the dependencies API
29
+ *
30
+ * Exit codes: 0 ok, 1 usage/resolution error.
31
+ */
32
+
33
+ import { parseArgs } from 'node:util';
34
+
35
+ import { runAsCli } from './lib/cli-utils.js';
36
+ import { resolveConfig } from './lib/config-resolver.js';
37
+ import { Logger, routeAllOutputToStderr } from './lib/Logger.js';
38
+ import {
39
+ buildStoriesEnvelope,
40
+ isSatisfiedBlocker,
41
+ parseIds,
42
+ readNativeBlockedBy,
43
+ toStoryRecord,
44
+ } from './lib/orchestration/resolve-stories.js';
45
+ import { createProvider } from './lib/provider-factory.js';
46
+ import { concurrentMap } from './lib/util/concurrent-map.js';
47
+ import { parseApiJson } from './providers/github/request-helpers.js';
48
+
49
+ export { buildStoriesEnvelope, parseIds, readNativeBlockedBy, toStoryRecord };
50
+
51
+ /**
52
+ * Bounded concurrency for the per-issue round-trips. Matches the edge-writer's
53
+ * cap: modest enough for GitHub's secondary rate limits, while collapsing
54
+ * wall-clock from sum(round-trips) toward sum/concurrency.
55
+ */
56
+ const FETCH_CONCURRENCY = 5;
57
+
58
+ const HELP = `\
59
+ Usage:
60
+ resolve-stories.js --ids <n,n,...> [--pretty] [--no-native]
61
+
62
+ Resolve Story ids into the { stories, dag, done } envelope /deliver sequences
63
+ from. Dependencies are discovered from live state: body edges union native
64
+ blocked_by edges, with every blocker (in-set or foreign) resolved against its
65
+ real issue state.
66
+
67
+ Options:
68
+ --ids <csv> Comma-separated Story issue numbers. Required.
69
+ --pretty Pretty-print the JSON envelope.
70
+ --no-native Skip the native blocked_by read (body edges only).
71
+ --help Show this help.
72
+ `;
73
+
74
+ /**
75
+ * @param {object} [deps]
76
+ * @returns {{ provider: object, config: object }}
77
+ */
78
+ export function resolveStoriesProvider({
79
+ resolveConfigFn = resolveConfig,
80
+ createProviderFn = createProvider,
81
+ } = {}) {
82
+ const config = resolveConfigFn();
83
+ return { provider: createProviderFn(config), config };
84
+ }
85
+
86
+ /**
87
+ * Fetch every requested id and map it to a Story record, failing on the first
88
+ * id that is not a deliverable Story.
89
+ *
90
+ * @param {object} provider
91
+ * @param {number[]} ids
92
+ * @returns {Promise<object[]>}
93
+ */
94
+ export async function fetchStories(provider, ids) {
95
+ return concurrentMap(
96
+ ids,
97
+ async (id) => {
98
+ const issue = await provider.getTicket(id);
99
+ if (!issue) {
100
+ throw new Error(`[resolve-stories] Issue #${id} was not found.`);
101
+ }
102
+ return toStoryRecord(issue, id);
103
+ },
104
+ { concurrency: FETCH_CONCURRENCY },
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Read native blocked_by edges for every Story in the set.
110
+ *
111
+ * @returns {Promise<Map<number, number[]>>}
112
+ */
113
+ export async function readNativeEdges({ provider, stories, owner, repo }) {
114
+ const entries = await concurrentMap(
115
+ stories,
116
+ async (story) => [
117
+ story.id,
118
+ await readNativeBlockedBy({
119
+ gh: provider._gh,
120
+ owner,
121
+ repo,
122
+ issueNumber: story.id,
123
+ parseJson: parseApiJson,
124
+ }),
125
+ ],
126
+ { concurrency: FETCH_CONCURRENCY },
127
+ );
128
+ return new Map(entries);
129
+ }
130
+
131
+ /**
132
+ * Resolve dependency ids that are NOT in the requested set against live issue
133
+ * state. A foreign blocker that already landed must enter `done[]`, or the
134
+ * scheduler withholds its dependent forever — the exact wedge that made
135
+ * cross-run delivery impossible.
136
+ *
137
+ * A foreign id that cannot be read is left OUT of `done[]`: unknown means
138
+ * "still gating", which withholds the dependent rather than dispatching it
139
+ * against a possibly-unlanded blocker.
140
+ *
141
+ * @returns {Promise<number[]>}
142
+ */
143
+ export async function resolveForeignDone({ provider, dag, inSetIds }) {
144
+ const foreign = [
145
+ ...new Set(
146
+ dag.flatMap((node) => node.dependsOn).filter((dep) => !inSetIds.has(dep)),
147
+ ),
148
+ ];
149
+ if (foreign.length === 0) return [];
150
+ const resolved = await concurrentMap(
151
+ foreign,
152
+ async (id) => {
153
+ try {
154
+ const issue = await provider.getTicket(id);
155
+ return isSatisfiedBlocker(issue) ? id : null;
156
+ } catch (err) {
157
+ Logger.warn(
158
+ `[resolve-stories] Could not read foreign blocker #${id} (${err?.message ?? err}) — ` +
159
+ `treating it as still gating.`,
160
+ );
161
+ return null;
162
+ }
163
+ },
164
+ { concurrency: FETCH_CONCURRENCY },
165
+ );
166
+ return resolved.filter((id) => id !== null);
167
+ }
168
+
169
+ async function main() {
170
+ const { values } = parseArgs({
171
+ options: {
172
+ ids: { type: 'string' },
173
+ pretty: { type: 'boolean', default: false },
174
+ native: { type: 'boolean', default: true },
175
+ help: { type: 'boolean', default: false },
176
+ },
177
+ // The documented opt-out is `--no-native`; without allowNegative,
178
+ // parseArgs rejects it as an unknown option and the CLI has no working
179
+ // way to skip the dependencies API.
180
+ allowNegative: true,
181
+ allowPositionals: false,
182
+ });
183
+
184
+ if (values.help) {
185
+ process.stdout.write(HELP);
186
+ return 0;
187
+ }
188
+ if (!values.ids) {
189
+ process.stderr.write(HELP);
190
+ throw new Error('[resolve-stories] --ids <n,n,...> is required');
191
+ }
192
+
193
+ // stdout is a JSON stream — keep human-readable output on stderr so a
194
+ // headless caller can pipe this straight into stories-wave-tick.js.
195
+ routeAllOutputToStderr();
196
+
197
+ const ids = parseIds(values.ids);
198
+ const { provider, config } = resolveStoriesProvider();
199
+ const owner = config.github?.owner;
200
+ const repo = config.github?.repo;
201
+
202
+ const stories = await fetchStories(provider, ids);
203
+ const nativeEdges = values.native
204
+ ? await readNativeEdges({ provider, stories, owner, repo })
205
+ : new Map();
206
+
207
+ const inSetIds = new Set(stories.map((s) => s.id));
208
+ const provisional = buildStoriesEnvelope({
209
+ stories,
210
+ nativeEdges,
211
+ warn: (m) => Logger.warn(m),
212
+ });
213
+ const foreignDone = await resolveForeignDone({
214
+ provider,
215
+ dag: provisional.dag,
216
+ inSetIds,
217
+ });
218
+ const envelope = buildStoriesEnvelope({
219
+ stories,
220
+ nativeEdges,
221
+ foreignDone,
222
+ warn: () => {},
223
+ });
224
+
225
+ process.stdout.write(
226
+ values.pretty
227
+ ? `${JSON.stringify(envelope, null, 2)}\n`
228
+ : `${JSON.stringify(envelope)}\n`,
229
+ );
230
+ return 0;
231
+ }
232
+
233
+ runAsCli(import.meta.url, main, {
234
+ source: 'resolve-stories',
235
+ propagateExitCode: true,
236
+ });
@@ -45,6 +45,7 @@ import { fileURLToPath } from 'node:url';
45
45
 
46
46
  import { cleanupRepoTestTempArtifacts } from './cleanup-repo-test-temp.js';
47
47
  import { C8_CLI } from './lib/c8-cli-path.js';
48
+ import { buildWebhookSafeTestEnv } from './lib/test-env.js';
48
49
  import { TEST_RUNNER_FLAGS } from './run-tests.js';
49
50
 
50
51
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -89,7 +90,9 @@ function runCoveragePipeline() {
89
90
  const testRun = spawnSync(process.execPath, buildCoverageTestArgs(), {
90
91
  cwd: ROOT,
91
92
  stdio: 'inherit',
92
- env: { ...process.env, NODE_V8_COVERAGE: V8_TMP },
93
+ // GIT_*-scrubbed: under a husky pre-push from a linked worktree the
94
+ // inherited GIT_DIR poisons fixture `git init` runs (#4580).
95
+ env: { ...buildWebhookSafeTestEnv(process.env), NODE_V8_COVERAGE: V8_TMP },
93
96
  });
94
97
 
95
98
  cleanupRepoTestTempArtifacts({ repoRoot: ROOT });
@@ -73,8 +73,8 @@ const tasks = [
73
73
  // spans for axis-shaped tokens (`type/epic`, etc.) and asserts
74
74
  // only the canonical `<axis>::<value>` separator from
75
75
  // `lib/label-constants.js` appears. Closes the drift gap that
76
- // let the original `type/epic` typo land at
77
- // `.agents/workflows/helpers/plan-epic.md:49`.
76
+ // let the original `type/epic` typo land in a since-retired
77
+ // planning workflow helper.
78
78
  name: 'label-vocabulary',
79
79
  cmd: 'node',
80
80
  args: ['.agents/scripts/lint-label-vocabulary.js'],
@@ -5,14 +5,28 @@
5
5
  * Local full verification — a true CI mirror for the gates that CAN be proven
6
6
  * locally, without epic-scoped MI projection or push semantics.
7
7
  *
8
- * Order: audit (SCA) → lint (includes docs:check) full test suite
9
- * unified baselines.
8
+ * Order: audit (SCA) → lint (includes docs:check + the arch-cycles ratchet)
9
+ * full test suite → unified baselines → the dead-exports and context-budget
10
+ * ratchets.
10
11
  *
11
12
  * The `audit` step runs `npm audit --audit-level=high`, matching CI's
12
13
  * "Dependency Vulnerability Audit (SCA)" gate so a local green no longer hides
13
14
  * a high-severity advisory that CI would fail on. It is independent of the
14
15
  * pre-push `PREPUSH_AUDIT` opt-in, which stays unchanged.
15
16
  *
17
+ * The trailing ratchets complete the mirror of CI's "Architecture Cycle Check"
18
+ * step in the `baselines` job (Story #4549). That step runs three checks;
19
+ * `check-arch-cycles.js` is deliberately absent from STEPS because the `lint`
20
+ * step above already runs it (see run-lint.js) — re-running it here would
21
+ * double-pay a gate verify already covers. `check-dead-exports.js` and
22
+ * `check-context-budget.js` had no such cover: they were reachable locally only
23
+ * via the diff-scoped `npm run quality:preview` or a direct invocation, so a
24
+ * clean `verify` could still hide a CI-red — the failure Story #4531 / PR #4548
25
+ * paid for with a full push → CI → fix → push round-trip. Both are pure-Node
26
+ * and baseline-aware, adding ~15s on a cold cache (dominated by knip's
27
+ * full-tree scan in check-dead-exports.js) to a command that already carries
28
+ * the full test suite.
29
+ *
16
30
  * A handful of CI gates cannot be reproduced by this command (action pinning,
17
31
  * TruffleHog secret scan, the BASELINE_SCOPE=full push-scoped maintainability
18
32
  * run) — those are catalogued in docs/ci-contract.md.
@@ -34,6 +48,21 @@ const STEPS = [
34
48
  cmd: 'node',
35
49
  args: ['.agents/scripts/check-baselines.js'],
36
50
  },
51
+ {
52
+ label: 'dead-exports',
53
+ cmd: 'node',
54
+ args: ['.agents/scripts/check-dead-exports.js'],
55
+ },
56
+ {
57
+ label: 'dead-exports-production',
58
+ cmd: 'node',
59
+ args: ['.agents/scripts/check-dead-exports.js', '--production'],
60
+ },
61
+ {
62
+ label: 'context-budget',
63
+ cmd: 'node',
64
+ args: ['.agents/scripts/check-context-budget.js'],
65
+ },
37
66
  ];
38
67
 
39
68
  export function runVerifySteps({
@@ -174,13 +174,13 @@ function describeEvent(evt) {
174
174
  * `process.stdout.write`). Logger is intentionally not used: the
175
175
  * viewer's contract is "dumb terminal compatible, parseable output".
176
176
  *
177
- * @param {{ epic: number | null, stories: Array<object> }} tree
177
+ * @param {{ run: number | null, stories: Array<object> }} tree
178
178
  * @param {{ storyFilter?: number | null }} [opts]
179
179
  * @returns {void}
180
180
  */
181
181
  export function renderTree(tree, opts = {}) {
182
182
  const filter = opts.storyFilter ?? null;
183
- println(`Run #${tree.epic ?? '?'}`);
183
+ println(`Run #${tree.run ?? '?'}`);
184
184
  const stories =
185
185
  filter == null ? tree.stories : tree.stories.filter((s) => s.id === filter);
186
186
 
@@ -231,10 +231,10 @@ export async function main(argv, deps = {}) {
231
231
  const buildSpanTree = deps.buildSpanTree ?? signals.buildSpanTree;
232
232
  const config = buildConfig(tempRoot);
233
233
 
234
- // The reader API still accepts the historical `epic` key while resolving
235
- // the run directory through temp-paths (`temp/run-<id>/`).
234
+ // The reader resolves the run directory through temp-paths
235
+ // (`temp/run-<id>/`) keyed by the `run` id.
236
236
  const iter = read(
237
- story != null ? { epic: runId, story, config } : { epic: runId, config },
237
+ story != null ? { run: runId, story, config } : { run: runId, config },
238
238
  );
239
239
 
240
240
  // Eagerly collect to detect the missing-file case before we start
@@ -267,11 +267,10 @@ export async function main(argv, deps = {}) {
267
267
  }
268
268
 
269
269
  // Pin the run id on the tree to the requested one — `buildSpanTree`
270
- // stores it in the legacy `epic` field from the first observed event,
271
- // which is normally the same, but if every event lacks that field the
272
- // tree's `epic` would be
273
- // `null` while we still know what was requested.
274
- if (tree.epic == null) tree = { ...tree, epic: runId };
270
+ // stores it in the `run` field from the first observed event, which is
271
+ // normally the same, but if every event lacks that field the tree's
272
+ // `run` would be `null` while we still know what was requested.
273
+ if (tree.run == null) tree = { ...tree, run: runId };
275
274
 
276
275
  renderTree(tree, { storyFilter: story });
277
276
  return 0;
@@ -21,14 +21,17 @@
21
21
  * the post-merge confirmation step,
22
22
  * `single-story-confirm-merge.js`)
23
23
  * 8. worktree-reap — drop the per-Story worktree
24
- * 9. confirm-merge — Story #4428, headless-only (`--wait-merge`):
24
+ * 9. confirm-merge — close-and-land (Story #4428; the DEFAULT for
25
+ * every run since `delivery.routing.closeAndLand`):
25
26
  * poll the just-armed PR to merge confirmation
26
- * (reusing `confirmStoryMerged`) or terminate
27
- * `agent::blocked` with a classified
28
- * `merge.unlanded` lifecycle event. Attended runs
29
- * (the default, no `--wait-merge`) skip this
30
- * phase entirely and keep resting at
31
- * `agent::closing`, exactly as before.
27
+ * (reusing `confirmStoryMerged`), capture the
28
+ * Story follow-ups, or terminate `agent::blocked`
29
+ * with a classified `merge.unlanded` event or
30
+ * `merge.flip-failed` when the merge landed and
31
+ * only the label write failed. Skipped when the
32
+ * operator owns the merge (`--no-wait-merge`,
33
+ * `--no-auto-merge`, or `autoMerge: "strict"`),
34
+ * which rests at `agent::closing` for the human.
32
35
  *
33
36
  * Existing tests import the re-exported helpers
34
37
  * (`runSingleStoryClose`, `ensurePullRequest`, `parsePrNumber`,
@@ -38,24 +41,55 @@
38
41
  * Usage:
39
42
  * node single-story-close.js --story <STORY_ID> [--cwd <main-repo>]
40
43
  * [--skip-validation] [--skip-sync]
41
- * [--no-auto-merge] [--no-full-scope-crap]
44
+ * [--no-auto-merge]
42
45
  * [--wait-merge | --no-wait-merge]
43
46
  *
44
- * `--wait-merge` is the headless must-land signal (Story #4428, Epic
45
- * #4425): the invoking surface (a headless `/deliver` run or a CI-driven
46
- * wrapper) opts in explicitly attended runs never pass it, so the default
47
- * exit shape (rest at `agent::closing`, issue OPEN) is unchanged.
48
- * `--no-wait-merge` is the explicit opt-out that always wins over
49
- * `--wait-merge`, for a caller that wants to manage merge confirmation
50
- * externally even in an otherwise headless context.
47
+ * Close-and-land is the DEFAULT for every run (Story #4428 introduced it as
48
+ * `--wait-merge`; `delivery.routing.closeAndLand` default `true` made it
49
+ * the default, and Story #4539 made that knob actually readable). Resolution
50
+ * order, highest first: `--no-wait-merge` (explicit opt-out, always wins);
51
+ * operator-owns-the-merge (`--no-auto-merge` or `delivery.ci.autoMerge:
52
+ * "strict"` the PR was deliberately left un-armed, so there is nothing to
53
+ * land and the Story rests at `agent::closing`); explicit `--wait-merge`;
54
+ * then the config. A genuine arm FAILURE is not an opt-out — it still waits
55
+ * and therefore still blocks, which is what keeps the must-land contract
56
+ * intact.
51
57
  *
52
- * Exit codes: 0 ok, 1 error (including a headless `--wait-merge` run that
53
- * gave up without a confirmed merge — see phase 9 above).
58
+ * Every invocation emits ONE schema-validated terminal envelope
59
+ * (`.agents/schemas/story-deliver-terminal.schema.json`, Story #4543) on
60
+ * stdout between `--- STORY DELIVER TERMINAL ---` markers. Its `status` is
61
+ * the contract; the exit code mirrors it:
62
+ *
63
+ * 0 — `landed`: the PR merged, the Story is `agent::done`, and the
64
+ * post-land tail ran (follow-ups, status resync, local ref
65
+ * cleanup, base fast-forward).
66
+ * 3 — `pending`: RESUMABLE, not a failure. Either the per-invocation merge
67
+ * wait (`delivery.mergeWatch.maxWaitSeconds`, default 300s
68
+ * to fit a single host tool invocation) expired with the PR
69
+ * still healthy and in flight, or the operator owns the
70
+ * merge (`--no-wait-merge` / `--no-auto-merge` /
71
+ * `autoMerge: "strict"`). NO label was mutated and no
72
+ * `merge.unlanded` event was emitted. The envelope's
73
+ * `nextCommand` names the single command that resumes it,
74
+ * and the cumulative budget is anchored at the PR's
75
+ * createdAt so the resume does not restart the clock.
76
+ * 1 — `blocked` or `failed`: a classified hard block (the Story carries
77
+ * `agent::blocked` and a friction comment) or a phase crash.
78
+ *
79
+ * The distinct `pending` code is the point: before it, a close-and-land whose
80
+ * CI outlived the host's tool-invocation ceiling was killed mid-poll with no
81
+ * terminal path taken at all, and merely shrinking the budget instead would
82
+ * have misfiled every slow-CI run as a hard block.
54
83
  *
55
84
  * @see .agents/workflows/helpers/deliver-story.md
85
+ * @see .agents/schemas/story-deliver-terminal.schema.json
56
86
  */
57
87
 
88
+ import { parseSprintArgs } from './lib/cli-args.js';
58
89
  import { runAsCli } from './lib/cli-utils.js';
90
+ import { formatCliError } from './lib/error-redactor.js';
91
+ import { Logger } from './lib/Logger.js';
92
+ import { emitTerminalFriction } from './lib/observability/runtime-friction.js';
59
93
  import { enableAutoMergeWith } from './lib/orchestration/single-story-close/phases/auto-merge.js';
60
94
  import {
61
95
  buildSyncFailureCommentBody,
@@ -67,6 +101,12 @@ import {
67
101
  runStoryScopeReview,
68
102
  } from './lib/orchestration/single-story-close/phases/code-review.js';
69
103
  import { ensurePullRequestWith } from './lib/orchestration/single-story-close/phases/pull-request.js';
104
+ import {
105
+ buildTerminalEnvelope,
106
+ emitTerminalEnvelope,
107
+ exitCodeForTerminal,
108
+ NEXT_COMMANDS,
109
+ } from './lib/orchestration/story-deliver-terminal.js';
70
110
 
71
111
  // Story #2990 moved the `gh`-spawn boundary into the `lib/gh-exec.js`
72
112
  // facade (the same shim the `providers/github/` gateways use). The
@@ -94,6 +134,121 @@ export async function runSingleStoryClose(opts) {
94
134
  return mod.runSingleStoryClose(opts);
95
135
  }
96
136
 
97
- runAsCli(import.meta.url, runSingleStoryClose, {
137
+ /**
138
+ * The close pipeline's phase order, as `setPhase` walks it. Only used to
139
+ * decide whether a gate had already run when a later phase died.
140
+ */
141
+ const PHASE_ORDER = Object.freeze([
142
+ 'init',
143
+ 'wrong-tree-guard',
144
+ 'close-validation',
145
+ 'base-sync',
146
+ 'push',
147
+ 'pull-request',
148
+ 'code-review',
149
+ 'auto-merge',
150
+ 'confirm-merge',
151
+ 'post-land',
152
+ 'done',
153
+ ]);
154
+
155
+ /** Each reported gate and the pipeline phase that decides it. */
156
+ const GATE_PHASES = Object.freeze([
157
+ ['validation', 'close-validation'],
158
+ ['baseSync', 'base-sync'],
159
+ ['codeReview', 'code-review'],
160
+ ]);
161
+
162
+ /**
163
+ * Report every gate's outcome for a run that died at `phase`.
164
+ *
165
+ * The schema's contract: "A gate the run skipped … reports `skipped` rather
166
+ * than being omitted, so a missing gate is never mistaken for a passing one."
167
+ * The previous shape named only the gate that died and omitted the rest
168
+ * entirely — exactly the ambiguity the contract forbids.
169
+ *
170
+ * Reconstructed from the phase order, which is sound because the pipeline is
171
+ * strictly sequential: reaching phase N means every gate before it completed.
172
+ * A gate whose phase the run never reached is `skipped`; one the operator
173
+ * turned off via `--skip-validation` / `--skip-sync` is `skipped` too (it did
174
+ * not pass — it never ran).
175
+ *
176
+ * @param {string} phase The phase the run died in.
177
+ * @param {{ skipValidation?: boolean, skipSync?: boolean }} args Parsed CLI args.
178
+ * @returns {Record<string, 'passed'|'failed'|'skipped'>}
179
+ */
180
+ export function gatesForFailedPhase(phase, args = {}) {
181
+ const skipped = { validation: args.skipValidation, baseSync: args.skipSync };
182
+ const failedAt = PHASE_ORDER.indexOf(phase);
183
+ const gates = {};
184
+ for (const [gate, gatePhase] of GATE_PHASES) {
185
+ const at = PHASE_ORDER.indexOf(gatePhase);
186
+ if (gatePhase === phase) gates[gate] = 'failed';
187
+ else if (failedAt < 0 || at > failedAt) gates[gate] = 'skipped';
188
+ else gates[gate] = skipped[gate] ? 'skipped' : 'passed';
189
+ }
190
+ return gates;
191
+ }
192
+
193
+ /**
194
+ * Build the `failed` terminal for a phase that crashed.
195
+ *
196
+ * The runner deliberately throws rather than returning a failure (a red gate
197
+ * must not look like a return value), so without this the most common
198
+ * non-happy ending — a failing close-validation gate — would emit **no
199
+ * envelope at all**, exiting 1 with only a stderr line while the workflow
200
+ * docs promise the agent a `failed` envelope naming the phase. Every close
201
+ * invocation emits exactly one envelope; this is the path that keeps that
202
+ * true when a phase dies.
203
+ *
204
+ * `err.closePhase` is tagged by the runner's phase tracker.
205
+ *
206
+ * @param {unknown} err
207
+ * @returns {object|null} A validated envelope, or null when even the story id
208
+ * is unknown (a usage error — there is nothing to report an envelope about).
209
+ */
210
+ function failedTerminalFor(err) {
211
+ const phase = err?.closePhase ?? 'init';
212
+ const args = parseSprintArgs();
213
+ const storyId = Number(args.storyId);
214
+ if (!Number.isInteger(storyId) || storyId <= 0) return null;
215
+ return buildTerminalEnvelope({
216
+ storyId,
217
+ status: 'failed',
218
+ phase,
219
+ gates: gatesForFailedPhase(phase, args),
220
+ failure: { reason: String(err?.message ?? err) },
221
+ nextCommand: NEXT_COMMANDS.recover(storyId),
222
+ elapsedSeconds: 0,
223
+ });
224
+ }
225
+
226
+ /**
227
+ * CLI entry — resolves the process exit code from the terminal envelope's
228
+ * status rather than from a thrown/not-thrown distinction, so `pending`
229
+ * (resumable) is distinguishable from `blocked` (come look) without parsing
230
+ * stdout.
231
+ */
232
+ async function main() {
233
+ try {
234
+ const outcome = await runSingleStoryClose();
235
+ return exitCodeForTerminal(outcome?.terminal ?? { status: 'failed' });
236
+ } catch (err) {
237
+ const terminal = failedTerminalFor(err);
238
+ if (!terminal) throw err;
239
+ // Mirror runAsCli's default error line (which this catch pre-empts) so the
240
+ // human-facing failure text is unchanged, then emit the envelope.
241
+ Logger.error(`[single-story-close] Fatal error: ${formatCliError(err)}`);
242
+ emitTerminalEnvelope(terminal);
243
+ // Story #4578 — a close that died before the runner could report its own
244
+ // terminal is exactly the friction the retro must see, so the crash path
245
+ // gets the same emit the happy path does. Best-effort; cannot throw.
246
+ await emitTerminalFriction({ envelope: terminal });
247
+ return exitCodeForTerminal(terminal);
248
+ }
249
+ }
250
+
251
+ runAsCli(import.meta.url, main, {
98
252
  source: 'single-story-close',
253
+ propagateExitCode: true,
99
254
  });