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,754 +0,0 @@
1
- /**
2
- * `tick({ epic, collaborators })` — single callable entry point for
3
- * "advance this Epic one beat." Stateless adapter over the continuous
4
- * ready-set scheduling core (`lib/wave-runner/ready-set.js`).
5
- *
6
- * Story #4155 (Epic #4151) — the Epic `/deliver` runtime cut over from
7
- * the wave-batch scheduler to the ready-set core. Each tick:
8
- *
9
- * 1. reads the shrunk `epic-run-state` checkpoint (per-Story status map
10
- * + the GLOBAL in-flight `concurrencyCap`),
11
- * 2. re-fetches the **live** Story records (body + labels + issue
12
- * state) for every Story in scope,
13
- * 3. classifies each by live label (`classifyStory`), re-derives
14
- * adjacency from the live bodies (`buildStoryAdjacency`, inside
15
- * `selectReadySet`) and selects the ready set under a global
16
- * in-flight cap with the file-overlap co-dispatch guard
17
- * (`storiesOverlap`),
18
- * 4. returns a `WaveTickResult` describing the next action.
19
- *
20
- * There is **no wave barrier**: a Story whose own dependencies are all
21
- * done is dispatched the instant a slot is free, even while an unrelated
22
- * sibling Story is still `agent::executing`. The selector neither reads
23
- * GitHub nor a checkpoint nor the ledger — this adapter supplies the live
24
- * records, the `inFlight` count (from the lifecycle ledger), and the
25
- * `globalCap`, then maps its return into the `WaveTickResult` envelope.
26
- *
27
- * Contract (Story #1430, refined by #4155): stateless; caller owns
28
- * concurrency, worktrees, and the checkpoint. Expected failures (blocked
29
- * stories) flow back through result fields; unexpected failures (GH 5xx,
30
- * malformed / old-shape checkpoint) throw `WaveRunnerError`.
31
- *
32
- * Story #4183 — the `tick(args)` orchestrator was a 252-line SRP /
33
- * cognitive-load hotspot carrying six distinct responsibilities in one
34
- * body. It is now a thin coordinator (Coordinator-plus-Phases pattern,
35
- * `docs/patterns.md`) that wires four extracted stages:
36
- * `resolveTickCollaborators` (collaborator/fallback resolution),
37
- * `readAndValidateCheckpoint` (checkpoint read + shape validation, folding
38
- * in `assertNotOldShape`), `refetchStoryRecords` (force-fresh re-fetch),
39
- * and the **pure** `planTick` (classification → cycle detection → ready-set
40
- * selection → dispatch decision, returning the signals to emit rather than
41
- * emitting them, so it carries no I/O). The exported `tick(args)`
42
- * signature, the `tickResult` / `withInFlight` envelope shapes, and every
43
- * `WaveRunnerError` code are preserved verbatim — callers and tests are
44
- * unchanged.
45
- *
46
- * @module lib/wave-runner/tick
47
- */
48
-
49
- import { existsSync, readFileSync } from 'node:fs';
50
-
51
- import { epicLedgerPath } from '../config/temp-paths.js';
52
- import { detectCycle } from '../Graph.js';
53
- import { AGENT_LABELS } from '../label-constants.js';
54
- import { appendEpicSignal } from '../observability/signals-writer.js';
55
- import * as epicRunStateStoreModule from '../orchestration/epic-run-state-store.js';
56
- import { detectRecurringFailures } from '../orchestration/recurring-failure-detector.js';
57
- import { upsertStructuredComment as defaultUpsertStructuredComment } from '../orchestration/ticketing.js';
58
- import { buildStoryAdjacency } from '../story-adjacency.js';
59
-
60
- import { classifyStory, selectReadySet, storyIdOf } from './ready-set.js';
61
- import { WaveRunnerError } from './wave-runner-error.js';
62
-
63
- /**
64
- * The checkpoint fields whose presence marks an **old-shape** (wave-batch)
65
- * `epic-run-state` comment. The ready-set runtime cannot mis-schedule
66
- * against a wave-indexed plan — indexing the old wave grouping would silently
67
- * dispatch the wrong stories — so the tick fails closed on any of these
68
- * fields with an explicit operator message rather than guessing.
69
- */
70
- const OLD_SHAPE_FIELDS = Object.freeze(['plan', 'currentWave', 'totalWaves']);
71
-
72
- /**
73
- * Advance the Epic one beat. Returns a `WaveTickResult`:
74
- *
75
- * nextAction: { kind: 'dispatch', stories: [{ id, title? }, ...] }
76
- * | { kind: 'observe', waitingOn: number[] }
77
- * | { kind: 'halt', reason: string, stuckStories: number[],
78
- * cycle?: number[] }
79
- * | { kind: 'epic-complete' }
80
- * blockedStories: [{ storyId, reason, detail? }, ...]
81
- * gateFailures: [{ storyId, gate, detail? }, ...]
82
- * readyCount: number // size of the ready set this beat
83
- * inFlight: number[] // ledger-derived dispatched-not-yet-ended ids
84
- *
85
- * Readiness comes entirely from the **live** Story bodies + labels — the
86
- * checkpoint contributes only the Story set in scope and the global cap.
87
- *
88
- * `epic-complete` is returned **only** when every in-scope Story is done.
89
- * If the ready set is empty and nothing is in flight but at least one Story
90
- * is still not done — a Story gated on an unsatisfiable dependency
91
- * (a dependency cycle, or a `blocked by #N` that survived adjacency closure)
92
- * — the tick returns a non-terminal `halt` naming the stuck Story ids rather
93
- * than silently reporting the Epic complete and stranding the Story. A
94
- * dependency cycle among the in-scope Stories is likewise surfaced as a
95
- * `halt` (with the offending `cycle`), never collapsed to `epic-complete`.
96
- *
97
- * Coordinator (Story #4183): this function is a thin dispatcher. It resolves
98
- * collaborators, reads + validates the checkpoint, re-fetches the live Story
99
- * records, runs the best-effort recurring-failure scan, delegates the pure
100
- * dispatch decision to `planTick`, then drains the `signals` `planTick`
101
- * returned through the configured emitter. Each stage is an independently
102
- * testable helper below.
103
- *
104
- * @typedef {object} WaveTickArgs
105
- * @property {number | { id: number }} epic
106
- * @property {{
107
- * provider?: object,
108
- * epicRunStateStore?: { read: () => Promise<object|null> },
109
- * signalEmit?: (signal: object) => Promise<unknown>,
110
- * inFlightReader?: () => Promise<number[]>,
111
- * recurringFailureReporter?: () => Promise<void>,
112
- * }} [collaborators]
113
- * @property {{ provider?: object, config?: object }} [ctx]
114
- *
115
- * @param {WaveTickArgs} args
116
- */
117
- export async function tick(args = {}) {
118
- const { epicId, provider, epicRunStateStore, emit, inFlightReader, ctx } =
119
- resolveTickCollaborators(args);
120
-
121
- const state = await readAndValidateCheckpoint(epicRunStateStore, epicId);
122
-
123
- const storyIds = checkpointStoryIds(state);
124
-
125
- if (storyIds.length === 0) {
126
- // No Stories in scope — the Epic has nothing to dispatch.
127
- return tickResult({
128
- nextAction: withInFlight({ kind: 'epic-complete' }, []),
129
- readyCount: 0,
130
- inFlight: [],
131
- });
132
- }
133
-
134
- // Re-fetch the live Story records (body + labels + issue state) for every
135
- // Story in scope. In-flight Stories are force-fresh-fetched so a label that
136
- // flipped since the last tick is observed; every other Story serves from
137
- // the provider's in-process cache.
138
- const inFlight = await safeReadInFlight(inFlightReader);
139
- const inFlightSet = new Set(inFlight);
140
- const records = await refetchStoryRecords(provider, storyIds, inFlightSet);
141
-
142
- // Best-effort recurring-failure scan (≥2 distinct Stories sharing the same
143
- // `close-validate.end` failedGate). Idempotent across re-ticks; a reporter
144
- // throw must not crash the planner.
145
- const recurringFailureReporter =
146
- args.collaborators?.recurringFailureReporter ??
147
- defaultRecurringFailureReporter({ provider, epicId, config: ctx?.config });
148
- await safeReportRecurringFailures(recurringFailureReporter);
149
-
150
- // Decide the next action from the live records + ledger in-flight set. The
151
- // decision is pure (no I/O); the signals it wants emitted come back in
152
- // `plan.signals` and are drained by the coordinator below.
153
- const plan = planTick(state, records, inFlight);
154
- for (const signal of plan.signals) {
155
- await emit(signal);
156
- }
157
-
158
- return tickResult({
159
- nextAction: withInFlight(plan.nextAction, inFlight),
160
- blockedStories: plan.blockedStories,
161
- gateFailures: plan.gateFailures,
162
- readyCount: plan.readyCount,
163
- inFlight,
164
- });
165
- }
166
-
167
- /**
168
- * Resolve the Epic id and the five injectable collaborators (with their
169
- * production-default fallbacks) from the `tick` args. The single home for the
170
- * collaborator/fallback wiring so the coordinator stays declarative.
171
- *
172
- * Throws `WaveRunnerError('invalid-input')` when the epic id is not a
173
- * positive integer (or `{ id: positiveInt }`) or when no provider is supplied
174
- * via either `collaborators.provider` or `ctx.provider`.
175
- *
176
- * @param {WaveTickArgs} args
177
- * @returns {{
178
- * epicId: number,
179
- * provider: object,
180
- * epicRunStateStore: { read: () => Promise<object|null> },
181
- * emit: (signal: object) => Promise<unknown>,
182
- * inFlightReader: () => Promise<number[]>,
183
- * ctx: object,
184
- * }}
185
- */
186
- function resolveTickCollaborators(args) {
187
- const epicId = resolveEpicId(args.epic);
188
- const {
189
- provider: collabProvider,
190
- epicRunStateStore: collabStore,
191
- signalEmit,
192
- inFlightReader: collabInFlightReader,
193
- } = args.collaborators ?? {};
194
- const ctx = args.ctx ?? {};
195
- const provider = collabProvider ?? ctx.provider;
196
- if (!provider) {
197
- throw new WaveRunnerError('invalid-input', 'provider is required');
198
- }
199
- // The ready-set tick is stateless. When the caller does not supply a
200
- // collaborator shim, read the `epic-run-state` structured comment directly
201
- // via the function-based store.
202
- const epicRunStateStore = collabStore ?? {
203
- read: () => epicRunStateStoreModule.read({ provider, epicId }),
204
- };
205
- const emit = signalEmit ?? defaultSignalEmit(epicId, ctx);
206
- const inFlightReader =
207
- collabInFlightReader ?? (() => defaultInFlightReader(epicId, ctx?.config));
208
- return { epicId, provider, epicRunStateStore, emit, inFlightReader, ctx };
209
- }
210
-
211
- /**
212
- * Read the `epic-run-state` checkpoint via the store, validate its shape, and
213
- * fail closed on a pre-ready-set (wave-batch) checkpoint.
214
- *
215
- * Throws:
216
- * - `WaveRunnerError('checkpoint-read')` when the store read rejects,
217
- * - `WaveRunnerError('checkpoint-missing')` when the read resolves to a
218
- * non-object (no comment),
219
- * - `WaveRunnerError('old-shape-checkpoint')` when the checkpoint still
220
- * carries a `plan` / `currentWave` / `totalWaves` field (via
221
- * `assertNotOldShape`). A `plan` / `currentWave` / `totalWaves` comment
222
- * predates the ready-set cutover (Story #4155); the ready-set runtime
223
- * would otherwise ignore those fields and re-derive readiness from live
224
- * labels — silently discarding an in-progress wave-batch run's resume
225
- * pointer. Refuse with an explicit operator remediation instead.
226
- *
227
- * @param {{ read: () => Promise<object|null> }} store
228
- * @param {number} epicId
229
- * @returns {Promise<object>} the validated checkpoint state.
230
- */
231
- async function readAndValidateCheckpoint(store, epicId) {
232
- let state;
233
- try {
234
- state = await store.read();
235
- } catch (err) {
236
- throw new WaveRunnerError('checkpoint-read', err);
237
- }
238
- if (!state || typeof state !== 'object') {
239
- throw new WaveRunnerError(
240
- 'checkpoint-missing',
241
- `no epic-run-state comment on Epic #${epicId}`,
242
- );
243
- }
244
- assertNotOldShape(state, epicId);
245
- return state;
246
- }
247
-
248
- /**
249
- * Re-fetch the live Story records (body + labels + issue state) for every
250
- * Story in scope. The body feeds `buildStoryAdjacency` (inside
251
- * `selectReadySet`) so the dependency edges are always read from the current
252
- * ticket text, never a stale checkpoint snapshot. Stories in `inFlightSet`
253
- * are force-fresh-fetched (`{ fresh: true }`) so a label that flipped since
254
- * the last tick is observed; every other Story serves from the provider's
255
- * in-process cache.
256
- *
257
- * Throws `WaveRunnerError('story-fetch')` when any `provider.getTicket`
258
- * rejects.
259
- *
260
- * @param {{ getTicket: (id: number, opts?: object) => Promise<object> }} provider
261
- * @param {number[]} storyIds Ascending, deduped in-scope Story ids.
262
- * @param {Set<number>} inFlightSet Ledger-derived in-flight Story ids.
263
- * @returns {Promise<Array<object>>} normalized Story records.
264
- */
265
- async function refetchStoryRecords(provider, storyIds, inFlightSet) {
266
- try {
267
- return await Promise.all(
268
- storyIds.map(async (id) => {
269
- const opts = inFlightSet.has(id) ? { fresh: true } : {};
270
- const ticket = await provider.getTicket(id, opts);
271
- return {
272
- id,
273
- title: ticket?.title,
274
- body: ticket?.body ?? '',
275
- labels: Array.isArray(ticket?.labels) ? ticket.labels : [],
276
- state: ticket?.state,
277
- // Forward every declared file-footprint shape so the selector's
278
- // overlap co-dispatch guard (`storiesOverlap`) can withhold two
279
- // Stories that would race the same path on parallel branches.
280
- files: Array.isArray(ticket?.files) ? ticket.files : undefined,
281
- changes: Array.isArray(ticket?.changes) ? ticket.changes : undefined,
282
- changeset: Array.isArray(ticket?.changeset)
283
- ? ticket.changeset
284
- : undefined,
285
- };
286
- }),
287
- );
288
- } catch (err) {
289
- throw new WaveRunnerError('story-fetch', err);
290
- }
291
- }
292
-
293
- /**
294
- * Pure dispatch planner — the scheduler tick's decision core with **no I/O**.
295
- * Given the parsed checkpoint, the live Story records, and the ledger-derived
296
- * in-flight id list, it classifies every Story, detects a sibling dependency
297
- * cycle, selects the ready set under the global in-flight cap, and decides the
298
- * `nextAction`. It performs no fetching, no signal emission, and no ledger
299
- * read: the two wave-window forensics signals are returned in the `signals`
300
- * array for the coordinator to drain, so this function stays independently
301
- * unit-testable against fixture records without a provider stub or an emitter.
302
- *
303
- * @param {object} state Parsed `epic-run-state` checkpoint (for the global
304
- * cap and the per-Story `failed` rows surfaced as gate failures).
305
- * @param {Array<object>} records Live Story records (id, title, body, labels,
306
- * state, file-footprint shapes).
307
- * @param {number[]} inFlight Ledger-derived dispatched-not-yet-ended ids.
308
- * @returns {{
309
- * nextAction: object,
310
- * blockedStories: Array<{ storyId: number, reason: string, detail?: string }>,
311
- * gateFailures: Array<{ storyId: number, gate: string, detail?: string }>,
312
- * readyCount: number,
313
- * signals: Array<object>,
314
- * }}
315
- */
316
- export function planTick(state, records, inFlight) {
317
- const globalCap = positiveIntOrZero(state.concurrencyCap);
318
- const inFlightSet = new Set(inFlight);
319
-
320
- // 1. Classify by live label. `done` / `blocked` / `executing` / `ready`.
321
- const byClass = { done: [], blocked: [], executing: [], ready: [] };
322
- for (const rec of records) {
323
- byClass[classifyStory(rec)].push(rec);
324
- }
325
-
326
- // 1a. Detect a dependency cycle among the in-scope Stories BEFORE selecting.
327
- // A cycle makes every Story on it permanently un-eligible (no member's
328
- // deps can all be done), so `selectReadySet` would return an empty set
329
- // and the terminal decision could otherwise mistake the stall for
330
- // completion. Surface it as a `halt` so the workflow parks the Epic on
331
- // a diagnosable condition instead of silently dropping the cycle. Build
332
- // adjacency with `dropForeign: true` to match the Epic-scoped semantics
333
- // (a cycle is only meaningful over the scheduled sibling set). Mirrors
334
- // the cycle handling in `stories-wave-tick.js`.
335
- const epicAdjacency = buildStoryAdjacency(records, { dropForeign: true });
336
- const cycle = detectCycle(epicAdjacency);
337
-
338
- // 2. Select the ready set under the GLOBAL in-flight cap. The selector
339
- // re-derives adjacency from the live bodies (with `dropForeign: true` so
340
- // a `blocked by #N` whose target is outside this Epic's Story set — a
341
- // foreign id or a typo — is pruned rather than treated as a permanent
342
- // unsatisfiable gate that strands the dependent), and applies the
343
- // file-overlap co-dispatch guard, returning the deterministic,
344
- // overlap-free, dependency-satisfied subset capped at the remaining
345
- // slots.
346
- //
347
- // A Story recorded in-flight on the ledger (`story.dispatch.start`
348
- // without a matching `.end`) but whose label has not yet flipped to
349
- // `agent::executing` (the child is mid-`story-init`, or the host crashed
350
- // after the dispatch-ledger write but before the label flip) still reads
351
- // as `ready` by label alone. Re-dispatching it would put a second agent
352
- // on the same `story-<id>` branch — the worst failure mode in the
353
- // system. So the candidate set passed to the selector marks those
354
- // Stories `executing`: they keep occupying a slot (and gate any
355
- // dependent, since they are not done) but are never re-selected.
356
- //
357
- // The slot denominator is the size of the UNION of (a) ledger-in-flight
358
- // ids and (b) Stories carrying `agent::executing` by label. A Story that
359
- // flipped to `agent::executing` but whose `story.dispatch.start` never
360
- // landed in the ledger (e.g. the label flip raced ahead of the ledger
361
- // write) occupies a real slot the ledger count alone misses; counting
362
- // only the ledger would let the global cap be exceeded. The union is the
363
- // authoritative occupied-slot count.
364
- const candidates = records.map((rec) =>
365
- inFlightSet.has(rec.id) && classifyStory(rec) === 'ready'
366
- ? { ...rec, labels: [...rec.labels, AGENT_LABELS.EXECUTING] }
367
- : rec,
368
- );
369
- const doneIds = byClass.done.map((s) => s.id);
370
- const occupiedSlotIds = new Set([
371
- ...inFlight,
372
- ...byClass.executing.map((s) => s.id),
373
- ]);
374
- const readySet = selectReadySet({
375
- stories: candidates,
376
- doneIds,
377
- inFlight: occupiedSlotIds.size,
378
- globalCap,
379
- dropForeign: true,
380
- });
381
-
382
- const blockedStories = byClass.blocked.map((s) => ({
383
- storyId: s.id,
384
- reason: 'agent::blocked',
385
- detail: s.title,
386
- }));
387
- const gateFailures = readGateFailures(state);
388
-
389
- // 3. Decide nextAction.
390
- // - A blocked Story halts the Epic → observe (the workflow flips the
391
- // Epic to agent::blocked and parks).
392
- // - A dependency cycle among the in-scope Stories halts the Epic → halt
393
- // (the cycle is an unsatisfiable gate; never collapse it to complete).
394
- // - A non-empty ready set → dispatch it. Fire `wave-start` on the very
395
- // first dispatch of the run (nothing executing / in-flight / done
396
- // yet) so the perf-aggregator can bracket the run's wall-clock.
397
- // - Otherwise, if any Story is still executing or in-flight → observe.
398
- // - Otherwise, if EVERY in-scope Story is done → epic-complete.
399
- // - Otherwise the ready set is empty, nothing is in flight, yet not all
400
- // Stories are done: at least one Story is permanently gated (an
401
- // unsatisfiable dependency that survived adjacency closure). Halt and
402
- // name the stuck Story ids — never silently report the Epic complete.
403
- const allDone = byClass.done.length === records.length;
404
- const signals = [];
405
- let nextAction;
406
- if (blockedStories.length) {
407
- nextAction = {
408
- kind: 'observe',
409
- waitingOn: byClass.blocked.map((s) => s.id).sort((a, b) => a - b),
410
- };
411
- } else if (cycle) {
412
- const cycleIds = cycle
413
- .filter((id) => Number.isInteger(id))
414
- .sort((a, b) => a - b);
415
- nextAction = {
416
- kind: 'halt',
417
- reason: 'dependency-cycle',
418
- stuckStories: cycleIds,
419
- cycle,
420
- };
421
- } else if (readySet.length) {
422
- if (
423
- byClass.executing.length === 0 &&
424
- byClass.done.length === 0 &&
425
- inFlight.length === 0
426
- ) {
427
- signals.push({
428
- // `index` is what the perf-aggregator's `bucketWaves` keys on to
429
- // bracket a wave's wall-clock (Epic #4406 / Story #4413). Without
430
- // it every wave-start collapsed to NaN and the waveParallelism
431
- // table rendered structurally empty. The continuous ready-set
432
- // scheduler runs a single logical wave, so `index: 0`.
433
- kind: 'wave-start',
434
- index: 0,
435
- stories: records.map((s) => ({ id: s.id, title: s.title })),
436
- });
437
- }
438
- nextAction = {
439
- kind: 'dispatch',
440
- stories: readySet.map((s) => ({
441
- id: storyIdOf(s),
442
- title: s.title,
443
- })),
444
- };
445
- } else if (byClass.executing.length || inFlight.length) {
446
- const waitingOn = [
447
- ...new Set([...byClass.executing.map((s) => s.id), ...inFlight]),
448
- ].sort((a, b) => a - b);
449
- nextAction = { kind: 'observe', waitingOn };
450
- } else if (allDone) {
451
- // Every Story is done and nothing is in flight: the run is complete.
452
- signals.push({ kind: 'wave-complete', index: 0 });
453
- nextAction = { kind: 'epic-complete' };
454
- } else {
455
- // Ready set empty, nothing in flight, but not all Stories are done — a
456
- // Story is gated on an unsatisfiable dependency. Halt with the stuck ids
457
- // (every not-done, not-in-flight Story) so the operator can see exactly
458
- // which Story stranded the run instead of a false epic-complete.
459
- const stuckStories = records
460
- .filter((rec) => classifyStory(rec) !== 'done')
461
- .map((rec) => rec.id)
462
- .filter((id) => Number.isInteger(id))
463
- .sort((a, b) => a - b);
464
- nextAction = {
465
- kind: 'halt',
466
- reason: 'unsatisfiable-dependency',
467
- stuckStories,
468
- };
469
- }
470
-
471
- return {
472
- nextAction,
473
- blockedStories,
474
- gateFailures,
475
- readyCount: readySet.length,
476
- signals,
477
- };
478
- }
479
-
480
- /**
481
- * Throw `WaveRunnerError('old-shape-checkpoint')` when the checkpoint still
482
- * carries any wave-batch field. The message names the offending field(s) and
483
- * the operator remediation so a stuck delivery is diagnosable from the
484
- * thrown error alone.
485
- *
486
- * @param {object} state Parsed checkpoint.
487
- * @param {number} epicId
488
- */
489
- function assertNotOldShape(state, epicId) {
490
- const present = OLD_SHAPE_FIELDS.filter((f) => Object.hasOwn(state, f));
491
- if (present.length === 0) return;
492
- throw new WaveRunnerError(
493
- 'old-shape-checkpoint',
494
- `Epic #${epicId} carries a pre-ready-set (wave-batch) epic-run-state ` +
495
- `checkpoint (fields: ${present.join(', ')}). The ready-set /deliver ` +
496
- `runtime cannot resume a wave-batch run. Re-run ` +
497
- `\`node .agents/scripts/epic-deliver-prepare.js --epic ${epicId}\` to ` +
498
- `re-seed the checkpoint in the per-Story-status shape, then re-run ` +
499
- `/deliver.`,
500
- );
501
- }
502
-
503
- /**
504
- * Extract the in-scope Story ids from the shrunk checkpoint's per-Story
505
- * `stories` status map (`{ [storyId]: { status, ... } }`). Returns an
506
- * ascending-sorted, deduped array of positive integers; tolerates an absent
507
- * / malformed map by returning `[]`.
508
- *
509
- * @param {object} state
510
- * @returns {number[]}
511
- */
512
- function checkpointStoryIds(state) {
513
- const stories = state?.stories;
514
- if (!stories || typeof stories !== 'object') return [];
515
- const ids = new Set();
516
- for (const key of Object.keys(stories)) {
517
- const id = Number(key);
518
- if (Number.isInteger(id) && id > 0) ids.add(id);
519
- }
520
- return [...ids].sort((a, b) => a - b);
521
- }
522
-
523
- /**
524
- * Attach the ledger-derived in-flight Story-id list to a `nextAction`
525
- * envelope under the `in-flight` key. Always present (empty array when the
526
- * ledger is silent) so downstream consumers pattern-match on presence
527
- * without an existence check.
528
- *
529
- * @param {object} nextAction
530
- * @param {number[]} inFlight
531
- * @returns {object} the same nextAction (mutated) for call-site convenience
532
- */
533
- function withInFlight(nextAction, inFlight) {
534
- nextAction['in-flight'] = inFlight;
535
- return nextAction;
536
- }
537
-
538
- /**
539
- * Wrap the configured `inFlightReader` with a defensive guard so an
540
- * unreadable ledger never crashes the tick. The default reader already
541
- * returns `[]` on missing files; this catches any other shape of
542
- * accidental throw and degrades to an empty list so the planner can
543
- * still make a decision.
544
- *
545
- * @param {() => Promise<number[]>} reader
546
- * @returns {Promise<number[]>}
547
- */
548
- async function safeReadInFlight(reader) {
549
- try {
550
- const raw = await reader();
551
- return Array.isArray(raw) ? raw.filter((n) => Number.isInteger(n)) : [];
552
- } catch {
553
- return [];
554
- }
555
- }
556
-
557
- /**
558
- * Default `recurringFailureReporter` collaborator — reads the per-Epic
559
- * lifecycle ledger via `detectRecurringFailures`, and when at least one
560
- * recurring-failure-class finding is returned, upserts a
561
- * `recurring-failure-class` structured comment on the Epic ticket.
562
- *
563
- * The body carries the findings array verbatim in a JSON fence plus a
564
- * compact human-readable bullet list keyed by gate. Idempotent across
565
- * re-ticks: `upsertStructuredComment` diffs body bytes, so a tick that
566
- * produces the same findings does not generate a new comment.
567
- *
568
- * Story #3062 (Epic #3051).
569
- *
570
- * @param {object} args
571
- * @param {object} args.provider Ticketing provider passed to upsert.
572
- * @param {number} args.epicId
573
- * @param {object} [args.config]
574
- * @returns {() => Promise<void>}
575
- */
576
- function defaultRecurringFailureReporter({ provider, epicId, config }) {
577
- return async () => {
578
- const ledgerPath = epicLedgerPath(epicId, config);
579
- const findings = detectRecurringFailures(epicId, { ledgerPath });
580
- if (findings.length === 0) return;
581
- const body = renderRecurringFailureBody(findings);
582
- await defaultUpsertStructuredComment(
583
- provider,
584
- epicId,
585
- 'recurring-failure-class',
586
- body,
587
- );
588
- };
589
- }
590
-
591
- /**
592
- * Render the comment body the recurring-failure-class reporter upserts.
593
- * The body is deterministic given a deterministic findings array (the
594
- * detector sorts findings by gate and storyIds ascending), which is what
595
- * makes the upsert idempotent across re-ticks.
596
- *
597
- * @param {Array<{gate: string, storyIds: number[], firstSeenAt: string, lastSeenAt: string}>} findings
598
- * @returns {string}
599
- */
600
- export function renderRecurringFailureBody(findings) {
601
- const lines = ['### 🔁 Recurring failure classes detected', ''];
602
- for (const f of findings) {
603
- const storiesList = f.storyIds.map((id) => `#${id}`).join(', ');
604
- lines.push(
605
- `- **\`${f.gate}\`** — ${f.storyIds.length} stories (${storiesList}); first \`${f.firstSeenAt}\`, last \`${f.lastSeenAt}\``,
606
- );
607
- }
608
- lines.push('');
609
- lines.push('```json');
610
- lines.push(
611
- JSON.stringify({ kind: 'recurring-failure-class', findings }, null, 2),
612
- );
613
- lines.push('```');
614
- return lines.join('\n');
615
- }
616
-
617
- /**
618
- * Wrap the reporter so a throw (e.g. transient provider error, malformed
619
- * ledger) never crashes the stateless tick. Best-effort — the next tick
620
- * will retry.
621
- *
622
- * @param {() => Promise<void>} reporter
623
- */
624
- async function safeReportRecurringFailures(reporter) {
625
- try {
626
- await reporter();
627
- } catch {
628
- // best-effort
629
- }
630
- }
631
-
632
- /**
633
- * Default `inFlightReader` — parses `temp/epic-<id>/lifecycle.ndjson`
634
- * and returns the Story IDs that have a `story.dispatch.start`
635
- * `emitted` record without a matching `story.dispatch.end` `emitted`
636
- * record. The check is order-insensitive (the wave-runner records the
637
- * pair on the same Bus, so the start always lands first, but we don't
638
- * depend on that here).
639
- *
640
- * Returns `[]` when the ledger file does not yet exist or is empty —
641
- * the tick is stateless and must not throw when nothing has been
642
- * dispatched on this Epic yet.
643
- *
644
- * @param {number} epicId
645
- * @param {object|undefined} config Resolved config (forwarded to
646
- * `epicLedgerPath` so `project.paths.tempRoot` overrides apply).
647
- * @returns {Promise<number[]>}
648
- */
649
- async function defaultInFlightReader(epicId, config) {
650
- const ledgerPath = epicLedgerPath(epicId, config);
651
- if (!existsSync(ledgerPath)) return [];
652
- let raw;
653
- try {
654
- raw = readFileSync(ledgerPath, 'utf8');
655
- } catch {
656
- return [];
657
- }
658
- if (!raw) return [];
659
- const started = new Set();
660
- const ended = new Set();
661
- for (const line of raw.split(/\r?\n/)) {
662
- if (!line) continue;
663
- let record;
664
- try {
665
- record = JSON.parse(line);
666
- } catch {
667
- continue;
668
- }
669
- if (!record || record.kind !== 'emitted') continue;
670
- const storyId = record.payload?.storyId;
671
- if (!Number.isInteger(storyId) || storyId <= 0) continue;
672
- if (record.event === 'story.dispatch.start') started.add(storyId);
673
- else if (record.event === 'story.dispatch.end') ended.add(storyId);
674
- }
675
- const inFlight = [];
676
- for (const id of started) {
677
- if (!ended.has(id)) inFlight.push(id);
678
- }
679
- return inFlight.sort((a, b) => a - b);
680
- }
681
-
682
- function tickResult({
683
- nextAction,
684
- blockedStories = [],
685
- gateFailures = [],
686
- readyCount = 0,
687
- inFlight = [],
688
- }) {
689
- return { nextAction, blockedStories, gateFailures, readyCount, inFlight };
690
- }
691
-
692
- function resolveEpicId(epic) {
693
- const id = typeof epic === 'number' ? epic : epic?.id;
694
- if (Number.isInteger(id) && id > 0) return id;
695
- throw new WaveRunnerError(
696
- 'invalid-input',
697
- `epic must be a positive integer or { id: positiveInt }; got ${
698
- epic === null ? 'null' : typeof epic
699
- }`,
700
- );
701
- }
702
-
703
- function positiveIntOrZero(v) {
704
- return Number.isInteger(v) && v >= 0 ? v : 0;
705
- }
706
-
707
- /**
708
- * Derive gate-failure rows from the checkpoint's per-Story `stories` status
709
- * map: every Story recorded as `failed` surfaces as a gate failure so the
710
- * operator workflow can act on it. The shrunk checkpoint no longer carries a
711
- * per-wave history with explicit gate names, so the gate is reported as
712
- * `unspecified` and the recorded `title` (when present) is the detail.
713
- *
714
- * @param {object} state Parsed checkpoint.
715
- * @returns {Array<{ storyId: number, gate: string, detail?: string }>}
716
- */
717
- function readGateFailures(state) {
718
- const stories = state?.stories;
719
- if (!stories || typeof stories !== 'object') return [];
720
- const out = [];
721
- for (const [key, rec] of Object.entries(stories)) {
722
- const id = Number(key);
723
- if (!Number.isInteger(id) || id <= 0) continue;
724
- if (rec?.status !== 'failed') continue;
725
- const row = { storyId: id, gate: 'unspecified' };
726
- if (typeof rec.title === 'string' && rec.title) row.detail = rec.title;
727
- out.push(row);
728
- }
729
- return out.sort((a, b) => a.storyId - b.storyId);
730
- }
731
-
732
- /**
733
- * Default emitter — appends to per-Epic `signals.ndjson`. Best-effort;
734
- * never throws. Tests override via `collaborators.signalEmit`.
735
- *
736
- * Story #3909 / #4155 — the planner emits only the two wave-window
737
- * forensics events with a live consumer: `wave-start` (fired on the run's
738
- * first dispatch) and `wave-complete` (fired when the run finishes), which
739
- * the perf-aggregator (`waveParallelism` report) brackets into the run's
740
- * wall-clock. The write-only per-call telemetry and `epic-complete` emits
741
- * were dropped — they duplicated the `epic-run-state` checkpoint and the
742
- * `epic-run-progress` rollup and nothing consumed them.
743
- */
744
- function defaultSignalEmit(epicId, ctx) {
745
- return async (signal) => {
746
- await appendEpicSignal({
747
- epicId,
748
- // Canonical envelope (Epic #4406 / Story #4413): the single
749
- // `epicId` key — the legacy `epic` alias is gone.
750
- signal: { ts: new Date().toISOString(), epicId, ...signal },
751
- config: ctx?.config,
752
- });
753
- };
754
- }