mandrel 1.94.0 → 2.0.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 (393) hide show
  1. package/.agents/README.md +59 -73
  2. package/.agents/agents/story-worker.md +12 -13
  3. package/.agents/docs/SDLC.md +489 -1285
  4. package/.agents/docs/agentrc-reference.json +177 -67
  5. package/.agents/docs/configuration.md +104 -138
  6. package/.agents/docs/execution-reference.md +17 -20
  7. package/.agents/docs/quality-gates.md +13 -19
  8. package/.agents/docs/workflows.md +3 -3
  9. package/.agents/instructions.md +70 -81
  10. package/.agents/rules/ci-remediation.md +8 -12
  11. package/.agents/rules/git-conventions-reference.md +6 -7
  12. package/.agents/rules/git-conventions.md +16 -22
  13. package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
  14. package/.agents/schemas/agentrc.schema.json +54 -214
  15. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  16. package/.agents/schemas/lifecycle/retro.end.schema.json +1 -1
  17. package/.agents/schemas/risk-verdict.schema.json +0 -13
  18. package/.agents/scripts/analyze-execution.js +1 -1
  19. package/.agents/scripts/audit-to-stories.js +7 -7
  20. package/.agents/scripts/boot-sweep.js +1 -1
  21. package/.agents/scripts/check-lifecycle-lint.js +6 -9
  22. package/.agents/scripts/check-prepush-recovery.js +1 -1
  23. package/.agents/scripts/cleanup-repo-test-temp.js +6 -1
  24. package/.agents/scripts/lib/Logger.js +6 -10
  25. package/.agents/scripts/lib/audit-suite/runner.js +2 -2
  26. package/.agents/scripts/lib/audit-suite/selector.js +5 -5
  27. package/.agents/scripts/lib/audit-to-stories/{seed-epic-from-findings.js → seed-from-findings.js} +9 -9
  28. package/.agents/scripts/lib/baselines/kernel.js +206 -18
  29. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -4
  30. package/.agents/scripts/lib/baselines/reader.js +1 -6
  31. package/.agents/scripts/lib/bdd-runner-detect.js +5 -9
  32. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +32 -33
  33. package/.agents/scripts/lib/checks/core-bare-clean.js +2 -2
  34. package/.agents/scripts/lib/checks/index.js +2 -1
  35. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +23 -21
  36. package/.agents/scripts/lib/cli/standard-args.js +13 -22
  37. package/.agents/scripts/lib/cli-args.js +16 -7
  38. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  39. package/.agents/scripts/lib/config/ci.js +6 -31
  40. package/.agents/scripts/lib/config/delivery-routing.js +45 -29
  41. package/.agents/scripts/lib/config/explain.js +55 -36
  42. package/.agents/scripts/lib/config/limits.js +17 -58
  43. package/.agents/scripts/lib/config/paths.js +0 -2
  44. package/.agents/scripts/lib/config/quality.js +1 -1
  45. package/.agents/scripts/lib/config/runners.js +17 -50
  46. package/.agents/scripts/lib/config/temp-paths.js +19 -14
  47. package/.agents/scripts/lib/config/worktree-isolation.js +0 -5
  48. package/.agents/scripts/lib/config-resolver.js +2 -7
  49. package/.agents/scripts/lib/config-settings-schema-delivery.js +24 -148
  50. package/.agents/scripts/lib/config-settings-schema-quality.js +8 -14
  51. package/.agents/scripts/lib/config-settings-schema.js +52 -38
  52. package/.agents/scripts/lib/dependency-parser.js +3 -2
  53. package/.agents/scripts/lib/doc-tiers.js +2 -2
  54. package/.agents/scripts/lib/duplicate-search.js +211 -41
  55. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
  56. package/.agents/scripts/lib/findings/promote-finding.js +5 -5
  57. package/.agents/scripts/lib/framework-version.js +2 -3
  58. package/.agents/scripts/lib/git-branch-cleanup.js +1 -10
  59. package/.agents/scripts/lib/git-branch-lifecycle.js +17 -22
  60. package/.agents/scripts/lib/git-utils.js +32 -6
  61. package/.agents/scripts/lib/github/framework-repo.js +6 -0
  62. package/.agents/scripts/lib/label-constants.js +10 -23
  63. package/.agents/scripts/lib/label-taxonomy.js +9 -43
  64. package/.agents/scripts/lib/observability/active-story-env.js +1 -1
  65. package/.agents/scripts/lib/observability/hook-heartbeat.js +20 -52
  66. package/.agents/scripts/lib/observability/source-classifier.js +3 -3
  67. package/.agents/scripts/lib/onboard/init-tail.js +1 -3
  68. package/.agents/scripts/lib/orchestration/audit-lens-routing.js +128 -0
  69. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +8 -5
  70. package/.agents/scripts/lib/orchestration/ceremony-routing.js +83 -20
  71. package/.agents/scripts/lib/orchestration/code-review.js +20 -268
  72. package/.agents/scripts/lib/orchestration/column-sync.js +1 -1
  73. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +1 -1
  74. package/.agents/scripts/lib/orchestration/context-envelope.js +2 -5
  75. package/.agents/scripts/lib/orchestration/docs-digest.js +8 -8
  76. package/.agents/scripts/lib/orchestration/file-assumptions.js +7 -13
  77. package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
  78. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +8 -8
  79. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +2 -2
  80. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +6 -3
  81. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +17 -43
  82. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +4 -4
  83. package/.agents/scripts/lib/orchestration/merge-block-class.js +1 -1
  84. package/.agents/scripts/lib/orchestration/phase-runner.js +3 -2
  85. package/.agents/scripts/lib/orchestration/plan-context.js +248 -266
  86. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +1 -1
  87. package/.agents/scripts/lib/orchestration/plan-navigation.js +92 -0
  88. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +61 -0
  89. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +97 -0
  90. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +223 -854
  91. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +361 -0
  92. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +35 -108
  93. package/.agents/scripts/lib/orchestration/plan-reachability.js +9 -14
  94. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +1 -1
  95. package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/authoring-context.js +13 -13
  96. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +27 -0
  97. package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/risk-verdict.js +3 -4
  98. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +2 -2
  99. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +8 -20
  100. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +3 -2
  101. package/.agents/scripts/lib/orchestration/pr-base-guard.js +18 -28
  102. package/.agents/scripts/lib/orchestration/preflight-cache.js +5 -5
  103. package/.agents/scripts/lib/orchestration/remote-verifier.js +1 -1
  104. package/.agents/scripts/lib/orchestration/resolve-plan-run.js +155 -0
  105. package/.agents/scripts/lib/orchestration/resolves-token.js +1 -1
  106. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +8 -8
  107. package/.agents/scripts/lib/orchestration/retro-proposals.js +140 -79
  108. package/.agents/scripts/lib/orchestration/review-depth.js +26 -12
  109. package/.agents/scripts/lib/orchestration/review-providers/codex.js +2 -2
  110. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +21 -56
  111. package/.agents/scripts/lib/orchestration/run-epilogue.js +426 -0
  112. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +1 -1
  113. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +95 -41
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +16 -13
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +40 -0
  116. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +11 -3
  117. package/.agents/scripts/lib/orchestration/spec-freshness.js +14 -205
  118. package/.agents/scripts/lib/orchestration/spec-section-validator.js +4 -5
  119. package/.agents/scripts/lib/orchestration/spec-spill.js +60 -0
  120. package/.agents/scripts/lib/orchestration/split-policy-validator.js +188 -0
  121. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +49 -0
  122. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +15 -12
  123. package/.agents/scripts/lib/orchestration/story-follow-ups.js +237 -0
  124. package/.agents/scripts/lib/orchestration/story-init-remote.js +47 -0
  125. package/.agents/scripts/lib/orchestration/story-plan-state.js +48 -0
  126. package/.agents/scripts/lib/orchestration/{epic-runner → story-progress}/story-run-progress-writer.js +3 -3
  127. package/.agents/scripts/lib/orchestration/structured-comment-parser.js +1 -1
  128. package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -18
  129. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +11 -61
  130. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +189 -373
  131. package/.agents/scripts/lib/orchestration/ticket-validator.js +2 -7
  132. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +0 -25
  133. package/.agents/scripts/lib/orchestration/ticketing/reads.js +29 -26
  134. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -5
  135. package/.agents/scripts/lib/planning-corpus.js +16 -11
  136. package/.agents/scripts/lib/preflight-runner.js +2 -2
  137. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -5
  138. package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +36 -0
  139. package/.agents/scripts/lib/single-story-sweep/protection-ctx.js +1 -1
  140. package/.agents/scripts/lib/story-adjacency.js +11 -14
  141. package/.agents/scripts/lib/story-body/story-body.js +124 -70
  142. package/.agents/scripts/lib/story-plan.js +2 -4
  143. package/.agents/scripts/lib/templates/decomposer-prompts.js +46 -45
  144. package/.agents/scripts/lib/templates/spec-author-prompts.js +47 -45
  145. package/.agents/scripts/lib/{epic-body-sections.js → ticket-body-sections.js} +26 -26
  146. package/.agents/scripts/lib/validation-evidence.js +1 -1
  147. package/.agents/scripts/lib/wave-runner/ready-set.js +6 -6
  148. package/.agents/scripts/lib/workspace-provisioner.js +1 -1
  149. package/.agents/scripts/lib/worktree/lifecycle/reap.js +5 -7
  150. package/.agents/scripts/lint-issue-body.js +71 -40
  151. package/.agents/scripts/mandrel-update-preflight.js +1 -1
  152. package/.agents/scripts/notify.js +4 -3
  153. package/.agents/scripts/plan-context.js +64 -74
  154. package/.agents/scripts/plan-persist.js +121 -280
  155. package/.agents/scripts/plan-run-epilogue.js +97 -0
  156. package/.agents/scripts/providers/github/issues.js +17 -33
  157. package/.agents/scripts/providers/github/mappers.js +0 -12
  158. package/.agents/scripts/providers/github/tickets.js +2 -5
  159. package/.agents/scripts/resolve-plan-run.js +117 -0
  160. package/.agents/scripts/signals-view.js +24 -19
  161. package/.agents/scripts/single-story-close.js +11 -14
  162. package/.agents/scripts/single-story-confirm-merge.js +39 -23
  163. package/.agents/scripts/single-story-init.js +29 -20
  164. package/.agents/scripts/stories-wave-tick.js +6 -6
  165. package/.agents/scripts/story-plan.js +26 -47
  166. package/.agents/scripts/update-ticket-state.js +6 -15
  167. package/.agents/skills/core/analyze-execution/SKILL.md +21 -18
  168. package/.agents/skills/core/idea-refinement/SKILL.md +6 -6
  169. package/.agents/skills/core/scope-triage/SKILL.md +28 -172
  170. package/.agents/skills/skills.index.json +3 -43
  171. package/.agents/starter-agentrc.json +0 -5
  172. package/.agents/templates/agent-protocol.md +9 -10
  173. package/.agents/workflows/audit-architecture.md +3 -3
  174. package/.agents/workflows/audit-clean-code.md +3 -3
  175. package/.agents/workflows/audit-dependencies.md +3 -3
  176. package/.agents/workflows/audit-devops.md +3 -3
  177. package/.agents/workflows/audit-documentation.md +5 -5
  178. package/.agents/workflows/audit-lighthouse.md +3 -3
  179. package/.agents/workflows/audit-navigability.md +3 -2
  180. package/.agents/workflows/audit-performance.md +3 -3
  181. package/.agents/workflows/audit-privacy.md +3 -3
  182. package/.agents/workflows/audit-quality.md +3 -3
  183. package/.agents/workflows/audit-security.md +3 -3
  184. package/.agents/workflows/audit-seo.md +3 -3
  185. package/.agents/workflows/audit-sre.md +3 -3
  186. package/.agents/workflows/audit-to-stories.md +20 -20
  187. package/.agents/workflows/audit-ux-ui.md +3 -3
  188. package/.agents/workflows/deliver.md +122 -174
  189. package/.agents/workflows/git-cleanup.md +3 -4
  190. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  191. package/.agents/workflows/helpers/acceptance-self-eval.md +16 -29
  192. package/.agents/workflows/helpers/code-review.md +70 -193
  193. package/.agents/workflows/helpers/{single-story-deliver-reference.md → deliver-story-reference.md} +12 -14
  194. package/.agents/workflows/helpers/{single-story-deliver.md → deliver-story.md} +113 -139
  195. package/.agents/workflows/helpers/diagnose.md +10 -10
  196. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  197. package/.agents/workflows/helpers/signals.md +16 -16
  198. package/.agents/workflows/helpers/worktree-lifecycle.md +48 -64
  199. package/.agents/workflows/mandrel-update.md +2 -1
  200. package/.agents/workflows/plan.md +112 -145
  201. package/.agents/workflows/qa-assist.md +24 -30
  202. package/.agents/workflows/qa-explore.md +29 -38
  203. package/.agents/workflows/qa-run.md +2 -2
  204. package/README.md +9 -8
  205. package/docs/CHANGELOG.md +30 -0
  206. package/lib/migrations/index.js +6 -5
  207. package/package.json +2 -2
  208. package/.agents/agents/retro.md +0 -42
  209. package/.agents/personas/architect.md +0 -113
  210. package/.agents/personas/devops-engineer.md +0 -38
  211. package/.agents/personas/engineer.md +0 -33
  212. package/.agents/personas/project-manager.md +0 -114
  213. package/.agents/personas/qa-engineer.md +0 -95
  214. package/.agents/personas/security-engineer.md +0 -111
  215. package/.agents/personas/technical-writer.md +0 -101
  216. package/.agents/schemas/dispatch-manifest.json +0 -232
  217. package/.agents/schemas/epic-spec.schema.json +0 -153
  218. package/.agents/schemas/lifecycle/slice.end.schema.json +0 -21
  219. package/.agents/schemas/lifecycle/slice.heartbeat.schema.json +0 -20
  220. package/.agents/schemas/lifecycle/slice.start.schema.json +0 -17
  221. package/.agents/scripts/acceptance-spec-reconciler.js +0 -642
  222. package/.agents/scripts/bookkeeping-reconcile.js +0 -117
  223. package/.agents/scripts/dispatcher.js +0 -295
  224. package/.agents/scripts/epic-audit-prepare.js +0 -497
  225. package/.agents/scripts/epic-audit-recheck.js +0 -274
  226. package/.agents/scripts/epic-deliver-note-intervention.js +0 -192
  227. package/.agents/scripts/epic-deliver-preflight.js +0 -462
  228. package/.agents/scripts/epic-deliver-prepare.js +0 -852
  229. package/.agents/scripts/epic-execute-record-wave.js +0 -449
  230. package/.agents/scripts/epic-plan-clarity.js +0 -211
  231. package/.agents/scripts/epic-plan-healthcheck.js +0 -581
  232. package/.agents/scripts/epic-reconcile.js +0 -625
  233. package/.agents/scripts/lib/baseline-snapshot.js +0 -979
  234. package/.agents/scripts/lib/checks/epic-merge-lock-stale.js +0 -54
  235. package/.agents/scripts/lib/checks/stale-origin-epic.js +0 -49
  236. package/.agents/scripts/lib/config/lifecycle.js +0 -40
  237. package/.agents/scripts/lib/config/preflight.js +0 -58
  238. package/.agents/scripts/lib/config/retro.js +0 -77
  239. package/.agents/scripts/lib/epic-merge-lock.js +0 -322
  240. package/.agents/scripts/lib/epic-plan-clarity.js +0 -181
  241. package/.agents/scripts/lib/epic-plan-ideation.js +0 -261
  242. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +0 -539
  243. package/.agents/scripts/lib/orchestration/deliver-route.js +0 -173
  244. package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -134
  245. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +0 -183
  246. package/.agents/scripts/lib/orchestration/epic-cleanup.js +0 -801
  247. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +0 -310
  248. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +0 -163
  249. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +0 -140
  250. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +0 -64
  251. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +0 -72
  252. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +0 -156
  253. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +0 -345
  254. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +0 -41
  255. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/reconcile-spawn.js +0 -86
  256. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +0 -391
  257. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +0 -94
  258. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +0 -236
  259. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +0 -307
  260. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +0 -117
  261. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +0 -117
  262. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +0 -621
  263. package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +0 -186
  264. package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +0 -50
  265. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +0 -129
  266. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +0 -103
  267. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +0 -267
  268. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -210
  269. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +0 -238
  270. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/_bullet-format.js +0 -32
  271. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +0 -203
  272. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +0 -227
  273. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +0 -117
  274. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/stalled-worktree.js +0 -37
  275. package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +0 -127
  276. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +0 -276
  277. package/.agents/scripts/lib/orchestration/epic-runner/wave-scheduler.js +0 -66
  278. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +0 -789
  279. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +0 -676
  280. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-discriminator.js +0 -389
  281. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-format.js +0 -230
  282. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +0 -361
  283. package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +0 -306
  284. package/.agents/scripts/lib/orchestration/finalize/post-handoff-comment.js +0 -489
  285. package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +0 -88
  286. package/.agents/scripts/lib/orchestration/lifecycle/emit-slice-lifecycle.js +0 -270
  287. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +0 -147
  288. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +0 -465
  289. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +0 -501
  290. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +0 -984
  291. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +0 -264
  292. package/.agents/scripts/lib/orchestration/lifecycle/listeners/checkpoint-pointer-writer.js +0 -284
  293. package/.agents/scripts/lib/orchestration/lifecycle/listeners/cleaner.js +0 -355
  294. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +0 -673
  295. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +0 -378
  296. package/.agents/scripts/lib/orchestration/lifecycle/listeners/intervention-recorder.js +0 -140
  297. package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +0 -144
  298. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +0 -174
  299. package/.agents/scripts/lib/orchestration/manifest-builder.js +0 -222
  300. package/.agents/scripts/lib/orchestration/plan-persist/amend.js +0 -359
  301. package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +0 -127
  302. package/.agents/scripts/lib/orchestration/post-merge-pipeline.js +0 -205
  303. package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +0 -152
  304. package/.agents/scripts/lib/orchestration/retro/phases/checks.js +0 -94
  305. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +0 -571
  306. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +0 -450
  307. package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +0 -191
  308. package/.agents/scripts/lib/orchestration/retro-heuristics.js +0 -57
  309. package/.agents/scripts/lib/orchestration/retro-runner.js +0 -197
  310. package/.agents/scripts/lib/orchestration/spec-renderer.js +0 -447
  311. package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +0 -747
  312. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/gate-failure.js +0 -211
  313. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +0 -158
  314. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +0 -446
  315. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +0 -297
  316. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/scope-discovery.js +0 -48
  317. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution-wiring.js +0 -67
  318. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution.js +0 -161
  319. package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +0 -117
  320. package/.agents/scripts/lib/orchestration/story-close/cd-out-guard.js +0 -86
  321. package/.agents/scripts/lib/orchestration/story-close/cleanup-reconciler.js +0 -147
  322. package/.agents/scripts/lib/orchestration/story-close/close-inputs.js +0 -142
  323. package/.agents/scripts/lib/orchestration/story-close/comment-bodies.js +0 -62
  324. package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +0 -658
  325. package/.agents/scripts/lib/orchestration/story-close/merge-subject.js +0 -198
  326. package/.agents/scripts/lib/orchestration/story-close/phases/branch-restore.js +0 -105
  327. package/.agents/scripts/lib/orchestration/story-close/phases/close.js +0 -222
  328. package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +0 -292
  329. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +0 -270
  330. package/.agents/scripts/lib/orchestration/story-close/phases/preflight.js +0 -110
  331. package/.agents/scripts/lib/orchestration/story-close/phases/refresh.js +0 -86
  332. package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked-emitter.js +0 -112
  333. package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked.js +0 -157
  334. package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +0 -421
  335. package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +0 -302
  336. package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +0 -163
  337. package/.agents/scripts/lib/orchestration/story-close-recovery.js +0 -690
  338. package/.agents/scripts/lib/orchestration/wave-marker.js +0 -28
  339. package/.agents/scripts/lib/orchestration/wave-record-io.js +0 -218
  340. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +0 -145
  341. package/.agents/scripts/lib/orchestration/wave-record-projection.js +0 -212
  342. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +0 -111
  343. package/.agents/scripts/lib/presentation/manifest-builder.js +0 -239
  344. package/.agents/scripts/lib/presentation/manifest-formatter.js +0 -242
  345. package/.agents/scripts/lib/presentation/manifest-helpers.js +0 -213
  346. package/.agents/scripts/lib/presentation/manifest-persistence.js +0 -261
  347. package/.agents/scripts/lib/presentation/manifest-procedures.js +0 -55
  348. package/.agents/scripts/lib/presentation/manifest-render-waves.js +0 -306
  349. package/.agents/scripts/lib/presentation/manifest-renderer.js +0 -188
  350. package/.agents/scripts/lib/presentation/manifest-story-views.js +0 -110
  351. package/.agents/scripts/lib/push-epic-retry.js +0 -209
  352. package/.agents/scripts/lib/spec/index.js +0 -36
  353. package/.agents/scripts/lib/spec/loader.js +0 -425
  354. package/.agents/scripts/lib/spec/state.js +0 -208
  355. package/.agents/scripts/lib/story-init/blocker-validator.js +0 -68
  356. package/.agents/scripts/lib/story-init/branch-initializer.js +0 -408
  357. package/.agents/scripts/lib/story-init/context-resolver.js +0 -92
  358. package/.agents/scripts/lib/story-init/donor-precheck.js +0 -207
  359. package/.agents/scripts/lib/story-init/state-transitioner.js +0 -80
  360. package/.agents/scripts/lib/story-init/task-graph-builder.js +0 -124
  361. package/.agents/scripts/lib/story-init/transition-summary.js +0 -34
  362. package/.agents/scripts/lib/test-reserved-epic-temp-ids.js +0 -35
  363. package/.agents/scripts/lib/wave-runner/tick.js +0 -754
  364. package/.agents/scripts/lib/wave-runner/wave-runner-error.js +0 -20
  365. package/.agents/scripts/lifecycle-emit-story-dispatch.js +0 -194
  366. package/.agents/scripts/lifecycle-emit.js +0 -510
  367. package/.agents/scripts/retro-run.js +0 -218
  368. package/.agents/scripts/slice-phase.js +0 -361
  369. package/.agents/scripts/standalone-feedback-rollup.js +0 -188
  370. package/.agents/scripts/story-close.js +0 -294
  371. package/.agents/scripts/story-init.js +0 -599
  372. package/.agents/scripts/story-phase.js +0 -369
  373. package/.agents/scripts/wave-tick.js +0 -464
  374. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +0 -172
  375. package/.agents/skills/core/epic-plan-consolidate/examples.md +0 -51
  376. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +0 -441
  377. package/.agents/skills/core/epic-plan-decompose-author/examples.md +0 -47
  378. package/.agents/skills/core/epic-plan-premortem/SKILL.md +0 -146
  379. package/.agents/skills/core/epic-plan-premortem/examples.md +0 -53
  380. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +0 -383
  381. package/.agents/skills/core/epic-plan-spec-author/examples.md +0 -91
  382. package/.agents/workflows/helpers/deliver-epic-reference.md +0 -547
  383. package/.agents/workflows/helpers/deliver-epic-single.md +0 -331
  384. package/.agents/workflows/helpers/deliver-epic.md +0 -998
  385. package/.agents/workflows/helpers/deliver-stories.md +0 -450
  386. package/.agents/workflows/helpers/epic-audit.md +0 -189
  387. package/.agents/workflows/helpers/epic-deliver-story.md +0 -436
  388. package/.agents/workflows/helpers/epic-testing.md +0 -125
  389. package/.agents/workflows/helpers/plan-epic-reference.md +0 -160
  390. package/.agents/workflows/helpers/plan-epic.md +0 -353
  391. package/.agents/workflows/helpers/plan-story.md +0 -251
  392. package/.agents/workflows/helpers/scope-triage-gate.md +0 -108
  393. /package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/spec-authoring-grounding.js +0 -0
