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,328 @@
1
+ /**
2
+ * deliver-recover.js — probe a stranded Story and name its ONE next command
3
+ * (Story #4543).
4
+ *
5
+ * Recovery here is a **read, not a repair**. Every input is already
6
+ * probeable — labels and lease from the ticket, the branch locally and via
7
+ * its tracking ref, the worktree path, the PR by head branch (with state,
8
+ * merge, and checks probes) — so this module is probes, a decision table
9
+ * over `label × PR × branch × worktree`, and one command with the evidence it
10
+ * was derived from. It never mutates anything, and it never prints a menu of
11
+ * options: a menu is what an operator already has, and it is the thing they
12
+ * cannot act on.
13
+ *
14
+ * The strand shapes the table resolves, and why each is real:
15
+ *
16
+ * - `executing` with no PR → resume implementation. The work never reached
17
+ * close.
18
+ * - `closing` with a pending PR → resume the land. The overwhelmingly
19
+ * common shape now that the merge wait is bounded: the wait returned
20
+ * `pending` and something has to pick it back up.
21
+ * - `closing` with a red PR → enter the fix loop. Waiting is pointless; no
22
+ * budget turns a failed check green.
23
+ * - `closing` with a MERGED PR → run confirm. **This is the strand a
24
+ * `/deliver` re-run refuses outright**, because `single-story-init.js`
25
+ * hard-errors on an already-closed Story — so before this surface, the
26
+ * merged-but-label-stale Story had no automated way back.
27
+ * - `done` with a drifted board → run resync. The GitHub Projects bot won
28
+ * the race.
29
+ * - `blocked` → print the class-specific remediation the friction comment
30
+ * already names, rather than inventing a second opinion about a
31
+ * condition that was already classified.
32
+ *
33
+ * The command vocabulary is shared with the terminal envelope
34
+ * (`story-deliver-terminal.js#NEXT_COMMANDS`), so recovery and normal
35
+ * resumption speak one language instead of two dialects for one state.
36
+ */
37
+
38
+ import { gh as defaultGh } from '../gh-exec.js';
39
+ import { gitSpawn as defaultGitSpawn, getStoryBranch } from '../git-utils.js';
40
+ import { deriveChecksStatus } from './merge-poll.js';
41
+ import { NEXT_COMMANDS } from './story-deliver-terminal.js';
42
+ import { STATE_LABELS } from './ticketing.js';
43
+
44
+ /**
45
+ * Probe the ticket: state labels, issue open/closed, and the lease holder.
46
+ *
47
+ * @returns {Promise<object>}
48
+ */
49
+ export async function probeTicket({ provider, storyId }) {
50
+ try {
51
+ const ticket = await provider.getTicket(storyId);
52
+ const labels = Array.isArray(ticket?.labels) ? ticket.labels : [];
53
+ const stateLabel =
54
+ labels.find((l) => typeof l === 'string' && l.startsWith('agent::')) ??
55
+ null;
56
+ return {
57
+ ok: true,
58
+ stateLabel,
59
+ labels,
60
+ issueState: ticket?.state ?? null,
61
+ title: ticket?.title ?? null,
62
+ lease: ticket?.assignees?.[0] ?? ticket?.assignee ?? null,
63
+ };
64
+ } catch (err) {
65
+ return { ok: false, error: String(err?.message ?? err) };
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Probe the Story branch locally and its remote tracking ref, plus the
71
+ * worktree path. Pure git — no network.
72
+ *
73
+ * @returns {object}
74
+ */
75
+ export function probeBranch({ cwd, storyBranch, config, gitSpawnFn }) {
76
+ const spawn = gitSpawnFn ?? defaultGitSpawn;
77
+ const localRef = spawn(
78
+ cwd,
79
+ 'show-ref',
80
+ '--verify',
81
+ '--quiet',
82
+ `refs/heads/${storyBranch}`,
83
+ );
84
+ const remoteRef = spawn(
85
+ cwd,
86
+ 'show-ref',
87
+ '--verify',
88
+ '--quiet',
89
+ `refs/remotes/origin/${storyBranch}`,
90
+ );
91
+ const worktreeRoot =
92
+ config?.delivery?.worktreeIsolation?.root ?? '.worktrees';
93
+ const worktrees = spawn(cwd, 'worktree', 'list', '--porcelain');
94
+ const worktreePath =
95
+ worktrees.status === 0 &&
96
+ typeof worktrees.stdout === 'string' &&
97
+ worktrees.stdout.includes(`${worktreeRoot}/${storyBranch}`)
98
+ ? `${worktreeRoot}/${storyBranch}`
99
+ : null;
100
+ return {
101
+ local: localRef.status === 0,
102
+ remote: remoteRef.status === 0,
103
+ worktreePath,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Probe the PR for the Story branch. `--state all` is required: a merged PR
109
+ * is no longer `open`, and the merged-but-label-stale strand is precisely
110
+ * the one that matters most here.
111
+ *
112
+ * @returns {Promise<object|null>}
113
+ */
114
+ export async function probePr({ storyBranch, gh = defaultGh }) {
115
+ try {
116
+ const rows = await gh.pr.list(
117
+ ['--head', storyBranch, '--state', 'all'],
118
+ ['number', 'url', 'state', 'mergedAt', 'statusCheckRollup'],
119
+ );
120
+ if (!Array.isArray(rows) || rows.length === 0) return null;
121
+ const row = rows[0];
122
+ return {
123
+ number: Number(row?.number) || null,
124
+ url: row?.url ?? null,
125
+ state: row?.state ?? null,
126
+ mergedAt: row?.mergedAt ?? null,
127
+ checksStatus: deriveChecksStatus(row?.statusCheckRollup),
128
+ };
129
+ } catch (err) {
130
+ return { error: String(err?.message ?? err) };
131
+ }
132
+ }
133
+
134
+ /**
135
+ * The decision table. Pure: every input is an already-observed probe, so the
136
+ * mapping is testable without git, GitHub, or a clock.
137
+ *
138
+ * Returns exactly one `{ shape, nextCommand, evidence[], detail }` — never a
139
+ * list of candidates.
140
+ *
141
+ * @param {{ storyId: number, ticket: object, branch: object, pr: object|null }} probes
142
+ * @returns {{ shape: string, nextCommand: string|null, detail: string, evidence: string[] }}
143
+ */
144
+ export function decideRecovery({ storyId, ticket, branch, pr }) {
145
+ const evidence = [
146
+ `label=${ticket?.stateLabel ?? 'none'}`,
147
+ `issue=${ticket?.issueState ?? 'unknown'}`,
148
+ `pr=${pr?.number ? `#${pr.number} ${pr.state ?? '?'}` : 'none'}`,
149
+ `checks=${pr?.checksStatus ?? 'n/a'}`,
150
+ `branch.local=${branch?.local ?? false}`,
151
+ `branch.remote=${branch?.remote ?? false}`,
152
+ `worktree=${branch?.worktreePath ?? 'none'}`,
153
+ `lease=${ticket?.lease ?? 'unclaimed'}`,
154
+ ];
155
+
156
+ const label = ticket?.stateLabel;
157
+ const merged = pr?.state === 'MERGED' || Boolean(pr?.mergedAt);
158
+
159
+ // A merged PR outranks every label reading. The code is on the base
160
+ // branch; whatever the label says, the only thing left is the flip + tail.
161
+ if (merged && label !== STATE_LABELS.DONE) {
162
+ return {
163
+ shape: 'merged-label-stale',
164
+ nextCommand: NEXT_COMMANDS.confirmMerge(storyId),
165
+ detail:
166
+ `PR #${pr.number} is MERGED but the Story is at \`${label ?? 'no state label'}\`. ` +
167
+ `A /deliver re-run cannot fix this — single-story-init.js hard-errors on an ` +
168
+ `already-closed Story. The confirm CLI is idempotent and flips the label from ` +
169
+ `the already-merged PR, then runs the land tail.`,
170
+ evidence,
171
+ };
172
+ }
173
+
174
+ if (label === STATE_LABELS.BLOCKED) {
175
+ return {
176
+ shape: 'blocked',
177
+ nextCommand: NEXT_COMMANDS.recover(storyId),
178
+ detail:
179
+ `Story is at \`agent::blocked\`. The block was already classified when it was ` +
180
+ `filed — read the \`friction\` comment on #${storyId} for the class-specific ` +
181
+ `remediation, resolve it, then transition back to \`agent::executing\`. ` +
182
+ `Re-run this probe afterwards to confirm the strand cleared.`,
183
+ evidence,
184
+ };
185
+ }
186
+
187
+ if (label === STATE_LABELS.DONE) {
188
+ return {
189
+ shape: 'done-board-drift',
190
+ nextCommand: NEXT_COMMANDS.resync(storyId),
191
+ detail:
192
+ `Story is \`agent::done\`. Nothing to deliver. If the Projects board still shows ` +
193
+ `it as In Progress, the GitHub built-in workflow won the post-merge race; the ` +
194
+ `resync re-asserts the column and is a no-op otherwise.`,
195
+ evidence,
196
+ };
197
+ }
198
+
199
+ if (label === STATE_LABELS.CLOSING) {
200
+ if (pr?.checksStatus === 'failure') {
201
+ return {
202
+ shape: 'closing-pr-red',
203
+ nextCommand: NEXT_COMMANDS.watchCi(storyId, pr.number),
204
+ detail:
205
+ `PR #${pr.number} has a red required check. Waiting cannot help — fix the ` +
206
+ `failure and push a new commit on \`story-${storyId}\`; auto-merge stays armed ` +
207
+ `across retries.`,
208
+ evidence,
209
+ };
210
+ }
211
+ if (pr?.number) {
212
+ return {
213
+ shape: 'closing-pr-pending',
214
+ nextCommand: NEXT_COMMANDS.resumeLand(storyId),
215
+ detail:
216
+ `PR #${pr.number} is open and healthy. This is the normal resumable shape after ` +
217
+ `a bounded merge wait returned \`pending\`. The confirm CLI polls it to a ` +
218
+ `confirmed merge and runs the land tail.`,
219
+ evidence,
220
+ };
221
+ }
222
+ return {
223
+ shape: 'closing-no-pr',
224
+ nextCommand: NEXT_COMMANDS.close(storyId),
225
+ detail:
226
+ `Story is at \`agent::closing\` but no PR exists for \`story-${storyId}\`. The ` +
227
+ `close did not reach the pull-request phase; re-run it (close is idempotent and ` +
228
+ `reuses an existing PR when one is found).`,
229
+ evidence,
230
+ };
231
+ }
232
+
233
+ if (label === STATE_LABELS.EXECUTING) {
234
+ if (pr?.number) {
235
+ return {
236
+ shape: 'executing-with-pr',
237
+ nextCommand: NEXT_COMMANDS.close(storyId),
238
+ detail:
239
+ `PR #${pr.number} exists but the Story is still \`agent::executing\` — the close ` +
240
+ `opened the PR and then died before the label flip. Re-run close; it reuses the ` +
241
+ `open PR rather than opening a duplicate.`,
242
+ evidence,
243
+ };
244
+ }
245
+ return {
246
+ shape: 'executing-no-pr',
247
+ nextCommand: NEXT_COMMANDS.implement(storyId),
248
+ detail:
249
+ `Story is \`agent::executing\` with no PR. Implementation never finished. Re-init ` +
250
+ `(idempotent — it reuses the existing branch and worktree) and resume in the ` +
251
+ `worktree it prints.`,
252
+ evidence,
253
+ };
254
+ }
255
+
256
+ return {
257
+ shape: 'ready',
258
+ nextCommand: NEXT_COMMANDS.close(storyId),
259
+ detail:
260
+ `Story is at \`${label ?? 'no agent:: state label'}\` — not mid-delivery, so there ` +
261
+ `is no strand to recover. Deliver it normally via /deliver ${storyId}.`,
262
+ evidence,
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Probe live state and resolve the single next command. Read-only.
268
+ *
269
+ * @param {object} args
270
+ * @param {number} args.storyId
271
+ * @param {string} args.cwd
272
+ * @param {object} args.provider
273
+ * @param {object} [args.config]
274
+ * @param {object} [args.gh]
275
+ * @param {Function} [args.gitSpawnFn]
276
+ * @returns {Promise<object>}
277
+ */
278
+ export async function recoverStory({
279
+ storyId,
280
+ cwd,
281
+ provider,
282
+ config,
283
+ gh = defaultGh,
284
+ gitSpawnFn,
285
+ }) {
286
+ const storyBranch = getStoryBranch(storyId);
287
+ const ticket = await probeTicket({ provider, storyId });
288
+ if (!ticket.ok) {
289
+ throw new Error(
290
+ `deliver-recover: could not read Story #${storyId}: ${ticket.error}`,
291
+ );
292
+ }
293
+ const branch = probeBranch({ cwd, storyBranch, config, gitSpawnFn });
294
+ const pr = await probePr({ storyBranch, gh });
295
+ const decision = decideRecovery({ storyId, ticket, branch, pr });
296
+ return {
297
+ storyId,
298
+ storyBranch,
299
+ probes: { ticket, branch, pr },
300
+ ...decision,
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Render the operator-facing report: the shape, the one command, and the
306
+ * evidence it was derived from — so the operator can check the reasoning
307
+ * rather than trust it.
308
+ *
309
+ * @param {object} recovery
310
+ * @returns {string}
311
+ */
312
+ export function renderRecovery(recovery) {
313
+ const lines = [
314
+ `Story #${recovery.storyId} — ${recovery.shape}`,
315
+ '',
316
+ recovery.detail,
317
+ '',
318
+ 'Evidence:',
319
+ ...recovery.evidence.map((e) => ` - ${e}`),
320
+ '',
321
+ ];
322
+ if (recovery.nextCommand) {
323
+ lines.push('Next command:', ` ${recovery.nextCommand}`, '');
324
+ } else {
325
+ lines.push('Next command: none — nothing to do.', '');
326
+ }
327
+ return lines.join('\n');
328
+ }
@@ -1,7 +1,12 @@
1
1
  /**
2
- * detectors-phase.js — pure post-merge phase that runs the per-Story
3
- * signal detectors (rework + retry) before `analyze-execution.js` shells
4
- * out to render the `<!-- structured:story-perf-summary -->` comment.
2
+ * detectors-phase.js — pure phase that runs the per-Story signal detectors
3
+ * (rework + retry) over the local NDJSON stream.
4
+ *
5
+ * Story #4545 — this phase has no production caller. Its sequencer
6
+ * (`post-merge-pipeline.js`) went in the v2.0.0 cutover, and the
7
+ * `analyze-execution.js` consumer it fed was deleted with the
8
+ * execution-analysis surface. Kept alive only by its own test — the
9
+ * test-importer blind spot the dead-exports ratchet cannot see.
5
10
  *
6
11
  * Extracted from `post-merge-pipeline.js` (Story #1770 / Task #1779) to
7
12
  * keep the parent sequencer's maintainability score above its baseline
@@ -39,8 +44,9 @@
39
44
  * @module lib/orchestration/detectors-phase
40
45
  */
41
46
 
47
+ import path from 'node:path';
42
48
  import { getSignals } from '../config/limits.js';
43
- import { storyArtifactPath } from '../config/temp-paths.js';
49
+ import { storyTempDir } from '../config/temp-paths.js';
44
50
  import { Logger } from '../Logger.js';
45
51
  import { appendSignal } from '../observability/signals-writer.js';
46
52
  import { detectRetry, detectRework } from '../signals/detectors/index.js';
@@ -114,7 +120,7 @@ function isValidIdPair(eid, sid) {
114
120
 
115
121
  /**
116
122
  * Run the rework + retry detectors for one Story and persist every
117
- * emission to `temp/epic-<eid>/stories/story-<sid>/signals.ndjson`.
123
+ * emission to `temp/run-<id>/stories/story-<sid>/signals.ndjson`.
118
124
  *
119
125
  * @param {{
120
126
  * epicId: number|string,
@@ -159,7 +165,7 @@ export async function detectorsPhase(ctx) {
159
165
  return { rework: 0, retry: 0 };
160
166
  }
161
167
 
162
- const tracesPath = storyArtifactPath(eid, sid, 'traces.ndjson', config);
168
+ const tracesPath = path.join(storyTempDir(eid, sid, config), 'traces.ndjson');
163
169
  const taskId = resolveLastTaskId(tasks);
164
170
  const baseArgs = { tracesPath, epicId: eid, storyId: sid, taskId };
165
171
  const common = {
@@ -59,6 +59,36 @@ export function planFastForward(ctx) {
59
59
  return { runnable: true, behind: ff.behind, currentBranch: cur };
60
60
  }
61
61
 
62
+ /**
63
+ * Return HEAD to the branch it started on.
64
+ *
65
+ * {@link maybeCheckout} moves the checkout to `baseBranch` so `merge
66
+ * --ff-only` has somewhere to land. Leaving it there silently relocates the
67
+ * operator: this phase runs from the MAIN checkout — which may be parked on
68
+ * unrelated work — and the close tail can reach it long after the operator
69
+ * walked away (a belated `single-story-confirm-merge --wait`, for instance).
70
+ * Fast-forwarding the base branch is the contract every delivering flow owes
71
+ * the checkout; moving someone off the branch they were using is not, and it
72
+ * is the kind of surprise that gets subsequent work committed to the wrong
73
+ * branch.
74
+ *
75
+ * Best-effort: a failed restore warns and never fails the phase — the
76
+ * fast-forward already succeeded, and `planFastForward` refuses on a dirty
77
+ * tree, so nothing uncommitted is ever at risk here.
78
+ */
79
+ function restoreBranchIfMoved({ plan, baseBranch, cwd, checkoutFn, logger }) {
80
+ const original = plan.currentBranch;
81
+ if (!original || original === baseBranch) return;
82
+ const co = checkoutFn(cwd, original);
83
+ if (!co.ok) {
84
+ logger.warn?.(
85
+ `${TAG} ⚠️ checkout left on ${baseBranch}: restoring ${original} failed: ${co.stderr}`,
86
+ );
87
+ return;
88
+ }
89
+ logger.info?.(`${TAG} ↩️ restored checkout to ${original}`);
90
+ }
91
+
62
92
  function maybeCheckout({ plan, baseBranch, cwd, checkoutFn, logger }) {
63
93
  if (!plan.currentBranch || plan.currentBranch === baseBranch) {
64
94
  return { ok: true };
@@ -108,6 +138,9 @@ export function executeFastForward(ctx) {
108
138
  logger.warn?.(
109
139
  `${TAG} ❌ merge --ff-only ${ref} failed: ${mergeRes.stderr}`,
110
140
  );
141
+ // We already moved HEAD to run the merge; put it back even on the
142
+ // failure path rather than stranding the operator on the base branch.
143
+ restoreBranchIfMoved({ plan, baseBranch, cwd, checkoutFn, logger });
111
144
  return {
112
145
  ok: false,
113
146
  applied: false,
@@ -119,5 +152,6 @@ export function executeFastForward(ctx) {
119
152
  logger.info?.(
120
153
  `${TAG} ✅ fast-forwarded ${baseBranch} by ${plan.behind} commit(s)`,
121
154
  );
155
+ restoreBranchIfMoved({ plan, baseBranch, cwd, checkoutFn, logger });
122
156
  return { ok: true, applied: true, skipped: false, behind: plan.behind };
123
157
  }
@@ -2,8 +2,9 @@
2
2
  * lease-guard-shared.js — Story #3992: single-source the lease-acquisition
3
3
  * kernel shared by the three per-surface lease guards.
4
4
  *
5
- * `epic-deliver-lease-guard.js`, `epic-plan-lease-guard.js`, and
6
- * `single-story-lease-guard.js` historically each carried their own copy of
5
+ * The per-surface lease guards — today `single-story-lease-guard.js`;
6
+ * historically also the retired Epic-tier deliver/plan guards —
7
+ * each carried their own copy of
7
8
  * the operator-handle resolution and the fail-closed acquire wrapper around
8
9
  * `ticket-lease.acquireLease` (anchor `heartbeatAt` to `now` so a foreign
9
10
  * assignee always reads as a live claim, then throw an operator-facing
@@ -0,0 +1,142 @@
1
+ /**
2
+ * emit-ledger-event.js — shared validate-and-append core for the
3
+ * merge-terminal lifecycle emitters (Story #4539).
4
+ *
5
+ * `emit-merge-unlanded.js` and `emit-merge-flip-failed.js` report the two
6
+ * terminal outcomes a landing attempt can reach without ending at
7
+ * `agent::done`. They share everything except their schema and their
8
+ * payload's meaning, so the Ajv compile/cache, the scope→ledger routing,
9
+ * and the NDJSON append live here once rather than being copy-pasted.
10
+ *
11
+ * Like its callers, this is a bare `appendFileSync` rather than a bus
12
+ * publish: these events fire from the `single-story-close` flow, which has
13
+ * no bus at all, so a direct append keeps the call site dependency-free.
14
+ */
15
+
16
+ import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
17
+ import path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ import Ajv2020 from 'ajv/dist/2020.js';
21
+ import addFormats from 'ajv-formats';
22
+
23
+ import { storyLedgerPath } from '../../config/temp-paths.js';
24
+
25
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
+ const SCHEMA_DIR = path.resolve(
27
+ __dirname,
28
+ '..',
29
+ '..',
30
+ '..',
31
+ '..',
32
+ 'schemas',
33
+ 'lifecycle',
34
+ );
35
+
36
+ /**
37
+ * The scopes a merge-terminal event may be WRITTEN with. v2.0.0 removed the
38
+ * Epic tier, so `'story'` is the only emittable scope.
39
+ *
40
+ * Note the deliberate asymmetry with the `merge.unlanded` /
41
+ * `merge.flip-failed` schema enums, which still accept `'epic'`: archived
42
+ * ledger records carry `scope: 'epic'` and the schemas must keep validating
43
+ * them on read. The value stays READABLE; only the writer path is gone —
44
+ * the same split `merge-block-class.js` keeps for `predicate-refused`.
45
+ */
46
+ const VALID_SCOPES = new Set(['story']);
47
+
48
+ /** @type {Map<string, Function>} */
49
+ const _validators = new Map();
50
+
51
+ /**
52
+ * Compile (once) and return the validator for a lifecycle schema file.
53
+ *
54
+ * @param {string} schemaFile Basename under `.agents/schemas/lifecycle/`.
55
+ * @returns {Function}
56
+ */
57
+ function getValidator(schemaFile) {
58
+ const cached = _validators.get(schemaFile);
59
+ if (cached) return cached;
60
+ const schema = JSON.parse(
61
+ readFileSync(path.resolve(SCHEMA_DIR, schemaFile), 'utf8'),
62
+ );
63
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
64
+ addFormats(ajv);
65
+ const validator = ajv.compile(schema);
66
+ _validators.set(schemaFile, validator);
67
+ return validator;
68
+ }
69
+
70
+ /**
71
+ * Assert the fields every merge-terminal event shares. Throws with the
72
+ * caller's own emitter name so the message stays attributable.
73
+ *
74
+ * @param {string} emitter
75
+ * @param {{ scope: string, ticketId: number, prNumber: number, reason: string, elapsedSeconds: number }} fields
76
+ */
77
+ export function assertMergeTerminalFields(
78
+ emitter,
79
+ { scope, ticketId, prNumber, reason, elapsedSeconds },
80
+ ) {
81
+ if (!VALID_SCOPES.has(scope)) {
82
+ throw new Error(
83
+ `${emitter}: scope "${scope}" must be one of: ${[...VALID_SCOPES].join(', ')}`,
84
+ );
85
+ }
86
+ if (!Number.isInteger(ticketId) || ticketId < 1) {
87
+ throw new Error(`${emitter}: ticketId must be a positive integer`);
88
+ }
89
+ if (!Number.isInteger(prNumber) || prNumber < 1) {
90
+ throw new Error(`${emitter}: prNumber must be a positive integer`);
91
+ }
92
+ if (typeof reason !== 'string' || reason.length === 0) {
93
+ throw new Error(`${emitter}: reason must be a non-empty string`);
94
+ }
95
+ if (typeof elapsedSeconds !== 'number' || elapsedSeconds < 0) {
96
+ throw new Error(`${emitter}: elapsedSeconds must be a non-negative number`);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Validate a payload against its lifecycle schema and append exactly one
102
+ * NDJSON record to the scope-resolved ledger.
103
+ *
104
+ * @param {object} args
105
+ * @param {string} args.emitter Caller name, for error attribution.
106
+ * @param {string} args.schemaFile Basename under `schemas/lifecycle/`.
107
+ * @param {object} args.payload The event payload (already assembled).
108
+ * @param {number} args.ticketId
109
+ * @param {string} args.timestamp
110
+ * @param {object} [args.config]
111
+ * @param {string} [args.ledgerPath] Override for tests / non-default layouts.
112
+ * @returns {{ ledgerPath: string, record: object }}
113
+ */
114
+ export function appendLedgerEvent({
115
+ emitter,
116
+ schemaFile,
117
+ payload,
118
+ ticketId,
119
+ timestamp,
120
+ config,
121
+ ledgerPath: ledgerPathOverride,
122
+ }) {
123
+ const validator = getValidator(schemaFile);
124
+ if (!validator(payload)) {
125
+ const detail = (validator.errors ?? [])
126
+ .map((e) => `${e.instancePath || '/'} ${e.message}`)
127
+ .join('; ');
128
+ throw new Error(`${emitter}: payload failed schema validation: ${detail}`);
129
+ }
130
+
131
+ const ledgerPath =
132
+ ledgerPathOverride ?? storyLedgerPath(null, ticketId, config);
133
+ mkdirSync(path.dirname(ledgerPath), { recursive: true });
134
+ const record = {
135
+ kind: 'emitted',
136
+ ts: timestamp,
137
+ event: payload.event,
138
+ payload,
139
+ };
140
+ appendFileSync(ledgerPath, `${JSON.stringify(record)}\n`, 'utf8');
141
+ return { ledgerPath, record };
142
+ }
@@ -9,14 +9,12 @@
9
9
  * appends an inspectable `emitted` line a reconciler can read for
10
10
  * forward-progress evidence.
11
11
  *
12
- * Distinct from `story.heartbeat` (emit-story-heartbeat.js): the
13
- * heartbeat carries Story-phase info for a single in-flight Story and is
14
- * always Epic-scoped (its ledger path is `epicLedgerPath(epicId)`). A
15
- * host loop is not bound to a Story tier, so `loop.tick` carries a
12
+ * A host loop is not bound to a Story tier, so `loop.tick` carries a
16
13
  * free-form `loopName`, a monotonic `round` counter, the loop's
17
- * configured `cadence` label, and a per-round `status` instead. Keeping
18
- * the two events separate means a loop tick never masquerades as Story
19
- * progress (and vice versa).
14
+ * configured `cadence` label, and a per-round `status`. It is now the only
15
+ * forward-progress beat: the Story-scoped `story.heartbeat` it was once
16
+ * contrasted against was Epic-scoped by construction, could never fire under
17
+ * v2 (which has no Epics), and was deleted (A22).
20
18
  *
21
19
  * Bus path (Story acceptance: "Emitting a loop.tick event THROUGH the
22
20
  * lifecycle bus appends a record to the per-run ledger"): this helper
@@ -39,7 +37,7 @@
39
37
  * Ledger path resolution: a caller supplies EITHER an explicit
40
38
  * `ledgerPath` (the host-loop case — the loop owns where its ledger
41
39
  * lives) OR an `epicId`, in which case the canonical
42
- * `epicLedgerPath(epicId)` is used so an Epic-scoped loop's ticks land
40
+ * `runLedgerPath(epicId)` is used so an Epic-scoped loop's ticks land
43
41
  * in the same `temp/run-<id>/lifecycle.ndjson` the rest of the run
44
42
  * reads. Exactly one of the two MUST be supplied.
45
43
  */
@@ -47,7 +45,7 @@
47
45
  import path from 'node:path';
48
46
  import { fileURLToPath } from 'node:url';
49
47
 
50
- import { epicLedgerPath } from '../../config/temp-paths.js';
48
+ import { runLedgerPath } from '../../config/temp-paths.js';
51
49
  import { createBus } from './bus.js';
52
50
  import { createLedgerWriter } from './ledger-writer.js';
53
51
 
@@ -104,7 +102,7 @@ function decomposeLedgerPath(ledgerPath) {
104
102
  * One of running|done|blocked.
105
103
  * @param {string} [opts.timestamp] ISO-8601 wall clock. Defaults to now().
106
104
  * @param {number} [opts.epicId] When supplied (and no `ledgerPath`),
107
- * the canonical `epicLedgerPath(epicId)`
105
+ * the canonical `runLedgerPath(epicId)`
108
106
  * is used for the ledger.
109
107
  * @param {object} [opts.config] Optional resolved config for tempRoot
110
108
  * (only consulted on the `epicId` path).
@@ -158,7 +156,7 @@ export async function emitLoopTick(opts) {
158
156
  if (!Number.isInteger(epicId) || epicId < 1) {
159
157
  throw new Error('emitLoopTick: epicId must be a positive integer');
160
158
  }
161
- ledgerPath = epicLedgerPath(epicId, config);
159
+ ledgerPath = runLedgerPath(epicId, config);
162
160
  }
163
161
 
164
162
  const payload = {