mandrel 1.94.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (560) hide show
  1. package/.agents/README.md +116 -99
  2. package/.agents/agents/acceptance-critic.md +9 -7
  3. package/.agents/agents/story-worker.md +45 -51
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +498 -1287
  6. package/.agents/docs/agentrc-reference.json +185 -80
  7. package/.agents/docs/configuration.md +132 -190
  8. package/.agents/docs/execution-reference.md +51 -25
  9. package/.agents/docs/quality-gates.md +25 -33
  10. package/.agents/docs/workflows.md +8 -8
  11. package/.agents/instructions.md +113 -139
  12. package/.agents/rules/ci-remediation.md +11 -15
  13. package/.agents/rules/git-conventions-reference.md +48 -58
  14. package/.agents/rules/git-conventions.md +16 -22
  15. package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
  16. package/.agents/schemas/agentrc.schema.json +83 -254
  17. package/.agents/schemas/audit-rules.json +59 -1
  18. package/.agents/schemas/audit-rules.schema.json +33 -1
  19. package/.agents/schemas/lifecycle/README.md +1 -2
  20. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  21. package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
  22. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
  23. package/.agents/schemas/lifecycle/retro.end.schema.json +1 -1
  24. package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
  25. package/.agents/schemas/signal-event.schema.json +3 -3
  26. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  27. package/.agents/schemas/validation-evidence.schema.json +1 -1
  28. package/.agents/scripts/acceptance-eval.js +22 -66
  29. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  30. package/.agents/scripts/audit-to-stories.js +7 -7
  31. package/.agents/scripts/boot-sweep.js +1 -1
  32. package/.agents/scripts/bootstrap.js +3 -3
  33. package/.agents/scripts/check-dead-exports.js +43 -104
  34. package/.agents/scripts/check-doc-links.js +2 -2
  35. package/.agents/scripts/check-lifecycle-lint.js +7 -10
  36. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  37. package/.agents/scripts/cleanup-repo-test-temp.js +6 -1
  38. package/.agents/scripts/deliver-recover.js +122 -0
  39. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  40. package/.agents/scripts/evidence-gate.js +20 -50
  41. package/.agents/scripts/generate-skills-index.js +17 -1
  42. package/.agents/scripts/generate-workflows-doc.js +4 -4
  43. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  44. package/.agents/scripts/lib/Logger.js +6 -10
  45. package/.agents/scripts/lib/audit-suite/runner.js +2 -2
  46. package/.agents/scripts/lib/audit-suite/selector.js +328 -28
  47. package/.agents/scripts/lib/audit-to-stories/{seed-epic-from-findings.js → seed-from-findings.js} +9 -9
  48. package/.agents/scripts/lib/baselines/kernel.js +206 -18
  49. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -15
  50. package/.agents/scripts/lib/baselines/reader.js +1 -6
  51. package/.agents/scripts/lib/bdd-runner-detect.js +5 -9
  52. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  53. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +32 -33
  54. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  55. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  56. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  57. package/.agents/scripts/lib/checks/core-bare-clean.js +6 -3
  58. package/.agents/scripts/lib/checks/index.js +3 -2
  59. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  60. package/.agents/scripts/lib/checks/state.js +17 -248
  61. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +26 -24
  62. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  63. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  64. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  65. package/.agents/scripts/lib/cli/standard-args.js +13 -22
  66. package/.agents/scripts/lib/cli-args.js +39 -9
  67. package/.agents/scripts/lib/close-validation/gates.js +15 -15
  68. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  69. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  70. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  71. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  72. package/.agents/scripts/lib/config/ci.js +6 -31
  73. package/.agents/scripts/lib/config/delivery-routing.js +52 -35
  74. package/.agents/scripts/lib/config/explain.js +61 -48
  75. package/.agents/scripts/lib/config/github.js +7 -5
  76. package/.agents/scripts/lib/config/limits.js +29 -80
  77. package/.agents/scripts/lib/config/paths.js +0 -2
  78. package/.agents/scripts/lib/config/quality.js +12 -15
  79. package/.agents/scripts/lib/config/runners.js +20 -66
  80. package/.agents/scripts/lib/config/temp-paths.js +30 -63
  81. package/.agents/scripts/lib/config/worktree-isolation.js +0 -5
  82. package/.agents/scripts/lib/config-resolver.js +2 -7
  83. package/.agents/scripts/lib/config-settings-schema-delivery.js +55 -161
  84. package/.agents/scripts/lib/config-settings-schema-quality.js +17 -16
  85. package/.agents/scripts/lib/config-settings-schema.js +100 -60
  86. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  87. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  88. package/.agents/scripts/lib/dependency-parser.js +3 -2
  89. package/.agents/scripts/lib/doc-tiers.js +2 -2
  90. package/.agents/scripts/lib/duplicate-search.js +242 -41
  91. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
  92. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  93. package/.agents/scripts/lib/format-generated-json.js +97 -0
  94. package/.agents/scripts/lib/framework-version.js +19 -190
  95. package/.agents/scripts/lib/gh-exec.js +8 -0
  96. package/.agents/scripts/lib/git-branch-cleanup.js +1 -10
  97. package/.agents/scripts/lib/git-branch-lifecycle.js +17 -180
  98. package/.agents/scripts/lib/git-utils.js +32 -20
  99. package/.agents/scripts/lib/github/framework-repo.js +6 -0
  100. package/.agents/scripts/lib/json-utils.js +1 -2
  101. package/.agents/scripts/lib/label-constants.js +10 -38
  102. package/.agents/scripts/lib/label-taxonomy.js +10 -55
  103. package/.agents/scripts/lib/observability/active-story-env.js +44 -165
  104. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  105. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  106. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  107. package/.agents/scripts/lib/observability/source-classifier.js +5 -5
  108. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  109. package/.agents/scripts/lib/onboard/init-tail.js +1 -3
  110. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  111. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  112. package/.agents/scripts/lib/orchestration/ceremony-routing.js +105 -44
  113. package/.agents/scripts/lib/orchestration/code-review.js +78 -436
  114. package/.agents/scripts/lib/orchestration/column-sync.js +1 -1
  115. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +4 -4
  116. package/.agents/scripts/lib/orchestration/context-envelope.js +2 -5
  117. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  118. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  119. package/.agents/scripts/lib/orchestration/docs-digest.js +8 -8
  120. package/.agents/scripts/lib/orchestration/file-assumptions.js +7 -13
  121. package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
  122. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  123. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  124. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  125. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +17 -19
  126. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  127. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  128. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +6 -3
  129. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +21 -43
  130. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  131. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  132. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +4 -4
  133. package/.agents/scripts/lib/orchestration/merge-block-class.js +77 -21
  134. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  135. package/.agents/scripts/lib/orchestration/phase-runner.js +3 -2
  136. package/.agents/scripts/lib/orchestration/plan-context.js +354 -282
  137. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +11 -22
  138. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +4 -8
  139. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  140. package/.agents/scripts/lib/orchestration/plan-navigation.js +92 -0
  141. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +71 -0
  142. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +245 -0
  143. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  144. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +432 -858
  145. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +778 -0
  146. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +47 -115
  147. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  148. package/.agents/scripts/lib/orchestration/plan-reachability.js +9 -14
  149. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +1 -1
  150. package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/authoring-context.js +52 -51
  151. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +27 -0
  152. package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/spec-authoring-grounding.js +1 -1
  153. package/.agents/scripts/lib/orchestration/pr-base-guard.js +18 -28
  154. package/.agents/scripts/lib/orchestration/remote-verifier.js +1 -1
  155. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  156. package/.agents/scripts/lib/orchestration/resolves-token.js +1 -1
  157. package/.agents/scripts/lib/orchestration/retro-proposals.js +140 -79
  158. package/.agents/scripts/lib/orchestration/review-depth.js +126 -47
  159. package/.agents/scripts/lib/orchestration/review-providers/codex.js +2 -2
  160. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  161. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  162. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  163. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +21 -56
  164. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  165. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  166. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  167. package/.agents/scripts/lib/orchestration/run-epilogue.js +784 -0
  168. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +25 -1
  169. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  170. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  171. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +86 -41
  172. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  173. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +77 -32
  174. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  175. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  176. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +44 -0
  177. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  178. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  179. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +275 -46
  180. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  181. package/.agents/scripts/lib/orchestration/spec-freshness.js +14 -205
  182. package/.agents/scripts/lib/orchestration/spec-section-validator.js +4 -5
  183. package/.agents/scripts/lib/orchestration/spec-spill.js +60 -0
  184. package/.agents/scripts/lib/orchestration/split-policy-validator.js +188 -0
  185. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +49 -0
  186. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  187. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +28 -42
  188. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  189. package/.agents/scripts/lib/orchestration/story-follow-ups.js +298 -0
  190. package/.agents/scripts/lib/orchestration/story-init-remote.js +51 -0
  191. package/.agents/scripts/lib/orchestration/story-plan-state.js +33 -0
  192. package/.agents/scripts/lib/orchestration/structured-comment-parser.js +1 -1
  193. package/.agents/scripts/lib/orchestration/task-body-validator.js +60 -25
  194. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  195. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +130 -75
  196. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +192 -377
  197. package/.agents/scripts/lib/orchestration/ticket-validator.js +123 -25
  198. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -72
  199. package/.agents/scripts/lib/orchestration/ticketing/reads.js +45 -55
  200. package/.agents/scripts/lib/orchestration/ticketing/transition.js +66 -6
  201. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  202. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  203. package/.agents/scripts/lib/planning-corpus.js +12 -281
  204. package/.agents/scripts/lib/preflight-runner.js +4 -4
  205. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -5
  206. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  207. package/.agents/scripts/lib/signals/index.js +4 -17
  208. package/.agents/scripts/lib/signals/read.js +35 -35
  209. package/.agents/scripts/lib/signals/schema.js +8 -11
  210. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  211. package/.agents/scripts/lib/signals/write.js +0 -1
  212. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  213. package/.agents/scripts/lib/single-story-sweep/protection-ctx.js +1 -1
  214. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  215. package/.agents/scripts/lib/story-adjacency.js +17 -19
  216. package/.agents/scripts/lib/story-body/story-body.js +130 -75
  217. package/.agents/scripts/lib/story-plan.js +2 -4
  218. package/.agents/scripts/lib/templates/decomposer-prompts.js +51 -46
  219. package/.agents/scripts/lib/templates/spec-author-prompts.js +47 -45
  220. package/.agents/scripts/lib/test-env.js +14 -1
  221. package/.agents/scripts/lib/test-tiers.js +0 -3
  222. package/.agents/scripts/lib/{epic-body-sections.js → ticket-body-sections.js} +25 -39
  223. package/.agents/scripts/lib/validation-evidence.js +32 -60
  224. package/.agents/scripts/lib/wave-runner/ready-set.js +38 -12
  225. package/.agents/scripts/lib/workspace-provisioner.js +1 -1
  226. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  227. package/.agents/scripts/lib/worktree/lifecycle/reap.js +72 -25
  228. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  229. package/.agents/scripts/lint-issue-body.js +71 -40
  230. package/.agents/scripts/mandrel-update-preflight.js +1 -1
  231. package/.agents/scripts/notify.js +4 -3
  232. package/.agents/scripts/plan-context.js +102 -80
  233. package/.agents/scripts/plan-persist.js +230 -279
  234. package/.agents/scripts/plan-run-epilogue.js +142 -0
  235. package/.agents/scripts/post-structured-comment.js +0 -38
  236. package/.agents/scripts/pr-watch-with-update.js +43 -22
  237. package/.agents/scripts/providers/github/compose.js +0 -1
  238. package/.agents/scripts/providers/github/errors.js +0 -19
  239. package/.agents/scripts/providers/github/issues.js +13 -39
  240. package/.agents/scripts/providers/github/mappers.js +5 -12
  241. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  242. package/.agents/scripts/providers/github/tickets.js +33 -156
  243. package/.agents/scripts/providers/github.js +17 -6
  244. package/.agents/scripts/resolve-stories.js +236 -0
  245. package/.agents/scripts/run-coverage.js +4 -1
  246. package/.agents/scripts/run-lint.js +2 -2
  247. package/.agents/scripts/run-verify.js +31 -2
  248. package/.agents/scripts/signals-view.js +25 -21
  249. package/.agents/scripts/single-story-close.js +178 -26
  250. package/.agents/scripts/single-story-confirm-merge.js +313 -24
  251. package/.agents/scripts/single-story-init.js +35 -30
  252. package/.agents/scripts/stories-wave-tick.js +85 -10
  253. package/.agents/scripts/story-plan.js +28 -49
  254. package/.agents/scripts/update-ticket-state.js +14 -65
  255. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  256. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  257. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  258. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  259. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  260. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  261. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  262. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  263. package/.agents/skills/core/idea-refinement/SKILL.md +9 -9
  264. package/.agents/skills/core/scope-triage/SKILL.md +31 -172
  265. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  266. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  267. package/.agents/skills/skills.index.json +3 -53
  268. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  269. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  270. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  271. package/.agents/starter-agentrc.json +0 -5
  272. package/.agents/templates/agent-protocol.md +9 -10
  273. package/.agents/workflows/audit-architecture.md +6 -7
  274. package/.agents/workflows/audit-clean-code.md +7 -7
  275. package/.agents/workflows/audit-dependencies.md +3 -3
  276. package/.agents/workflows/audit-devops.md +3 -3
  277. package/.agents/workflows/audit-documentation.md +9 -10
  278. package/.agents/workflows/audit-lighthouse.md +11 -3
  279. package/.agents/workflows/audit-navigability.md +13 -2
  280. package/.agents/workflows/audit-performance.md +5 -6
  281. package/.agents/workflows/audit-privacy.md +3 -3
  282. package/.agents/workflows/audit-quality.md +11 -12
  283. package/.agents/workflows/audit-security.md +4 -5
  284. package/.agents/workflows/audit-seo.md +13 -3
  285. package/.agents/workflows/audit-sre.md +3 -3
  286. package/.agents/workflows/audit-to-stories.md +20 -20
  287. package/.agents/workflows/audit-ux-ui.md +10 -3
  288. package/.agents/workflows/deliver.md +177 -176
  289. package/.agents/workflows/git-cleanup.md +5 -6
  290. package/.agents/workflows/git-deliver.md +1 -1
  291. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  292. package/.agents/workflows/helpers/acceptance-self-eval.md +35 -40
  293. package/.agents/workflows/helpers/code-quality-guardrails.md +7 -7
  294. package/.agents/workflows/helpers/code-review.md +75 -196
  295. package/.agents/workflows/helpers/{single-story-deliver-reference.md → deliver-story-reference.md} +83 -44
  296. package/.agents/workflows/helpers/deliver-story.md +606 -0
  297. package/.agents/workflows/helpers/diagnose.md +10 -10
  298. package/.agents/workflows/helpers/parallel-tooling.md +3 -3
  299. package/.agents/workflows/helpers/signals.md +16 -16
  300. package/.agents/workflows/helpers/worktree-lifecycle.md +66 -86
  301. package/.agents/workflows/mandrel-update.md +2 -1
  302. package/.agents/workflows/plan.md +277 -145
  303. package/.agents/workflows/qa-assist.md +27 -33
  304. package/.agents/workflows/qa-explore.md +29 -38
  305. package/.agents/workflows/qa-run.md +2 -6
  306. package/README.md +9 -8
  307. package/bin/mandrel.js +12 -1
  308. package/docs/CHANGELOG.md +70 -0
  309. package/lib/cli/registry.js +262 -19
  310. package/lib/cli/sync-agents.js +157 -0
  311. package/lib/cli/sync-commands.js +115 -6
  312. package/lib/cli/sync.js +168 -6
  313. package/lib/cli/update.js +105 -8
  314. package/lib/cli/version-helpers.js +131 -0
  315. package/lib/migrations/README.md +7 -5
  316. package/lib/migrations/index.js +12 -8
  317. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  318. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  319. package/package.json +2 -2
  320. package/.agents/agents/retro.md +0 -42
  321. package/.agents/personas/architect.md +0 -113
  322. package/.agents/personas/devops-engineer.md +0 -38
  323. package/.agents/personas/engineer.md +0 -33
  324. package/.agents/personas/project-manager.md +0 -114
  325. package/.agents/personas/qa-engineer.md +0 -95
  326. package/.agents/personas/security-engineer.md +0 -111
  327. package/.agents/personas/technical-writer.md +0 -101
  328. package/.agents/schemas/dispatch-manifest.json +0 -232
  329. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  330. package/.agents/schemas/epic-spec.schema.json +0 -153
  331. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  332. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  333. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  334. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  335. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  336. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  337. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  338. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  339. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  340. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  341. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  342. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  343. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  344. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  345. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  346. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  347. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  348. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  349. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  350. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  351. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  352. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  353. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  354. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  355. package/.agents/schemas/lifecycle/slice.end.schema.json +0 -21
  356. package/.agents/schemas/lifecycle/slice.heartbeat.schema.json +0 -20
  357. package/.agents/schemas/lifecycle/slice.start.schema.json +0 -17
  358. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  359. package/.agents/schemas/risk-verdict.schema.json +0 -66
  360. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  361. package/.agents/scripts/acceptance-spec-reconciler.js +0 -642
  362. package/.agents/scripts/analyze-execution.js +0 -444
  363. package/.agents/scripts/bookkeeping-reconcile.js +0 -117
  364. package/.agents/scripts/check-prepush-recovery.js +0 -90
  365. package/.agents/scripts/dispatcher.js +0 -295
  366. package/.agents/scripts/epic-audit-prepare.js +0 -497
  367. package/.agents/scripts/epic-audit-recheck.js +0 -274
  368. package/.agents/scripts/epic-deliver-note-intervention.js +0 -192
  369. package/.agents/scripts/epic-deliver-preflight.js +0 -462
  370. package/.agents/scripts/epic-deliver-prepare.js +0 -852
  371. package/.agents/scripts/epic-execute-record-wave.js +0 -449
  372. package/.agents/scripts/epic-plan-clarity.js +0 -211
  373. package/.agents/scripts/epic-plan-healthcheck.js +0 -581
  374. package/.agents/scripts/epic-reconcile.js +0 -625
  375. package/.agents/scripts/lib/baseline-snapshot.js +0 -979
  376. package/.agents/scripts/lib/checks/epic-merge-lock-stale.js +0 -54
  377. package/.agents/scripts/lib/checks/stale-origin-epic.js +0 -49
  378. package/.agents/scripts/lib/config/lifecycle.js +0 -40
  379. package/.agents/scripts/lib/config/preflight.js +0 -58
  380. package/.agents/scripts/lib/config/retro.js +0 -77
  381. package/.agents/scripts/lib/epic-merge-lock.js +0 -322
  382. package/.agents/scripts/lib/epic-plan-clarity.js +0 -181
  383. package/.agents/scripts/lib/epic-plan-ideation.js +0 -261
  384. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  385. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  386. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -219
  387. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  388. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  389. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  390. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -270
  391. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +0 -539
  392. package/.agents/scripts/lib/orchestration/deliver-route.js +0 -173
  393. package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -134
  394. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +0 -183
  395. package/.agents/scripts/lib/orchestration/epic-cleanup.js +0 -801
  396. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +0 -310
  397. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +0 -163
  398. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +0 -140
  399. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +0 -64
  400. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +0 -72
  401. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +0 -156
  402. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +0 -345
  403. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +0 -41
  404. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/reconcile-spawn.js +0 -86
  405. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +0 -391
  406. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +0 -94
  407. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +0 -236
  408. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/risk-verdict.js +0 -105
  409. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +0 -307
  410. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +0 -117
  411. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +0 -117
  412. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +0 -621
  413. package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +0 -186
  414. package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +0 -50
  415. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +0 -129
  416. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +0 -103
  417. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +0 -267
  418. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -210
  419. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +0 -238
  420. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/_bullet-format.js +0 -32
  421. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +0 -203
  422. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +0 -227
  423. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +0 -117
  424. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/stalled-worktree.js +0 -37
  425. package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +0 -127
  426. package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +0 -400
  427. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +0 -276
  428. package/.agents/scripts/lib/orchestration/epic-runner/wave-scheduler.js +0 -66
  429. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +0 -789
  430. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +0 -676
  431. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-discriminator.js +0 -389
  432. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-format.js +0 -230
  433. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +0 -361
  434. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  435. package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +0 -306
  436. package/.agents/scripts/lib/orchestration/finalize/post-handoff-comment.js +0 -489
  437. package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +0 -88
  438. package/.agents/scripts/lib/orchestration/lifecycle/emit-slice-lifecycle.js +0 -270
  439. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +0 -147
  440. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  441. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  442. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +0 -465
  443. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +0 -501
  444. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +0 -984
  445. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +0 -264
  446. package/.agents/scripts/lib/orchestration/lifecycle/listeners/checkpoint-pointer-writer.js +0 -284
  447. package/.agents/scripts/lib/orchestration/lifecycle/listeners/cleaner.js +0 -355
  448. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +0 -673
  449. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +0 -378
  450. package/.agents/scripts/lib/orchestration/lifecycle/listeners/intervention-recorder.js +0 -140
  451. package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +0 -144
  452. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  453. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +0 -174
  454. package/.agents/scripts/lib/orchestration/manifest-builder.js +0 -222
  455. package/.agents/scripts/lib/orchestration/plan-persist/amend.js +0 -359
  456. package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +0 -127
  457. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  458. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  459. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  460. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  461. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -33
  462. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  463. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  464. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  465. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -396
  466. package/.agents/scripts/lib/orchestration/post-merge-pipeline.js +0 -205
  467. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  468. package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +0 -152
  469. package/.agents/scripts/lib/orchestration/retro/phases/checks.js +0 -94
  470. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +0 -571
  471. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +0 -450
  472. package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +0 -191
  473. package/.agents/scripts/lib/orchestration/retro-heuristics.js +0 -57
  474. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  475. package/.agents/scripts/lib/orchestration/retro-runner.js +0 -197
  476. package/.agents/scripts/lib/orchestration/spec-renderer.js +0 -447
  477. package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +0 -747
  478. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/gate-failure.js +0 -211
  479. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +0 -158
  480. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +0 -446
  481. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +0 -297
  482. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/scope-discovery.js +0 -48
  483. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution-wiring.js +0 -67
  484. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution.js +0 -161
  485. package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +0 -117
  486. package/.agents/scripts/lib/orchestration/story-close/cd-out-guard.js +0 -86
  487. package/.agents/scripts/lib/orchestration/story-close/cleanup-reconciler.js +0 -147
  488. package/.agents/scripts/lib/orchestration/story-close/close-inputs.js +0 -142
  489. package/.agents/scripts/lib/orchestration/story-close/comment-bodies.js +0 -62
  490. package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +0 -658
  491. package/.agents/scripts/lib/orchestration/story-close/merge-subject.js +0 -198
  492. package/.agents/scripts/lib/orchestration/story-close/phases/branch-restore.js +0 -105
  493. package/.agents/scripts/lib/orchestration/story-close/phases/close.js +0 -222
  494. package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +0 -292
  495. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +0 -270
  496. package/.agents/scripts/lib/orchestration/story-close/phases/preflight.js +0 -110
  497. package/.agents/scripts/lib/orchestration/story-close/phases/refresh.js +0 -86
  498. package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked-emitter.js +0 -112
  499. package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked.js +0 -157
  500. package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +0 -421
  501. package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +0 -302
  502. package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +0 -163
  503. package/.agents/scripts/lib/orchestration/story-close-recovery.js +0 -690
  504. package/.agents/scripts/lib/orchestration/wave-marker.js +0 -28
  505. package/.agents/scripts/lib/orchestration/wave-record-io.js +0 -218
  506. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +0 -145
  507. package/.agents/scripts/lib/orchestration/wave-record-projection.js +0 -212
  508. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +0 -111
  509. package/.agents/scripts/lib/presentation/manifest-builder.js +0 -239
  510. package/.agents/scripts/lib/presentation/manifest-formatter.js +0 -242
  511. package/.agents/scripts/lib/presentation/manifest-helpers.js +0 -213
  512. package/.agents/scripts/lib/presentation/manifest-persistence.js +0 -261
  513. package/.agents/scripts/lib/presentation/manifest-procedures.js +0 -55
  514. package/.agents/scripts/lib/presentation/manifest-render-waves.js +0 -306
  515. package/.agents/scripts/lib/presentation/manifest-renderer.js +0 -188
  516. package/.agents/scripts/lib/presentation/manifest-story-views.js +0 -110
  517. package/.agents/scripts/lib/push-epic-retry.js +0 -209
  518. package/.agents/scripts/lib/spec/index.js +0 -36
  519. package/.agents/scripts/lib/spec/loader.js +0 -425
  520. package/.agents/scripts/lib/spec/state.js +0 -208
  521. package/.agents/scripts/lib/story-init/blocker-validator.js +0 -68
  522. package/.agents/scripts/lib/story-init/branch-initializer.js +0 -408
  523. package/.agents/scripts/lib/story-init/context-resolver.js +0 -92
  524. package/.agents/scripts/lib/story-init/donor-precheck.js +0 -207
  525. package/.agents/scripts/lib/story-init/state-transitioner.js +0 -80
  526. package/.agents/scripts/lib/story-init/task-graph-builder.js +0 -124
  527. package/.agents/scripts/lib/story-init/transition-summary.js +0 -34
  528. package/.agents/scripts/lib/test-reserved-epic-temp-ids.js +0 -35
  529. package/.agents/scripts/lib/wave-runner/tick.js +0 -754
  530. package/.agents/scripts/lib/wave-runner/wave-runner-error.js +0 -20
  531. package/.agents/scripts/lifecycle-emit-story-dispatch.js +0 -194
  532. package/.agents/scripts/lifecycle-emit.js +0 -510
  533. package/.agents/scripts/retro-run.js +0 -218
  534. package/.agents/scripts/slice-phase.js +0 -361
  535. package/.agents/scripts/standalone-feedback-rollup.js +0 -188
  536. package/.agents/scripts/story-close.js +0 -294
  537. package/.agents/scripts/story-init.js +0 -599
  538. package/.agents/scripts/story-phase.js +0 -369
  539. package/.agents/scripts/wave-tick.js +0 -464
  540. package/.agents/skills/core/analyze-execution/SKILL.md +0 -98
  541. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +0 -172
  542. package/.agents/skills/core/epic-plan-consolidate/examples.md +0 -51
  543. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +0 -441
  544. package/.agents/skills/core/epic-plan-decompose-author/examples.md +0 -47
  545. package/.agents/skills/core/epic-plan-premortem/SKILL.md +0 -146
  546. package/.agents/skills/core/epic-plan-premortem/examples.md +0 -53
  547. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +0 -383
  548. package/.agents/skills/core/epic-plan-spec-author/examples.md +0 -91
  549. package/.agents/workflows/helpers/deliver-epic-reference.md +0 -547
  550. package/.agents/workflows/helpers/deliver-epic-single.md +0 -331
  551. package/.agents/workflows/helpers/deliver-epic.md +0 -998
  552. package/.agents/workflows/helpers/deliver-stories.md +0 -450
  553. package/.agents/workflows/helpers/epic-audit.md +0 -189
  554. package/.agents/workflows/helpers/epic-deliver-story.md +0 -436
  555. package/.agents/workflows/helpers/epic-testing.md +0 -125
  556. package/.agents/workflows/helpers/plan-epic-reference.md +0 -160
  557. package/.agents/workflows/helpers/plan-epic.md +0 -353
  558. package/.agents/workflows/helpers/plan-story.md +0 -251
  559. package/.agents/workflows/helpers/scope-triage-gate.md +0 -108
  560. package/.agents/workflows/helpers/single-story-deliver.md +0 -557
@@ -1,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
- }