@@ -1,984 +0,0 @@
1
- // .agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js
2
- /**
3
- * AutomergePredicate — lifecycle listener that decides whether the Epic
4
- * PR is safe to auto-merge after the required-check watch settles.
5
- * Story #2256 / Task #2260 (Epic #2172); inlined from the now-deleted
6
- * legacy `automerge-predicate` module in Story #2415 (Epic #2307).
7
- * Rewritten in Story #4361 (Epic #4355) so green required CI is the
8
- * arming signal, gated by the `delivery.ci.autoMerge` policy.
9
- *
10
- * Subscribes to:
11
- * - `epic.automerge.start` (production path, Story #3901) → the
12
- * `/deliver` Phase 8.5 boundary that the `lifecycle-emit.js`
13
- * CLI actually fires. This event carries `prUrl` but NO
14
- * `checkOutcomes`. Before Story #4361 the listener trusted Phase 8
15
- * (`pr-watch-with-update.js`) to have polled every required check to
16
- * green and therefore skipped any CI probe on this path. That left a
17
- * hole (Story #3901): if the Phase 8 watch was interrupted (host
18
- * crash, `/loop` handoff, operator resume) the predicate could arm
19
- * merge with red or pending required checks. Story #4361 closes it —
20
- * the listener now runs a LIVE `gh pr checks --required` probe on
21
- * `epic.automerge.start` and refuses to arm when any required check
22
- * is not green, regardless of what Phase 8 believed.
23
- * - `epic.watch.end` (test-only `Watcher` path) → carries an
24
- * all-settled `checkOutcomes` map. Any non-passing required check
25
- * is a hard block evaluated BEFORE the structured-signal evaluator.
26
- * On this path the pre-supplied `checkOutcomes` map IS the CI truth,
27
- * so no live probe is issued (the map is authoritative).
28
- *
29
- * Policy (`delivery.ci.autoMerge`, Story #4356 / #4361):
30
- * - `"trust-ci"` (framework default) — green required CI is the arming
31
- * signal. The ONLY structured conditions that block arming are an
32
- * unresolved 🔴 critical (red) code-review finding or an
33
- * `agent::blocked` state (a story-level blocker recorded in
34
- * run-state, or a missing run-state checkpoint we cannot certify).
35
- * Manual interventions, 🟠 warning-level findings, and a non-clean
36
- * retro are RECORDED for audit (surfaced on the verdict and the
37
- * classification log) but no longer block the merge.
38
- * - `"strict"` — restores the prior clean-sprint predicate EXACTLY:
39
- * zero manual interventions, every Story done, no story blockers,
40
- * no 🔴/🟠 review findings, and a machine-readable `cleanSprint`
41
- * retro trailer. Any dirty signal blocks.
42
- *
43
- * Code-review parse-miss policy (Story #4222): a code-review comment that is
44
- * present but whose severity bullets cannot be parsed is treated as a DISTINCT
45
- * condition — surfaced via the `codeReviewUnparseable` signal — and FAILS OPEN
46
- * rather than blocking. Failing closed on a format miss is indistinguishable,
47
- * to the operator and to downstream telemetry, from a real disqualifying
48
- * finding; a parser miss must never masquerade as "the signal said no" inside
49
- * a generic `epic.merge.blocked`. Genuine critical/high findings still block
50
- * (per policy), because those require the counts to have parsed.
51
- *
52
- * Idempotency contract (AC-10): per-instance `Set<string>` of
53
- * `${event}:${seqId}` keys. A repeat `(event, seqId)` short-circuits
54
- * without re-evaluating and emits nothing. The evaluator is read-only
55
- * on GitHub state, so re-running it is safe; the seqId guard is the
56
- * defence against double-emit.
57
- *
58
- * Side-effect firewall: the listener calls the read-only evaluator, runs
59
- * the read-only `gh pr checks` probe, and emits on the bus. It does NOT
60
- * mutate labels, post comments, or call `notify`. Downstream consumers
61
- * (`AutomergeArmer` on `epic.merge.ready`; LabelTransitioner /
62
- * StructuredCommentPoster on `epic.merge.blocked`) own those side
63
- * effects. AutomergeArmer remains the SOLE site that shells `gh pr merge`
64
- * (the merge-lockout lint in `check-lifecycle-lint.js` enforces this).
65
- */
66
-
67
- import { spawnSync } from 'node:child_process';
68
-
69
- import { hasSurvivingCritical } from '../../../audit-suite/findings.js';
70
- import { getCiDelivery } from '../../../config/ci.js';
71
- import { parsePrNumberFromUrl } from '../../../github-url.js';
72
- import * as epicRunStateStore from '../../epic-run-state-store.js';
73
- import { findStructuredComment } from '../../ticketing.js';
74
- import { emitMergeUnlanded } from '../emit-merge-unlanded.js';
75
- import { normalizeCheckState, RECOGNIZED_CHECK_STATES } from './watcher.js';
76
-
77
- /**
78
- * Outcomes that count as "this required check did not block the merge".
79
- * `'neutral'` and `'skipped'` are non-failures by GitHub's own
80
- * convention; `'success'` is the happy path.
81
- *
82
- * Pure — exported for tests.
83
- */
84
- export const NON_FAILING_CHECK_OUTCOMES = Object.freeze(
85
- new Set(['success', 'neutral', 'skipped']),
86
- );
87
-
88
- /**
89
- * Reason categories emitted by the structured-signal evaluator. The
90
- * policy layer (`applyAutoMergePolicy`) decides which categories block
91
- * arming under each `delivery.ci.autoMerge` posture. Pure constant —
92
- * exported so the taxonomy is reviewable as code.
93
- *
94
- * - `criticalReview` — an unresolved 🔴 critical (red) code-review
95
- * finding. Blocks under BOTH policies.
96
- * - `blockedState` — an `agent::blocked` state (story-level blocker
97
- * recorded in run-state, non-done stories, or a missing run-state
98
- * checkpoint). Blocks under BOTH policies.
99
- * - `warningReview` — a 🟠 warning-level (high-risk) code-review
100
- * finding. Blocks under `strict` only.
101
- * - `intervention` — a recorded manual intervention. Blocks under
102
- * `strict` only.
103
- * - `retro` — a non-clean / missing retro verdict trailer.
104
- * Blocks under `strict` only.
105
- */
106
- export const REASON_CATEGORY = Object.freeze({
107
- CRITICAL_REVIEW: 'criticalReview',
108
- BLOCKED_STATE: 'blockedState',
109
- WARNING_REVIEW: 'warningReview',
110
- INTERVENTION: 'intervention',
111
- RETRO: 'retro',
112
- });
113
-
114
- /**
115
- * Categories that block arming under the default `"trust-ci"` policy.
116
- * Everything else is recorded for audit but does not block. Pure —
117
- * exported for tests.
118
- */
119
- export const TRUST_CI_BLOCKING_CATEGORIES = Object.freeze(
120
- new Set([REASON_CATEGORY.CRITICAL_REVIEW, REASON_CATEGORY.BLOCKED_STATE]),
121
- );
122
-
123
- /**
124
- * Regex that extracts the machine-readable auto-merge verdict trailer
125
- * emitted by the retro body composer (`retro/phases/compose-body.js`,
126
- * Story #3901). Shape:
127
- * `<!-- automerge-verdict: {"cleanSprint":true,"scorecard":{…}} -->`
128
- *
129
- * Reading a parsed JSON boolean replaces the pre-#3901 emoji
130
- * `.includes('🟢 Clean sprint')` string-match — a brittle prose scan
131
- * that false-positived on any retro that quoted the marker and
132
- * false-negatived on any compact-body copy edit. Pure — exported for
133
- * tests so the trailer contract is reviewable as code.
134
- */
135
- export const AUTOMERGE_VERDICT_TRAILER_RE =
136
- /<!--\s*automerge-verdict:\s*(\{[\s\S]*?\})\s*-->/;
137
-
138
- /**
139
- * Parse the machine-readable auto-merge verdict trailer out of a retro
140
- * body. Returns the decoded object on success, or `null` when the
141
- * trailer is absent or its JSON payload is malformed (a malformed
142
- * trailer is treated as "no verdict", which downstream disqualifies
143
- * the Epic rather than silently passing). Pure — exported for tests.
144
- *
145
- * @param {string} body
146
- * @returns {{ cleanSprint?: boolean, scorecard?: object } | null}
147
- */
148
- export function parseAutomergeVerdictTrailer(body) {
149
- if (typeof body !== 'string' || body.length === 0) return null;
150
- const m = AUTOMERGE_VERDICT_TRAILER_RE.exec(body);
151
- if (!m) return null;
152
- try {
153
- const parsed = JSON.parse(m[1]);
154
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
155
- ? parsed
156
- : null;
157
- } catch {
158
- return null;
159
- }
160
- }
161
-
162
- /**
163
- * Reduce a `checkOutcomes` map to the list of names that did NOT pass.
164
- * Pure — exported for tests so the failure-classification rule is
165
- * reviewable as code. Returns `[]` for an all-green map.
166
- */
167
- export function listFailingChecks(checkOutcomes) {
168
- const failures = [];
169
- for (const [name, outcome] of Object.entries(checkOutcomes ?? {})) {
170
- if (!NON_FAILING_CHECK_OUTCOMES.has(outcome)) {
171
- failures.push({ name, outcome });
172
- }
173
- }
174
- return failures;
175
- }
176
-
177
- /**
178
- * Format a non-empty failing-check list into a single-line `reason`
179
- * string for the `epic.merge.blocked` emit. Pure — exported for tests.
180
- */
181
- export function formatCheckFailureReason(failures) {
182
- const parts = failures.slice(0, 5).map((f) => `${f.name}=${f.outcome}`);
183
- const suffix = failures.length > 5 ? `; +${failures.length - 5} more` : '';
184
- return `required checks not green: ${parts.join(', ')}${suffix}`;
185
- }
186
-
187
- /**
188
- * Default live `gh pr checks --required` probe. Mirrors the Watcher's
189
- * spawn shape (`--json name,state,bucket,workflow`) so the required-set
190
- * projection is identical. Pure-spawn helper — exported so tests can
191
- * stub the shell-out. Returns the raw spawn envelope; the caller
192
- * classifies the payload.
193
- *
194
- * @param {{ prUrl: string, cwd?: string, spawnFn?: typeof spawnSync }} opts
195
- * @returns {{ status: number, stdout: string, stderr: string }}
196
- */
197
- export function probeRequiredChecks({ prUrl, cwd, spawnFn = spawnSync }) {
198
- const result = spawnFn(
199
- 'gh',
200
- ['pr', 'checks', prUrl, '--required', '--json', 'name,state,bucket'],
201
- { cwd, encoding: 'utf-8', shell: false },
202
- );
203
- return {
204
- status: result.status ?? 1,
205
- stdout: result.stdout ?? '',
206
- stderr: result.stderr ?? '',
207
- };
208
- }
209
-
210
- /**
211
- * Classify a live `gh pr checks --required` probe envelope into a
212
- * `{ ok, reason, outcomes }` verdict. Pure — exported for tests.
213
- *
214
- * - `ok: true` → every required check is green (non-failing) AND the
215
- * probe returned a parseable, non-empty required-check set.
216
- * - `ok: false` → at least one required check is not green, the probe
217
- * shelled out non-zero, or the payload could not be parsed. A probe
218
- * we cannot read is a hard block: we NEVER arm merge on an
219
- * unreadable CI signal (fail closed on the CI gate specifically —
220
- * the opposite of the code-review parse-miss policy, because CI
221
- * greenness is the whole point of the trust-ci arming signal).
222
- *
223
- * `gh pr checks --required` exits non-zero (status 8) when a required
224
- * check has failed OR is still pending, so a non-zero status is treated
225
- * as "not green" only when we cannot otherwise read a clean set from
226
- * stdout. We parse stdout first (it is populated even on the non-zero
227
- * exit) and classify from the outcomes.
228
- *
229
- * Story #4472 — checks-less repos. In a repo with zero required checks
230
- * (no branch protection, or protection that requires no status checks),
231
- * `gh pr checks --required` writes NOTHING to stdout and reports
232
- * `no checks reported on the <branch> branch` to stderr with a non-zero
233
- * exit. That is the SAME empty-parsed-set condition the outcomes loop
234
- * below already treats as green — there is simply nothing to gate on — so
235
- * we must not conflate it with a genuine probe failure (auth, network, no
236
- * PR). We detect the `no checks reported` stderr signature and return
237
- * green, UNLESS the consumer opted into `delivery.ci.requireChecks`, in
238
- * which case the absent CI gate is a deliberate hard block.
239
- *
240
- * @param {{ status: number, stdout: string, stderr: string }} probe
241
- * @param {{ requireChecks?: boolean }} [opts] When `requireChecks` is
242
- * true, a checks-less repo fails closed instead of arming.
243
- * @returns {{ ok: boolean, reason: string|null, outcomes: Record<string, string> }}
244
- */
245
- export function classifyRequiredChecksProbe(
246
- probe,
247
- { requireChecks = false } = {},
248
- ) {
249
- const stdout = String(probe?.stdout ?? '').trim();
250
- const stderr = String(probe?.stderr ?? '').trim();
251
- // Empty stdout: either a checks-less repo (green, nothing to gate on) or
252
- // a genuine probe failure. The `no checks reported` stderr signature
253
- // distinguishes them.
254
- if (stdout.length === 0) {
255
- const noChecksReported = /no checks reported/i.test(stderr);
256
- if (noChecksReported && !requireChecks) {
257
- // Zero required checks configured — matches the empty-parsed-set
258
- // "treated as green" branch below. Nothing to gate on.
259
- return { ok: true, reason: null, outcomes: {} };
260
- }
261
- if (noChecksReported && requireChecks) {
262
- return {
263
- ok: false,
264
- reason:
265
- 'no required checks reported and delivery.ci.requireChecks is set — failing closed per policy',
266
- outcomes: {},
267
- };
268
- }
269
- return {
270
- ok: false,
271
- reason:
272
- `live required-check probe failed (status=${probe?.status ?? 'unknown'})` +
273
- (stderr ? `: ${stderr.slice(0, 200)}` : ''),
274
- outcomes: {},
275
- };
276
- }
277
- let entries;
278
- try {
279
- const parsed = JSON.parse(stdout);
280
- entries = Array.isArray(parsed) ? parsed : null;
281
- } catch {
282
- entries = null;
283
- }
284
- if (!entries) {
285
- return {
286
- ok: false,
287
- reason: 'live required-check probe returned an unparseable payload',
288
- outcomes: {},
289
- };
290
- }
291
- const outcomes = {};
292
- for (const e of entries) {
293
- if (e && typeof e === 'object' && typeof e.name === 'string') {
294
- const raw = String(e.state || e.bucket || '')
295
- .trim()
296
- .toLowerCase();
297
- // Fail closed on the arming probe: a token we have not enumerated
298
- // could be a genuinely-failing GitHub conclusion. normalizeCheckState
299
- // collapses unknowns to 'skipped' (safe for the watch path, unsafe
300
- // here), so map an unrecognized token to 'unknown' — which is not in
301
- // NON_FAILING_CHECK_OUTCOMES and therefore blocks arming.
302
- outcomes[e.name] = RECOGNIZED_CHECK_STATES.has(raw)
303
- ? normalizeCheckState(raw)
304
- : 'unknown';
305
- }
306
- }
307
- // An empty required set (no required checks configured) is treated as
308
- // green — there is nothing to gate on. This matches branch protection
309
- // that requires no status checks.
310
- const failing = [];
311
- for (const [name, outcome] of Object.entries(outcomes)) {
312
- // `pending` is NOT in NON_FAILING — a still-running required check
313
- // blocks arming (the Phase 8 watch was interrupted before green).
314
- if (!NON_FAILING_CHECK_OUTCOMES.has(outcome)) {
315
- failing.push({ name, outcome });
316
- }
317
- }
318
- if (failing.length > 0) {
319
- return { ok: false, reason: formatCheckFailureReason(failing), outcomes };
320
- }
321
- return { ok: true, reason: null, outcomes };
322
- }
323
-
324
- /**
325
- * Regex-parse the rendered severity bullets on the code-review markdown
326
- * body. Pure. Exported for tests.
327
- *
328
- * @param {string} body
329
- * @returns {{ critical: number|null, high: number|null, medium: number|null, suggestion: number|null }}
330
- */
331
- export function parseSeverityCounts(body) {
332
- if (typeof body !== 'string' || body.length === 0) {
333
- return { critical: null, high: null, medium: null, suggestion: null };
334
- }
335
- const match = (re) => {
336
- const m = body.match(re);
337
- if (!m) return null;
338
- const n = Number.parseInt(m[1], 10);
339
- return Number.isFinite(n) ? n : null;
340
- };
341
- return {
342
- critical: match(/🔴\s*Critical Blocker:\s*(\d+)/i),
343
- high: match(/🟠\s*High Risk:\s*(\d+)/i),
344
- medium: match(/🟡\s*Medium Risk:\s*(\d+)/i),
345
- suggestion: match(/🟢\s*Suggestion:\s*(\d+)/i),
346
- };
347
- }
348
-
349
- /**
350
- * Push a categorized reason onto the reasons array. Each reason carries
351
- * a `category` (from `REASON_CATEGORY`) so the policy layer can decide
352
- * whether it blocks under the active posture, plus a human-facing
353
- * `message`. Pure helper.
354
- */
355
- function pushReason(reasons, category, message) {
356
- reasons.push({ category, message });
357
- }
358
-
359
- function evaluateStateSignals(state, reasons) {
360
- const interventionCount = Array.isArray(state?.manualInterventions)
361
- ? state.manualInterventions.length
362
- : 0;
363
- if (!state) {
364
- // A missing checkpoint means we cannot certify the run at all — this
365
- // is a blocked-state condition (we do not know whether a Story is
366
- // blocked), so it blocks under BOTH policies.
367
- pushReason(
368
- reasons,
369
- REASON_CATEGORY.BLOCKED_STATE,
370
- 'epic-run-state checkpoint missing — cannot certify clean run',
371
- );
372
- } else if (interventionCount > 0) {
373
- pushReason(
374
- reasons,
375
- REASON_CATEGORY.INTERVENTION,
376
- `manual interventions recorded (${interventionCount}): ${state.manualInterventions
377
- .map((i) => i.reason)
378
- .slice(0, 3)
379
- .join('; ')}${interventionCount > 3 ? '; …' : ''}`,
380
- );
381
- }
382
- // Story #4155 — the ready-set runtime records a flat per-Story status map
383
- // on the checkpoint (`stories: { [id]: { status, blockerCommentId? } }`)
384
- // instead of a per-wave `waves[]` history. The clean-run certification
385
- // reads it directly: a run is clean only when every Story reached `done`
386
- // and none carries a recorded blocker comment.
387
- const stories =
388
- state?.stories && typeof state.stories === 'object' ? state.stories : {};
389
- const storyStatuses = Object.values(stories).map(
390
- (s) => s?.status ?? 'pending',
391
- );
392
- const nonDoneStatuses = storyStatuses.filter((s) => s !== 'done');
393
- if (nonDoneStatuses.length > 0) {
394
- // A story not done is an unfinished / blocked run — blocks under both.
395
- pushReason(
396
- reasons,
397
- REASON_CATEGORY.BLOCKED_STATE,
398
- `${nonDoneStatuses.length} story(ies) not done (statuses: ${nonDoneStatuses.join(', ')})`,
399
- );
400
- }
401
- const storyBlockers = countStoryBlockers(stories);
402
- if (storyBlockers > 0) {
403
- // A recorded story-level blocker is the `agent::blocked` state — it
404
- // blocks under both policies.
405
- pushReason(
406
- reasons,
407
- REASON_CATEGORY.BLOCKED_STATE,
408
- `${storyBlockers} story-level blocker(s) recorded in run-state`,
409
- );
410
- }
411
- return { interventionCount, storyStatuses, storyBlockers };
412
- }
413
-
414
- /**
415
- * Count blockers in the flat per-Story `stories` status map: each Story with
416
- * a recorded `blockerCommentId` and each Story whose status is not `done`
417
- * contributes one blocker (matching the prior per-wave count semantics).
418
- *
419
- * @param {Record<string, { status?: string, blockerCommentId?: string }>} stories
420
- * @returns {number}
421
- */
422
- function countStoryBlockers(stories) {
423
- let blockers = 0;
424
- for (const s of Object.values(stories ?? {})) {
425
- if (
426
- s &&
427
- typeof s.blockerCommentId === 'string' &&
428
- s.blockerCommentId.length > 0
429
- ) {
430
- blockers += 1;
431
- }
432
- if (s?.status && s.status !== 'done') {
433
- blockers += 1;
434
- }
435
- }
436
- return blockers;
437
- }
438
-
439
- function evaluateCodeReviewSignals(codeReview, reasons) {
440
- const codeReviewFound = !!codeReview && typeof codeReview.body === 'string';
441
- const severity = codeReviewFound
442
- ? parseSeverityCounts(codeReview.body)
443
- : { critical: null, high: null, medium: null, suggestion: null };
444
- if (!codeReviewFound) {
445
- // A missing code-review comment is a soft signal — it does not, on
446
- // its own, prove a critical finding. Categorize it as a warning-level
447
- // reason so it blocks under `strict` (which demands the clean gate)
448
- // but not under `trust-ci` (which only blocks on a PARSED 🔴 count).
449
- pushReason(
450
- reasons,
451
- REASON_CATEGORY.WARNING_REVIEW,
452
- 'code-review structured comment not found on Epic',
453
- );
454
- return { codeReviewFound, codeReviewUnparseable: false, severity };
455
- }
456
- // "Present but unparseable" is a DISTINCT condition from "present and says
457
- // no" (Story #4222). The canonical renderer
458
- // (`review-providers/findings-renderer.js`) always emits all four severity
459
- // bullets, so a body whose critical/high counts we cannot extract is a
460
- // FORMAT MISS, not a disqualifying signal. Failing closed here — pushing a
461
- // generic block reason — is indistinguishable, to the operator and to
462
- // downstream telemetry (the mandrel-bench Autonomy dimension), from a real
463
- // critical finding: it stalls an otherwise-clean unattended run for a
464
- // non-reason.
465
- //
466
- // Chosen policy: FAIL OPEN on an unparseable code-review body. We surface
467
- // the condition explicitly via the `codeReviewUnparseable` signal so
468
- // telemetry can tell a parser miss from a true HITL hand-off, but we do NOT
469
- // add a disqualifying `reasons[]` entry — the absence of a parseable
470
- // critical/high count cannot, on its own, block a run whose other signals
471
- // are clean. Genuine disqualifying review findings (critical > 0 /
472
- // high > 0) still block below, because those require the counts to have
473
- // parsed successfully.
474
- const codeReviewUnparseable =
475
- severity.critical === null || severity.high === null;
476
- if (codeReviewUnparseable) {
477
- return { codeReviewFound, codeReviewUnparseable, severity };
478
- }
479
- // Route the halt-on-critical decision through the single halting rule of
480
- // the unified verification-results contract (Story #4411) rather than a
481
- // re-derived `critical > 0` expression. The unparseable branch above has
482
- // already returned, so `severity.critical` is a concrete number here.
483
- if (hasSurvivingCritical(severity)) {
484
- pushReason(
485
- reasons,
486
- REASON_CATEGORY.CRITICAL_REVIEW,
487
- `code-review has ${severity.critical} 🔴 Critical Blocker(s)`,
488
- );
489
- }
490
- if (severity.high > 0) {
491
- pushReason(
492
- reasons,
493
- REASON_CATEGORY.WARNING_REVIEW,
494
- `code-review has ${severity.high} 🟠 High Risk finding(s)`,
495
- );
496
- }
497
- return { codeReviewFound, codeReviewUnparseable, severity };
498
- }
499
-
500
- function evaluateRetroSignals(retro, reasons) {
501
- const retroFound = !!retro && typeof retro.body === 'string';
502
- if (!retroFound) {
503
- pushReason(
504
- reasons,
505
- REASON_CATEGORY.RETRO,
506
- 'retro structured comment not found on Epic',
507
- );
508
- return { retroFound, retroCompact: false };
509
- }
510
- // Read the machine-readable verdict trailer instead of string-matching
511
- // the human-facing "🟢 Clean sprint" prose (Story #3901). A missing or
512
- // malformed trailer is a hard disqualifier under `strict` — we never arm
513
- // strict-policy auto-merge on a retro whose verdict we cannot read.
514
- const verdict = parseAutomergeVerdictTrailer(retro.body);
515
- if (!verdict) {
516
- pushReason(
517
- reasons,
518
- REASON_CATEGORY.RETRO,
519
- 'retro is missing the machine-readable automerge-verdict trailer (cannot certify clean sprint)',
520
- );
521
- return { retroFound, retroCompact: false };
522
- }
523
- const retroCompact = verdict.cleanSprint === true;
524
- if (!retroCompact) {
525
- pushReason(
526
- reasons,
527
- REASON_CATEGORY.RETRO,
528
- 'retro automerge-verdict trailer reports cleanSprint=false (full retro indicates friction / parked / interventions)',
529
- );
530
- }
531
- return { retroFound, retroCompact };
532
- }
533
-
534
- /**
535
- * Pure verdict-from-signals function. Composes the three signal sources into
536
- * a single envelope. The `clean` boolean is the strict-policy verdict (true
537
- * iff there are zero reasons of any category) — it is preserved so the
538
- * `strict` policy restores the prior predicate EXACTLY. `reasons` is the
539
- * flat string[] of human-facing messages (byte-identical to the pre-#4361
540
- * output for the same inputs); `categorizedReasons` is the same list tagged
541
- * with a `REASON_CATEGORY` so the policy filter (`applyAutoMergePolicy`) can
542
- * narrow which reasons block per posture. Exported for tests.
543
- *
544
- * @param {{
545
- * state: object|null,
546
- * codeReview: { body: string }|null,
547
- * retro: { body: string }|null,
548
- * }} input
549
- * @returns {{
550
- * clean: boolean,
551
- * reasons: string[],
552
- * categorizedReasons: Array<{ category: string, message: string }>,
553
- * signals: {
554
- * manualInterventions: number,
555
- * storyStatuses: string[],
556
- * storyBlockers: number,
557
- * severity: { critical: number|null, high: number|null, medium: number|null, suggestion: number|null },
558
- * codeReviewUnparseable: boolean,
559
- * retroCompact: boolean,
560
- * codeReviewFound: boolean,
561
- * retroFound: boolean,
562
- * stateFound: boolean,
563
- * },
564
- * }}
565
- */
566
- export function deriveAutoMergeVerdict({ state, codeReview, retro }) {
567
- const categorizedReasons = [];
568
- const stateSig = evaluateStateSignals(state, categorizedReasons);
569
- const reviewSig = evaluateCodeReviewSignals(codeReview, categorizedReasons);
570
- const retroSig = evaluateRetroSignals(retro, categorizedReasons);
571
-
572
- return {
573
- // `clean` is the STRICT verdict: no reason of any category. This is the
574
- // exact pre-#4361 predicate, preserved so `strict` policy is unchanged.
575
- clean: categorizedReasons.length === 0,
576
- // Flat string[] — byte-identical to the pre-#4361 reason messages.
577
- reasons: categorizedReasons.map((r) => r.message),
578
- categorizedReasons,
579
- signals: {
580
- manualInterventions: stateSig.interventionCount,
581
- storyStatuses: stateSig.storyStatuses,
582
- storyBlockers: stateSig.storyBlockers,
583
- severity: reviewSig.severity,
584
- codeReviewUnparseable: reviewSig.codeReviewUnparseable,
585
- retroCompact: retroSig.retroCompact,
586
- codeReviewFound: reviewSig.codeReviewFound,
587
- retroFound: retroSig.retroFound,
588
- stateFound: !!state,
589
- },
590
- };
591
- }
592
-
593
- /**
594
- * Apply the `delivery.ci.autoMerge` policy to a categorized verdict.
595
- * Returns the EFFECTIVE arming decision plus the split of blocking vs.
596
- * recorded-only reasons. Pure — exported for tests.
597
- *
598
- * - `"strict"` → every reason blocks (identical to the pre-#4361
599
- * `clean` verdict). `recordedReasons` is empty.
600
- * - `"trust-ci"` → only `criticalReview` / `blockedState` reasons
601
- * block; the rest (interventions, warnings, retro) land in
602
- * `recordedReasons` for audit and do NOT gate the merge.
603
- *
604
- * @param {{ clean: boolean, categorizedReasons: Array<{ category: string, message: string }> }} verdict
605
- * @param {'trust-ci'|'strict'} policy
606
- * @returns {{
607
- * arm: boolean,
608
- * policy: 'trust-ci'|'strict',
609
- * blockingReasons: Array<{ category: string, message: string }>,
610
- * recordedReasons: Array<{ category: string, message: string }>,
611
- * }}
612
- */
613
- export function applyAutoMergePolicy(verdict, policy) {
614
- const reasons = Array.isArray(verdict?.categorizedReasons)
615
- ? verdict.categorizedReasons
616
- : [];
617
- if (policy === 'strict') {
618
- return {
619
- arm: reasons.length === 0,
620
- policy: 'strict',
621
- blockingReasons: reasons,
622
- recordedReasons: [],
623
- };
624
- }
625
- // trust-ci (default): only critical-review and blocked-state reasons gate.
626
- const blockingReasons = [];
627
- const recordedReasons = [];
628
- for (const r of reasons) {
629
- if (TRUST_CI_BLOCKING_CATEGORIES.has(r.category)) {
630
- blockingReasons.push(r);
631
- } else {
632
- recordedReasons.push(r);
633
- }
634
- }
635
- return {
636
- arm: blockingReasons.length === 0,
637
- policy: 'trust-ci',
638
- blockingReasons,
639
- recordedReasons,
640
- };
641
- }
642
-
643
- /**
644
- * Join a categorized reason list into a single-line `reason` string for
645
- * the `epic.merge.blocked` emit / classification log. Pure.
646
- */
647
- function formatReasons(reasons, prefix = '') {
648
- const messages = reasons.map((r) => r.message);
649
- const head = messages.slice(0, 3).join('; ');
650
- const suffix = messages.length > 3 ? `; +${messages.length - 3} more` : '';
651
- return `${prefix}${head}${suffix}`;
652
- }
653
-
654
- /**
655
- * IO-bound entry. Loads all three signal sources from the structured-comment
656
- * surface on the Epic ticket and hands them to `deriveAutoMergeVerdict`.
657
- * DI-friendly via the `findCommentFn` and `readRunStateFn` hooks; both
658
- * default to the production stack (the `epic-run-state-store.read` function
659
- * replaces the previous `checkpointerFactory` indirection introduced by the
660
- * now-deleted `Checkpointer` class).
661
- *
662
- * @param {{
663
- * provider: object,
664
- * epicId: number,
665
- * findCommentFn?: typeof findStructuredComment,
666
- * readRunStateFn?: typeof epicRunStateStore.read,
667
- * }} opts
668
- * @returns {Promise<{ clean: boolean, reasons: object[], signals: object }>}
669
- */
670
- export async function evaluateAutoMergePredicate({
671
- provider,
672
- epicId,
673
- findCommentFn = findStructuredComment,
674
- readRunStateFn = epicRunStateStore.read,
675
- }) {
676
- if (!provider)
677
- throw new TypeError('evaluateAutoMergePredicate: provider required');
678
- if (!Number.isInteger(epicId) || epicId <= 0) {
679
- throw new TypeError(
680
- 'evaluateAutoMergePredicate: epicId must be a positive integer',
681
- );
682
- }
683
-
684
- // Sequential awaits (not Promise.all) — the lifecycle lint surface forbids
685
- // Promise.all under `lib/orchestration/lifecycle/**` because parallelizing
686
- // listener invocations breaks bus repeatability. This evaluator is read-
687
- // only IO, but the rule is directory-scoped; sequencing here is a
688
- // cheap concession for living inside the listener tree.
689
- const state = await readRunStateFn({ provider, epicId });
690
- const codeReview = await findCommentFn(
691
- provider,
692
- epicId,
693
- 'verification-results',
694
- );
695
- let retro = await findCommentFn(provider, epicId, 'retro');
696
- if (!retro) {
697
- retro = await findCommentFn(provider, epicId, 'retro-partial');
698
- }
699
-
700
- return deriveAutoMergeVerdict({ state, codeReview, retro });
701
- }
702
-
703
- /**
704
- * AutomergePredicate listener.
705
- */
706
- export class AutomergePredicate {
707
- /**
708
- * @param {object} opts
709
- * @param {object} opts.bus
710
- * @param {number} opts.epicId
711
- * @param {object} opts.provider GitHub provider (passed through to the
712
- * evaluator). Required for the read of run-state + structured
713
- * comments.
714
- * @param {object} [opts.config] Resolved agent config. Read for the
715
- * `delivery.ci.autoMerge` policy and the `delivery.ci.requireChecks`
716
- * fail-closed-without-checks policy via `getCiDelivery`. Defaults to the
717
- * framework defaults (`trust-ci` / `requireChecks: false`) when omitted.
718
- * @param {boolean} [opts.headless] When true (a `/deliver --yes` run), a
719
- * predicate refusal escalates to an explicit `merge.unlanded` +
720
- * `epic.blocked` terminal instead of silently parking on the
721
- * operator-merges path (Story #4472). Defaults to `false` (attended).
722
- * @param {string} [opts.cwd] Working directory for the live
723
- * `gh pr checks --required` probe. Defaults to `process.cwd()`.
724
- * @param {Function} [opts.evaluatePredicateFn] override of
725
- * `evaluateAutoMergePredicate` for tests.
726
- * @param {Function} [opts.probeRequiredChecksFn] override of
727
- * `probeRequiredChecks` for tests.
728
- * @param {{ info?: Function, warn?: Function, debug?: Function }} [opts.logger]
729
- */
730
- constructor(opts = {}) {
731
- if (
732
- !opts.bus ||
733
- typeof opts.bus.on !== 'function' ||
734
- typeof opts.bus.emit !== 'function'
735
- ) {
736
- throw new TypeError(
737
- 'AutomergePredicate requires a bus with on() and emit()',
738
- );
739
- }
740
- if (!Number.isInteger(opts.epicId) || opts.epicId < 1) {
741
- throw new TypeError('AutomergePredicate requires a numeric epicId');
742
- }
743
- if (!opts.provider) {
744
- throw new TypeError('AutomergePredicate requires a provider');
745
- }
746
- this.bus = opts.bus;
747
- this.epicId = opts.epicId;
748
- this.provider = opts.provider;
749
- this.cwd = opts.cwd ?? process.cwd();
750
- // Resolve the merge posture + fail-closed policy once at construction.
751
- // `getCiDelivery` applies the framework defaults (`trust-ci` /
752
- // `requireChecks: false`) for any omitted field.
753
- const ci = getCiDelivery(opts.config ?? null);
754
- this.policy = ci.autoMerge;
755
- this.requireChecks = ci.requireChecks;
756
- this.headless = opts.headless === true;
757
- this.evaluatePredicateFn =
758
- opts.evaluatePredicateFn ?? evaluateAutoMergePredicate;
759
- this.probeRequiredChecksFn =
760
- opts.probeRequiredChecksFn ?? probeRequiredChecks;
761
- // Injected for tests so the headless terminal escalation can be
762
- // observed without touching disk.
763
- this.emitMergeUnlandedFn = opts.emitMergeUnlandedFn ?? emitMergeUnlanded;
764
- this.logger = opts.logger ?? console;
765
- /** @type {Set<string>} `${event}:${seqId}` idempotency cache. */
766
- this._seen = new Set();
767
- /**
768
- * Classification log — every event we observe lands here with the
769
- * outcome (`ready`, `blocked`, `skipped-duplicate`, `failed`).
770
- * Mirrors the Finalizer / Reconciler "no silent skip" surface.
771
- */
772
- this.classifications = [];
773
- this.events = Object.freeze(['epic.automerge.start', 'epic.watch.end']);
774
- }
775
-
776
- register() {
777
- return this.events.map((event) =>
778
- this.bus.on(event, async (ctx) => this.handle(ctx)),
779
- );
780
- }
781
-
782
- async handle({ event, seqId, payload }) {
783
- const key = `${event}:${seqId}`;
784
- if (this._seen.has(key)) {
785
- this.classifications.push({
786
- event,
787
- seqId,
788
- outcome: 'skipped',
789
- reason: 'duplicate-seqId',
790
- });
791
- this.logger.debug?.(
792
- `[AutomergePredicate] skip duplicate ${key} (idempotent)`,
793
- );
794
- return;
795
- }
796
- this._seen.add(key);
797
-
798
- const prUrl = payload?.prUrl;
799
- if (typeof prUrl !== 'string' || prUrl.length === 0) {
800
- this.classifications.push({
801
- event,
802
- seqId,
803
- outcome: 'failed',
804
- reason: 'no-pr-url',
805
- });
806
- return;
807
- }
808
- // Gate 1 — required-check freshness. Any non-passing required check is
809
- // a hard block: short-circuit before consulting the structured-signal
810
- // evaluator so the operator sees the CI failure as the reason, not a
811
- // downstream signal.
812
- //
813
- // Two sources of CI truth:
814
- // (a) `epic.watch.end` (test-only Watcher path) carries a settled
815
- // `checkOutcomes` map — that map is authoritative, so we classify
816
- // it directly and issue NO live probe.
817
- // (b) `epic.automerge.start` (production Phase 8.5) carries no map.
818
- // Story #4361: we run a LIVE `gh pr checks --required` probe here
819
- // so an interrupted Phase 8 watch cannot arm merge on red/pending
820
- // required checks (closes the Story #3901 hole).
821
- if (payload?.checkOutcomes !== undefined) {
822
- const failures = listFailingChecks(payload.checkOutcomes);
823
- if (failures.length > 0) {
824
- const reason = formatCheckFailureReason(failures);
825
- this.classifications.push({ event, seqId, outcome: 'blocked', reason });
826
- await this._emitBlocked(prUrl, reason);
827
- return;
828
- }
829
- } else {
830
- // Live probe — production path. Fail closed on any non-green,
831
- // pending, or unreadable required-check result.
832
- let probeVerdict;
833
- try {
834
- const probe = this.probeRequiredChecksFn({ prUrl, cwd: this.cwd });
835
- probeVerdict = classifyRequiredChecksProbe(probe, {
836
- requireChecks: this.requireChecks,
837
- });
838
- } catch (err) {
839
- probeVerdict = {
840
- ok: false,
841
- reason: `live required-check probe threw: ${err?.message ?? err}`,
842
- outcomes: {},
843
- };
844
- }
845
- if (!probeVerdict.ok) {
846
- const reason = probeVerdict.reason ?? 'required checks not green';
847
- this.classifications.push({ event, seqId, outcome: 'blocked', reason });
848
- await this._emitBlocked(prUrl, reason);
849
- return;
850
- }
851
- }
852
-
853
- // Gate 2 — structured-signal verdict, filtered by the merge policy.
854
- let verdict;
855
- try {
856
- verdict = await this.evaluatePredicateFn({
857
- provider: this.provider,
858
- epicId: this.epicId,
859
- });
860
- } catch (err) {
861
- const reason = `predicate-threw:${err?.message ?? err}`;
862
- this.classifications.push({ event, seqId, outcome: 'failed', reason });
863
- this.logger.warn?.(
864
- `[AutomergePredicate] evaluator threw (swallowed): ${err?.message ?? err}`,
865
- );
866
- // Conservative: a thrown evaluator is treated as blocked rather
867
- // than ready — we never arm auto-merge on uncertain signals.
868
- await this._emitBlocked(prUrl, reason);
869
- return;
870
- }
871
-
872
- const decision = applyAutoMergePolicy(verdict, this.policy);
873
- // Surface recorded-but-non-blocking reasons for audit even when the
874
- // trust-ci policy arms anyway (interventions / warnings / non-clean
875
- // retro). These never gate the merge but must not be silently dropped.
876
- const recorded = decision.recordedReasons ?? [];
877
-
878
- if (decision.arm) {
879
- this.classifications.push({
880
- event,
881
- seqId,
882
- outcome: 'ready',
883
- policy: decision.policy,
884
- signals: verdict.signals,
885
- ...(recorded.length > 0
886
- ? { recordedReasons: recorded.map((r) => r.message) }
887
- : {}),
888
- });
889
- if (recorded.length > 0) {
890
- this.logger.info?.(
891
- `[AutomergePredicate] arming under ${decision.policy}; recorded (non-blocking): ${formatReasons(recorded)}`,
892
- );
893
- }
894
- try {
895
- await this.bus.emit('epic.merge.ready', {
896
- prUrl,
897
- reason: `all required checks green; ${decision.policy} policy signals clear`,
898
- });
899
- } catch (err) {
900
- this.logger.warn?.(
901
- `[AutomergePredicate] epic.merge.ready emit failed (swallowed): ${err?.message ?? err}`,
902
- );
903
- }
904
- return;
905
- }
906
-
907
- const blocking = decision.blockingReasons ?? [];
908
- const reason =
909
- blocking.length > 0
910
- ? formatReasons(blocking)
911
- : 'predicate dirty (no reasons reported)';
912
- this.classifications.push({
913
- event,
914
- seqId,
915
- outcome: 'blocked',
916
- policy: decision.policy,
917
- reason,
918
- ...(recorded.length > 0
919
- ? { recordedReasons: recorded.map((r) => r.message) }
920
- : {}),
921
- });
922
- await this._emitBlocked(prUrl, reason);
923
- }
924
-
925
- /**
926
- * Emit `epic.merge.blocked`. Helper carved out so the blocking paths
927
- * (CI failure / predicate dirty / evaluator throw) share the same emit
928
- * shape.
929
- *
930
- * Story #4472 — must-land coverage of predicate refusal. In a headless
931
- * (`/deliver --yes`) run there is no operator to act on a bare
932
- * `epic.merge.blocked` (nothing in the listener chain consumes it), so
933
- * the run would silently park on the operator-merges path. When
934
- * `this.headless`, we additionally attribute the refusal to the
935
- * lifecycle ledger via `merge.unlanded` (blockClass `predicate-refused`)
936
- * and drive the explicit `epic.blocked` terminal — the same
937
- * escalation the MergeWatcher performs on post-arm budget exhaustion —
938
- * so the Epic transitions to `agent::blocked` with an operator-visible
939
- * reason instead of stalling.
940
- */
941
- async _emitBlocked(prUrl, reason) {
942
- try {
943
- await this.bus.emit('epic.merge.blocked', { prUrl, reason });
944
- } catch (err) {
945
- this.logger.warn?.(
946
- `[AutomergePredicate] epic.merge.blocked emit failed (swallowed): ${err?.message ?? err}`,
947
- );
948
- }
949
- if (!this.headless) return;
950
- // Ledger attribution — best-effort; a failed append must NOT mask the
951
- // epic.blocked transition below.
952
- try {
953
- const prNumber = parsePrNumberFromUrl(prUrl);
954
- if (Number.isInteger(prNumber) && prNumber > 0) {
955
- this.emitMergeUnlandedFn({
956
- scope: 'epic',
957
- ticketId: this.epicId,
958
- prNumber,
959
- blockClass: 'predicate-refused',
960
- reason,
961
- elapsedSeconds: 0,
962
- });
963
- }
964
- } catch (err) {
965
- this.logger.warn?.(
966
- `[AutomergePredicate] emitMergeUnlanded failed (swallowed): ${err?.message ?? err}`,
967
- );
968
- }
969
- try {
970
- await this.bus.emit('epic.blocked', {
971
- reason: `merge-predicate:refused`,
972
- });
973
- } catch (err) {
974
- this.logger.warn?.(
975
- `[AutomergePredicate] epic.blocked emit on predicate refusal failed (swallowed): ${err?.message ?? err}`,
976
- );
977
- }
978
- }
979
-
980
- reset() {
981
- this._seen.clear();
982
- this.classifications = [];
983
- }
984
- }