mandrel 1.94.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (393) hide show
  1. package/.agents/README.md +59 -73
  2. package/.agents/agents/story-worker.md +12 -13
  3. package/.agents/docs/SDLC.md +489 -1285
  4. package/.agents/docs/agentrc-reference.json +177 -67
  5. package/.agents/docs/configuration.md +104 -138
  6. package/.agents/docs/execution-reference.md +17 -20
  7. package/.agents/docs/quality-gates.md +13 -19
  8. package/.agents/docs/workflows.md +3 -3
  9. package/.agents/instructions.md +70 -81
  10. package/.agents/rules/ci-remediation.md +8 -12
  11. package/.agents/rules/git-conventions-reference.md +6 -7
  12. package/.agents/rules/git-conventions.md +16 -22
  13. package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
  14. package/.agents/schemas/agentrc.schema.json +54 -214
  15. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  16. package/.agents/schemas/lifecycle/retro.end.schema.json +1 -1
  17. package/.agents/schemas/risk-verdict.schema.json +0 -13
  18. package/.agents/scripts/analyze-execution.js +1 -1
  19. package/.agents/scripts/audit-to-stories.js +7 -7
  20. package/.agents/scripts/boot-sweep.js +1 -1
  21. package/.agents/scripts/check-lifecycle-lint.js +6 -9
  22. package/.agents/scripts/check-prepush-recovery.js +1 -1
  23. package/.agents/scripts/cleanup-repo-test-temp.js +6 -1
  24. package/.agents/scripts/lib/Logger.js +6 -10
  25. package/.agents/scripts/lib/audit-suite/runner.js +2 -2
  26. package/.agents/scripts/lib/audit-suite/selector.js +5 -5
  27. package/.agents/scripts/lib/audit-to-stories/{seed-epic-from-findings.js → seed-from-findings.js} +9 -9
  28. package/.agents/scripts/lib/baselines/kernel.js +206 -18
  29. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -4
  30. package/.agents/scripts/lib/baselines/reader.js +1 -6
  31. package/.agents/scripts/lib/bdd-runner-detect.js +5 -9
  32. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +32 -33
  33. package/.agents/scripts/lib/checks/core-bare-clean.js +2 -2
  34. package/.agents/scripts/lib/checks/index.js +2 -1
  35. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +23 -21
  36. package/.agents/scripts/lib/cli/standard-args.js +13 -22
  37. package/.agents/scripts/lib/cli-args.js +16 -7
  38. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  39. package/.agents/scripts/lib/config/ci.js +6 -31
  40. package/.agents/scripts/lib/config/delivery-routing.js +45 -29
  41. package/.agents/scripts/lib/config/explain.js +55 -36
  42. package/.agents/scripts/lib/config/limits.js +17 -58
  43. package/.agents/scripts/lib/config/paths.js +0 -2
  44. package/.agents/scripts/lib/config/quality.js +1 -1
  45. package/.agents/scripts/lib/config/runners.js +17 -50
  46. package/.agents/scripts/lib/config/temp-paths.js +19 -14
  47. package/.agents/scripts/lib/config/worktree-isolation.js +0 -5
  48. package/.agents/scripts/lib/config-resolver.js +2 -7
  49. package/.agents/scripts/lib/config-settings-schema-delivery.js +24 -148
  50. package/.agents/scripts/lib/config-settings-schema-quality.js +8 -14
  51. package/.agents/scripts/lib/config-settings-schema.js +52 -38
  52. package/.agents/scripts/lib/dependency-parser.js +3 -2
  53. package/.agents/scripts/lib/doc-tiers.js +2 -2
  54. package/.agents/scripts/lib/duplicate-search.js +211 -41
  55. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
  56. package/.agents/scripts/lib/findings/promote-finding.js +5 -5
  57. package/.agents/scripts/lib/framework-version.js +2 -3
  58. package/.agents/scripts/lib/git-branch-cleanup.js +1 -10
  59. package/.agents/scripts/lib/git-branch-lifecycle.js +17 -22
  60. package/.agents/scripts/lib/git-utils.js +32 -6
  61. package/.agents/scripts/lib/github/framework-repo.js +6 -0
  62. package/.agents/scripts/lib/label-constants.js +10 -23
  63. package/.agents/scripts/lib/label-taxonomy.js +9 -43
  64. package/.agents/scripts/lib/observability/active-story-env.js +1 -1
  65. package/.agents/scripts/lib/observability/hook-heartbeat.js +20 -52
  66. package/.agents/scripts/lib/observability/source-classifier.js +3 -3
  67. package/.agents/scripts/lib/onboard/init-tail.js +1 -3
  68. package/.agents/scripts/lib/orchestration/audit-lens-routing.js +128 -0
  69. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +8 -5
  70. package/.agents/scripts/lib/orchestration/ceremony-routing.js +83 -20
  71. package/.agents/scripts/lib/orchestration/code-review.js +20 -268
  72. package/.agents/scripts/lib/orchestration/column-sync.js +1 -1
  73. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +1 -1
  74. package/.agents/scripts/lib/orchestration/context-envelope.js +2 -5
  75. package/.agents/scripts/lib/orchestration/docs-digest.js +8 -8
  76. package/.agents/scripts/lib/orchestration/file-assumptions.js +7 -13
  77. package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
  78. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +8 -8
  79. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +2 -2
  80. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +6 -3
  81. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +17 -43
  82. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +4 -4
  83. package/.agents/scripts/lib/orchestration/merge-block-class.js +1 -1
  84. package/.agents/scripts/lib/orchestration/phase-runner.js +3 -2
  85. package/.agents/scripts/lib/orchestration/plan-context.js +248 -266
  86. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +1 -1
  87. package/.agents/scripts/lib/orchestration/plan-navigation.js +92 -0
  88. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +61 -0
  89. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +97 -0
  90. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +223 -854
  91. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +361 -0
  92. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +35 -108
  93. package/.agents/scripts/lib/orchestration/plan-reachability.js +9 -14
  94. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +1 -1
  95. package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/authoring-context.js +13 -13
  96. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +27 -0
  97. package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/risk-verdict.js +3 -4
  98. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +2 -2
  99. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +8 -20
  100. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +3 -2
  101. package/.agents/scripts/lib/orchestration/pr-base-guard.js +18 -28
  102. package/.agents/scripts/lib/orchestration/preflight-cache.js +5 -5
  103. package/.agents/scripts/lib/orchestration/remote-verifier.js +1 -1
  104. package/.agents/scripts/lib/orchestration/resolve-plan-run.js +155 -0
  105. package/.agents/scripts/lib/orchestration/resolves-token.js +1 -1
  106. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +8 -8
  107. package/.agents/scripts/lib/orchestration/retro-proposals.js +140 -79
  108. package/.agents/scripts/lib/orchestration/review-depth.js +26 -12
  109. package/.agents/scripts/lib/orchestration/review-providers/codex.js +2 -2
  110. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +21 -56
  111. package/.agents/scripts/lib/orchestration/run-epilogue.js +426 -0
  112. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +1 -1
  113. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +95 -41
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +16 -13
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +40 -0
  116. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +11 -3
  117. package/.agents/scripts/lib/orchestration/spec-freshness.js +14 -205
  118. package/.agents/scripts/lib/orchestration/spec-section-validator.js +4 -5
  119. package/.agents/scripts/lib/orchestration/spec-spill.js +60 -0
  120. package/.agents/scripts/lib/orchestration/split-policy-validator.js +188 -0
  121. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +49 -0
  122. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +15 -12
  123. package/.agents/scripts/lib/orchestration/story-follow-ups.js +237 -0
  124. package/.agents/scripts/lib/orchestration/story-init-remote.js +47 -0
  125. package/.agents/scripts/lib/orchestration/story-plan-state.js +48 -0
  126. package/.agents/scripts/lib/orchestration/{epic-runner → story-progress}/story-run-progress-writer.js +3 -3
  127. package/.agents/scripts/lib/orchestration/structured-comment-parser.js +1 -1
  128. package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -18
  129. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +11 -61
  130. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +189 -373
  131. package/.agents/scripts/lib/orchestration/ticket-validator.js +2 -7
  132. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +0 -25
  133. package/.agents/scripts/lib/orchestration/ticketing/reads.js +29 -26
  134. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -5
  135. package/.agents/scripts/lib/planning-corpus.js +16 -11
  136. package/.agents/scripts/lib/preflight-runner.js +2 -2
  137. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -5
  138. package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +36 -0
  139. package/.agents/scripts/lib/single-story-sweep/protection-ctx.js +1 -1
  140. package/.agents/scripts/lib/story-adjacency.js +11 -14
  141. package/.agents/scripts/lib/story-body/story-body.js +124 -70
  142. package/.agents/scripts/lib/story-plan.js +2 -4
  143. package/.agents/scripts/lib/templates/decomposer-prompts.js +46 -45
  144. package/.agents/scripts/lib/templates/spec-author-prompts.js +47 -45
  145. package/.agents/scripts/lib/{epic-body-sections.js → ticket-body-sections.js} +26 -26
  146. package/.agents/scripts/lib/validation-evidence.js +1 -1
  147. package/.agents/scripts/lib/wave-runner/ready-set.js +6 -6
  148. package/.agents/scripts/lib/workspace-provisioner.js +1 -1
  149. package/.agents/scripts/lib/worktree/lifecycle/reap.js +5 -7
  150. package/.agents/scripts/lint-issue-body.js +71 -40
  151. package/.agents/scripts/mandrel-update-preflight.js +1 -1
  152. package/.agents/scripts/notify.js +4 -3
  153. package/.agents/scripts/plan-context.js +64 -74
  154. package/.agents/scripts/plan-persist.js +121 -280
  155. package/.agents/scripts/plan-run-epilogue.js +97 -0
  156. package/.agents/scripts/providers/github/issues.js +17 -33
  157. package/.agents/scripts/providers/github/mappers.js +0 -12
  158. package/.agents/scripts/providers/github/tickets.js +2 -5
  159. package/.agents/scripts/resolve-plan-run.js +117 -0
  160. package/.agents/scripts/signals-view.js +24 -19
  161. package/.agents/scripts/single-story-close.js +11 -14
  162. package/.agents/scripts/single-story-confirm-merge.js +39 -23
  163. package/.agents/scripts/single-story-init.js +29 -20
  164. package/.agents/scripts/stories-wave-tick.js +6 -6
  165. package/.agents/scripts/story-plan.js +26 -47
  166. package/.agents/scripts/update-ticket-state.js +6 -15
  167. package/.agents/skills/core/analyze-execution/SKILL.md +21 -18
  168. package/.agents/skills/core/idea-refinement/SKILL.md +6 -6
  169. package/.agents/skills/core/scope-triage/SKILL.md +28 -172
  170. package/.agents/skills/skills.index.json +3 -43
  171. package/.agents/starter-agentrc.json +0 -5
  172. package/.agents/templates/agent-protocol.md +9 -10
  173. package/.agents/workflows/audit-architecture.md +3 -3
  174. package/.agents/workflows/audit-clean-code.md +3 -3
  175. package/.agents/workflows/audit-dependencies.md +3 -3
  176. package/.agents/workflows/audit-devops.md +3 -3
  177. package/.agents/workflows/audit-documentation.md +5 -5
  178. package/.agents/workflows/audit-lighthouse.md +3 -3
  179. package/.agents/workflows/audit-navigability.md +3 -2
  180. package/.agents/workflows/audit-performance.md +3 -3
  181. package/.agents/workflows/audit-privacy.md +3 -3
  182. package/.agents/workflows/audit-quality.md +3 -3
  183. package/.agents/workflows/audit-security.md +3 -3
  184. package/.agents/workflows/audit-seo.md +3 -3
  185. package/.agents/workflows/audit-sre.md +3 -3
  186. package/.agents/workflows/audit-to-stories.md +20 -20
  187. package/.agents/workflows/audit-ux-ui.md +3 -3
  188. package/.agents/workflows/deliver.md +122 -174
  189. package/.agents/workflows/git-cleanup.md +3 -4
  190. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  191. package/.agents/workflows/helpers/acceptance-self-eval.md +16 -29
  192. package/.agents/workflows/helpers/code-review.md +70 -193
  193. package/.agents/workflows/helpers/{single-story-deliver-reference.md → deliver-story-reference.md} +12 -14
  194. package/.agents/workflows/helpers/{single-story-deliver.md → deliver-story.md} +113 -139
  195. package/.agents/workflows/helpers/diagnose.md +10 -10
  196. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  197. package/.agents/workflows/helpers/signals.md +16 -16
  198. package/.agents/workflows/helpers/worktree-lifecycle.md +48 -64
  199. package/.agents/workflows/mandrel-update.md +2 -1
  200. package/.agents/workflows/plan.md +112 -145
  201. package/.agents/workflows/qa-assist.md +24 -30
  202. package/.agents/workflows/qa-explore.md +29 -38
  203. package/.agents/workflows/qa-run.md +2 -2
  204. package/README.md +9 -8
  205. package/docs/CHANGELOG.md +30 -0
  206. package/lib/migrations/index.js +6 -5
  207. package/package.json +2 -2
  208. package/.agents/agents/retro.md +0 -42
  209. package/.agents/personas/architect.md +0 -113
  210. package/.agents/personas/devops-engineer.md +0 -38
  211. package/.agents/personas/engineer.md +0 -33
  212. package/.agents/personas/project-manager.md +0 -114
  213. package/.agents/personas/qa-engineer.md +0 -95
  214. package/.agents/personas/security-engineer.md +0 -111
  215. package/.agents/personas/technical-writer.md +0 -101
  216. package/.agents/schemas/dispatch-manifest.json +0 -232
  217. package/.agents/schemas/epic-spec.schema.json +0 -153
  218. package/.agents/schemas/lifecycle/slice.end.schema.json +0 -21
  219. package/.agents/schemas/lifecycle/slice.heartbeat.schema.json +0 -20
  220. package/.agents/schemas/lifecycle/slice.start.schema.json +0 -17
  221. package/.agents/scripts/acceptance-spec-reconciler.js +0 -642
  222. package/.agents/scripts/bookkeeping-reconcile.js +0 -117
  223. package/.agents/scripts/dispatcher.js +0 -295
  224. package/.agents/scripts/epic-audit-prepare.js +0 -497
  225. package/.agents/scripts/epic-audit-recheck.js +0 -274
  226. package/.agents/scripts/epic-deliver-note-intervention.js +0 -192
  227. package/.agents/scripts/epic-deliver-preflight.js +0 -462
  228. package/.agents/scripts/epic-deliver-prepare.js +0 -852
  229. package/.agents/scripts/epic-execute-record-wave.js +0 -449
  230. package/.agents/scripts/epic-plan-clarity.js +0 -211
  231. package/.agents/scripts/epic-plan-healthcheck.js +0 -581
  232. package/.agents/scripts/epic-reconcile.js +0 -625
  233. package/.agents/scripts/lib/baseline-snapshot.js +0 -979
  234. package/.agents/scripts/lib/checks/epic-merge-lock-stale.js +0 -54
  235. package/.agents/scripts/lib/checks/stale-origin-epic.js +0 -49
  236. package/.agents/scripts/lib/config/lifecycle.js +0 -40
  237. package/.agents/scripts/lib/config/preflight.js +0 -58
  238. package/.agents/scripts/lib/config/retro.js +0 -77
  239. package/.agents/scripts/lib/epic-merge-lock.js +0 -322
  240. package/.agents/scripts/lib/epic-plan-clarity.js +0 -181
  241. package/.agents/scripts/lib/epic-plan-ideation.js +0 -261
  242. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +0 -539
  243. package/.agents/scripts/lib/orchestration/deliver-route.js +0 -173
  244. package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -134
  245. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +0 -183
  246. package/.agents/scripts/lib/orchestration/epic-cleanup.js +0 -801
  247. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +0 -310
  248. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +0 -163
  249. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +0 -140
  250. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +0 -64
  251. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +0 -72
  252. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +0 -156
  253. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +0 -345
  254. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +0 -41
  255. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/reconcile-spawn.js +0 -86
  256. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +0 -391
  257. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +0 -94
  258. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +0 -236
  259. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +0 -307
  260. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +0 -117
  261. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +0 -117
  262. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +0 -621
  263. package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +0 -186
  264. package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +0 -50
  265. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +0 -129
  266. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +0 -103
  267. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +0 -267
  268. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -210
  269. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +0 -238
  270. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/_bullet-format.js +0 -32
  271. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +0 -203
  272. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +0 -227
  273. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +0 -117
  274. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/stalled-worktree.js +0 -37
  275. package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +0 -127
  276. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +0 -276
  277. package/.agents/scripts/lib/orchestration/epic-runner/wave-scheduler.js +0 -66
  278. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +0 -789
  279. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +0 -676
  280. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-discriminator.js +0 -389
  281. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-format.js +0 -230
  282. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +0 -361
  283. package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +0 -306
  284. package/.agents/scripts/lib/orchestration/finalize/post-handoff-comment.js +0 -489
  285. package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +0 -88
  286. package/.agents/scripts/lib/orchestration/lifecycle/emit-slice-lifecycle.js +0 -270
  287. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +0 -147
  288. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +0 -465
  289. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +0 -501
  290. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +0 -984
  291. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +0 -264
  292. package/.agents/scripts/lib/orchestration/lifecycle/listeners/checkpoint-pointer-writer.js +0 -284
  293. package/.agents/scripts/lib/orchestration/lifecycle/listeners/cleaner.js +0 -355
  294. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +0 -673
  295. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +0 -378
  296. package/.agents/scripts/lib/orchestration/lifecycle/listeners/intervention-recorder.js +0 -140
  297. package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +0 -144
  298. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +0 -174
  299. package/.agents/scripts/lib/orchestration/manifest-builder.js +0 -222
  300. package/.agents/scripts/lib/orchestration/plan-persist/amend.js +0 -359
  301. package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +0 -127
  302. package/.agents/scripts/lib/orchestration/post-merge-pipeline.js +0 -205
  303. package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +0 -152
  304. package/.agents/scripts/lib/orchestration/retro/phases/checks.js +0 -94
  305. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +0 -571
  306. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +0 -450
  307. package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +0 -191
  308. package/.agents/scripts/lib/orchestration/retro-heuristics.js +0 -57
  309. package/.agents/scripts/lib/orchestration/retro-runner.js +0 -197
  310. package/.agents/scripts/lib/orchestration/spec-renderer.js +0 -447
  311. package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +0 -747
  312. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/gate-failure.js +0 -211
  313. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +0 -158
  314. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +0 -446
  315. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +0 -297
  316. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/scope-discovery.js +0 -48
  317. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution-wiring.js +0 -67
  318. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution.js +0 -161
  319. package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +0 -117
  320. package/.agents/scripts/lib/orchestration/story-close/cd-out-guard.js +0 -86
  321. package/.agents/scripts/lib/orchestration/story-close/cleanup-reconciler.js +0 -147
  322. package/.agents/scripts/lib/orchestration/story-close/close-inputs.js +0 -142
  323. package/.agents/scripts/lib/orchestration/story-close/comment-bodies.js +0 -62
  324. package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +0 -658
  325. package/.agents/scripts/lib/orchestration/story-close/merge-subject.js +0 -198
  326. package/.agents/scripts/lib/orchestration/story-close/phases/branch-restore.js +0 -105
  327. package/.agents/scripts/lib/orchestration/story-close/phases/close.js +0 -222
  328. package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +0 -292
  329. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +0 -270
  330. package/.agents/scripts/lib/orchestration/story-close/phases/preflight.js +0 -110
  331. package/.agents/scripts/lib/orchestration/story-close/phases/refresh.js +0 -86
  332. package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked-emitter.js +0 -112
  333. package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked.js +0 -157
  334. package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +0 -421
  335. package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +0 -302
  336. package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +0 -163
  337. package/.agents/scripts/lib/orchestration/story-close-recovery.js +0 -690
  338. package/.agents/scripts/lib/orchestration/wave-marker.js +0 -28
  339. package/.agents/scripts/lib/orchestration/wave-record-io.js +0 -218
  340. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +0 -145
  341. package/.agents/scripts/lib/orchestration/wave-record-projection.js +0 -212
  342. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +0 -111
  343. package/.agents/scripts/lib/presentation/manifest-builder.js +0 -239
  344. package/.agents/scripts/lib/presentation/manifest-formatter.js +0 -242
  345. package/.agents/scripts/lib/presentation/manifest-helpers.js +0 -213
  346. package/.agents/scripts/lib/presentation/manifest-persistence.js +0 -261
  347. package/.agents/scripts/lib/presentation/manifest-procedures.js +0 -55
  348. package/.agents/scripts/lib/presentation/manifest-render-waves.js +0 -306
  349. package/.agents/scripts/lib/presentation/manifest-renderer.js +0 -188
  350. package/.agents/scripts/lib/presentation/manifest-story-views.js +0 -110
  351. package/.agents/scripts/lib/push-epic-retry.js +0 -209
  352. package/.agents/scripts/lib/spec/index.js +0 -36
  353. package/.agents/scripts/lib/spec/loader.js +0 -425
  354. package/.agents/scripts/lib/spec/state.js +0 -208
  355. package/.agents/scripts/lib/story-init/blocker-validator.js +0 -68
  356. package/.agents/scripts/lib/story-init/branch-initializer.js +0 -408
  357. package/.agents/scripts/lib/story-init/context-resolver.js +0 -92
  358. package/.agents/scripts/lib/story-init/donor-precheck.js +0 -207
  359. package/.agents/scripts/lib/story-init/state-transitioner.js +0 -80
  360. package/.agents/scripts/lib/story-init/task-graph-builder.js +0 -124
  361. package/.agents/scripts/lib/story-init/transition-summary.js +0 -34
  362. package/.agents/scripts/lib/test-reserved-epic-temp-ids.js +0 -35
  363. package/.agents/scripts/lib/wave-runner/tick.js +0 -754
  364. package/.agents/scripts/lib/wave-runner/wave-runner-error.js +0 -20
  365. package/.agents/scripts/lifecycle-emit-story-dispatch.js +0 -194
  366. package/.agents/scripts/lifecycle-emit.js +0 -510
  367. package/.agents/scripts/retro-run.js +0 -218
  368. package/.agents/scripts/slice-phase.js +0 -361
  369. package/.agents/scripts/standalone-feedback-rollup.js +0 -188
  370. package/.agents/scripts/story-close.js +0 -294
  371. package/.agents/scripts/story-init.js +0 -599
  372. package/.agents/scripts/story-phase.js +0 -369
  373. package/.agents/scripts/wave-tick.js +0 -464
  374. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +0 -172
  375. package/.agents/skills/core/epic-plan-consolidate/examples.md +0 -51
  376. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +0 -441
  377. package/.agents/skills/core/epic-plan-decompose-author/examples.md +0 -47
  378. package/.agents/skills/core/epic-plan-premortem/SKILL.md +0 -146
  379. package/.agents/skills/core/epic-plan-premortem/examples.md +0 -53
  380. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +0 -383
  381. package/.agents/skills/core/epic-plan-spec-author/examples.md +0 -91
  382. package/.agents/workflows/helpers/deliver-epic-reference.md +0 -547
  383. package/.agents/workflows/helpers/deliver-epic-single.md +0 -331
  384. package/.agents/workflows/helpers/deliver-epic.md +0 -998
  385. package/.agents/workflows/helpers/deliver-stories.md +0 -450
  386. package/.agents/workflows/helpers/epic-audit.md +0 -189
  387. package/.agents/workflows/helpers/epic-deliver-story.md +0 -436
  388. package/.agents/workflows/helpers/epic-testing.md +0 -125
  389. package/.agents/workflows/helpers/plan-epic-reference.md +0 -160
  390. package/.agents/workflows/helpers/plan-epic.md +0 -353
  391. package/.agents/workflows/helpers/plan-story.md +0 -251
  392. package/.agents/workflows/helpers/scope-triage-gate.md +0 -108
  393. /package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/spec-authoring-grounding.js +0 -0
@@ -1,54 +0,0 @@
1
- /**
2
- * epic-merge-lock-stale — refuse-and-print warning check.
3
- *
4
- * Detects orphan `epic-<id>.merge.lock` files left behind by a crashed
5
- * or hung `withEpicMergeLock()` holder. The live serialization path is
6
- * owned by `lib/epic-merge-lock.js` itself; this check intentionally
7
- * does NOT serialize against it. Instead, at preflight time it probes
8
- * the lock file for each known epic and surfaces a warning when:
9
- *
10
- * - the lock file exists, AND
11
- * - the recorded holder PID is not alive (per `process.kill(pid, 0)`).
12
- *
13
- * Severity is `warning` rather than `blocker` because the live lock
14
- * implementation already steals stale locks on its own (per
15
- * `tryStealStale()` in epic-merge-lock.js). Surfacing the residue
16
- * earlier just helps operators diagnose hung CI runs before they
17
- * snowball into wave-aggregation timeouts. The fix is a literal
18
- * `rm <lockPath>` — refuse-and-print so the operator runs it
19
- * deliberately.
20
- *
21
- * Reads `state.fs.epicMergeLocks`, a map of `epicId → { exists, pid,
22
- * holderAlive, path, ... }` populated by state.js.
23
- */
24
- export default {
25
- id: 'epic-merge-lock-stale',
26
- severity: 'warning',
27
- scope: ['story-close', 'retro'],
28
- autoCorrect: 'refuse-and-print',
29
- detect(state) {
30
- const locks = state?.fs?.epicMergeLocks ?? {};
31
- for (const [epicId, info] of Object.entries(locks)) {
32
- if (!info?.exists) continue;
33
- if (info.holderAlive) continue;
34
- // Lock exists and the holder PID is not alive — orphan.
35
- return {
36
- id: 'epic-merge-lock-stale',
37
- severity: 'warning',
38
- scope: state?.scope ?? 'story-close',
39
- summary: `Orphan epic merge lock for epic/${epicId} (holder PID ${info.pid ?? 'unknown'} not alive)`,
40
- detail: [
41
- `lock path: ${info.path}`,
42
- `holder pid: ${info.pid ?? 'unknown'}`,
43
- info.acquiredAt
44
- ? `acquired at: ${new Date(info.acquiredAt).toISOString()}`
45
- : 'acquired at: unknown',
46
- 'The live lock will steal this on next contention, but the residue can be cleared manually.',
47
- ].join('\n'),
48
- fixCommand: `rm "${info.path}"`,
49
- autoCorrectable: false,
50
- };
51
- }
52
- return null;
53
- },
54
- };
@@ -1,49 +0,0 @@
1
- /**
2
- * stale-origin-epic — refuse-and-print blocker check.
3
- *
4
- * Detects the failure mode where a manual merge into `epic/<id>` landed
5
- * locally but the operator forgot to `git push` the epic branch before
6
- * re-running `story-close.js`. The close script's rebase then fights a
7
- * stale `origin/epic/<id>` and re-introduces a different conflict on
8
- * every retry.
9
- *
10
- * The check surfaces this at story-close preflight (and at retro time
11
- * read-only) so the operator is told to fetch + push the epic branch
12
- * before the close re-runs. It is `refuse-and-print` because the fix
13
- * is `git fetch origin` followed by `git push origin epic/<id>`; both
14
- * touch remote state and therefore violate the autoCorrect rules.
15
- *
16
- * Detection reads `state.git.epicBranchSync` (assembled in state.js).
17
- * Each entry reports `{ local, remote, ahead }` for an `epic/<id>` ref;
18
- * we surface the first ref whose local SHA differs from its remote SHA.
19
- * If the remote ref is missing entirely we return null (this is a
20
- * pre-push epic, not a stale one — story-close.js handles that path).
21
- */
22
- export default {
23
- id: 'stale-origin-epic',
24
- severity: 'blocker',
25
- scope: ['story-close', 'retro'],
26
- autoCorrect: 'refuse-and-print',
27
- detect(state) {
28
- const sync = state?.git?.epicBranchSync ?? {};
29
- const entries = Object.entries(sync);
30
- for (const [branch, info] of entries) {
31
- if (!info?.local || !info.remote) continue;
32
- if (!info.ahead) continue;
33
- return {
34
- id: 'stale-origin-epic',
35
- severity: 'blocker',
36
- scope: state?.scope ?? 'story-close',
37
- summary: `Local ${branch} differs from origin/${branch}; close will rebase against a stale base.`,
38
- detail: [
39
- `local ${branch} = ${info.local}`,
40
- `remote origin/${branch} = ${info.remote}`,
41
- 'Push the epic branch (or fetch origin) before re-running story-close.',
42
- ].join('\n'),
43
- fixCommand: `git fetch origin && git push origin ${branch}`,
44
- autoCorrectable: false,
45
- };
46
- }
47
- return null;
48
- },
49
- };
@@ -1,40 +0,0 @@
1
- /**
2
- * `delivery.lifecycle` accessor + framework defaults.
3
- *
4
- * `TimeoutWatchdog` returns null (no budget) for any timeout key absent from
5
- * the operator config, meaning phases can hang indefinitely on projects that
6
- * omit the block. These defaults ensure every Epic run has a watchdog out of
7
- * the box; projects override individual keys by supplying their own values,
8
- * which are merged over the defaults.
9
- */
10
-
11
- export const LIFECYCLE_DEFAULTS = Object.freeze({
12
- timeouts: Object.freeze({
13
- 'acceptance.reconcile': 600,
14
- 'epic.finalize': 600,
15
- 'epic.watch': 1800,
16
- }),
17
- heartbeatWarnSeconds: 60,
18
- });
19
-
20
- /**
21
- * Read the merged `delivery.lifecycle` block, applying framework defaults
22
- * for any field the operator omitted. Accepts the full resolved config or
23
- * a bare `{ delivery: { lifecycle: ... } }` / `{ lifecycle: ... }` shape.
24
- *
25
- * User-supplied timeout keys win over defaults; unknown keys are preserved
26
- * so projects can define custom phase budgets.
27
- *
28
- * @param {object | null | undefined} config
29
- * @returns {{ timeouts: Record<string, number>, heartbeatWarnSeconds: number }}
30
- */
31
- export function getLifecycle(config) {
32
- const lc = config?.delivery?.lifecycle ?? config?.lifecycle ?? config ?? {};
33
- return {
34
- timeouts: { ...LIFECYCLE_DEFAULTS.timeouts, ...(lc.timeouts ?? {}) },
35
- heartbeatWarnSeconds:
36
- Number.isInteger(lc.heartbeatWarnSeconds) && lc.heartbeatWarnSeconds >= 1
37
- ? lc.heartbeatWarnSeconds
38
- : LIFECYCLE_DEFAULTS.heartbeatWarnSeconds,
39
- };
40
- }
@@ -1,58 +0,0 @@
1
- /**
2
- * `delivery.preflight` accessor + framework defaults — Story #2899
3
- * (Epic #2880, F13).
4
- *
5
- * Thresholds consumed by `.agents/scripts/epic-deliver-preflight.js`.
6
- * The defaults are intentionally generous so that a brand-new Epic on a
7
- * zero-config project does not trip a breach. Operators tighten thresholds
8
- * per project in `.agentrc.json`. A `null` floor means "no cap" — used by
9
- * the preflight runner to skip the corresponding breach check.
10
- */
11
-
12
- export const PREFLIGHT_DEFAULTS = Object.freeze({
13
- maxStories: null,
14
- maxWaves: null,
15
- maxInstallCostSeconds: null,
16
- maxGithubApiRequests: null,
17
- maxClaudeQuotaTokens: null,
18
- });
19
-
20
- /**
21
- * Read the merged `delivery.preflight` block, applying framework defaults
22
- * for any field the operator omitted. Accepts the full resolved config,
23
- * the bare delivery bag, or the bare preflight bag.
24
- *
25
- * @param {object | null | undefined} config
26
- * @returns {{
27
- * maxStories: number | null,
28
- * maxWaves: number | null,
29
- * maxInstallCostSeconds: number | null,
30
- * maxGithubApiRequests: number | null,
31
- * maxClaudeQuotaTokens: number | null,
32
- * }}
33
- */
34
- export function getPreflight(config) {
35
- const pf = config?.delivery?.preflight ?? config?.preflight ?? config ?? {};
36
- return {
37
- maxStories: normaliseFloor(pf.maxStories, PREFLIGHT_DEFAULTS.maxStories),
38
- maxWaves: normaliseFloor(pf.maxWaves, PREFLIGHT_DEFAULTS.maxWaves),
39
- maxInstallCostSeconds: normaliseFloor(
40
- pf.maxInstallCostSeconds,
41
- PREFLIGHT_DEFAULTS.maxInstallCostSeconds,
42
- ),
43
- maxGithubApiRequests: normaliseFloor(
44
- pf.maxGithubApiRequests,
45
- PREFLIGHT_DEFAULTS.maxGithubApiRequests,
46
- ),
47
- maxClaudeQuotaTokens: normaliseFloor(
48
- pf.maxClaudeQuotaTokens,
49
- PREFLIGHT_DEFAULTS.maxClaudeQuotaTokens,
50
- ),
51
- };
52
- }
53
-
54
- function normaliseFloor(value, fallback) {
55
- if (value === null) return null;
56
- if (Number.isInteger(value) && value >= 1) return value;
57
- return fallback;
58
- }
@@ -1,77 +0,0 @@
1
- /**
2
- * Retro accessor (Story #3042 / Task #3043 — Epic #3019).
3
- *
4
- * Resolves `.agentrc.json → delivery.retro` into the canonical shape the
5
- * retro-runner consumes. Currently exposes only `perfThresholds`, the
6
- * operator-tunable gates for the perf-signals classifier in
7
- * `lib/orchestration/retro-perf-heuristics.js`.
8
- *
9
- * Defaults mirror `DEFAULT_RETRO_PERF_THRESHOLDS` in the heuristics module
10
- * (single source of behavioural truth; this resolver is a thin merge layer).
11
- */
12
-
13
- /**
14
- * Default perf-threshold trio applied when `.agentrc.json` omits
15
- * `delivery.retro.perfThresholds` (or any of its sub-keys). Frozen so
16
- * downstream callers cannot accidentally mutate the resolver's defaults
17
- * across processes.
18
- *
19
- * Keep in lockstep with `DEFAULT_RETRO_PERF_THRESHOLDS` in
20
- * `lib/orchestration/retro-perf-heuristics.js` and the schema mirror in
21
- * `agentrc.schema.json → $defs.retro.properties.perfThresholds`.
22
- */
23
- export const DEFAULT_RETRO = Object.freeze({
24
- perfThresholds: Object.freeze({
25
- utilisation: 0.6,
26
- bootstrapShare: 0.4,
27
- capBindingRunLength: 2,
28
- }),
29
- });
30
-
31
- /**
32
- * Read the merged retro block. Returns the canonical shape:
33
- *
34
- * {
35
- * perfThresholds: {
36
- * utilisation: number,
37
- * bootstrapShare: number,
38
- * capBindingRunLength: number,
39
- * },
40
- * }
41
- *
42
- * Each sub-key falls back to its documented default when the resolved
43
- * `.agentrc.json` omits it. Out-of-range values fall back to defaults too —
44
- * the AJV schema rejects them before they reach this accessor, but the
45
- * resolver stays defensive so unit-test fixtures and degraded configs
46
- * never produce nonsensical thresholds.
47
- *
48
- * @param {object | null | undefined} config
49
- * @returns {{ perfThresholds: { utilisation: number, bootstrapShare: number, capBindingRunLength: number } }}
50
- */
51
- export function getRetro(config) {
52
- const user = config?.delivery?.retro?.perfThresholds ?? {};
53
- const defaults = DEFAULT_RETRO.perfThresholds;
54
- return {
55
- perfThresholds: {
56
- utilisation: resolveUnit(user.utilisation, defaults.utilisation),
57
- bootstrapShare: resolveUnit(user.bootstrapShare, defaults.bootstrapShare),
58
- capBindingRunLength: resolvePositiveInt(
59
- user.capBindingRunLength,
60
- defaults.capBindingRunLength,
61
- ),
62
- },
63
- };
64
- }
65
-
66
- function resolveUnit(value, fallback) {
67
- if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;
68
- if (value < 0 || value > 1) return fallback;
69
- return value;
70
- }
71
-
72
- function resolvePositiveInt(value, fallback) {
73
- if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
74
- return fallback;
75
- }
76
- return value;
77
- }
@@ -1,322 +0,0 @@
1
- /**
2
- * lib/epic-merge-lock.js — Filesystem mutex for Epic-branch merges.
3
- *
4
- * Parallel-wave story closures can race on the Epic branch: two
5
- * `story-close.js` invocations both `git checkout <epic>`, both
6
- * `git pull --rebase`, and both attempt to merge — the second push
7
- * often ends up rejected or, worse, races past the first and produces
8
- * an incorrect history.
9
- *
10
- * This module provides a best-effort cooperative lock keyed per Epic.
11
- * The lock file lives at `<repoRoot>/.git/epic-<epicId>.merge.lock`
12
- * (inside `.git/` so it never lands in a commit). Acquisition uses
13
- * `fs.openSync(..., 'wx')` for atomicity; on contention we poll every
14
- * 250ms until `timeoutMs` elapses.
15
- *
16
- * Stale-lock stealing:
17
- * - If the PID recorded in the lock is not running (per
18
- * `process.kill(pid, 0)`), or
19
- * - if the lock file is older than `timeoutMs * 2`,
20
- * the lock is stolen (unlinked) and re-acquired.
21
- *
22
- * Test seams (mirroring `single-story-sweep/sweep-lock.js`):
23
- * - `nowFn` — `() => number` (ms epoch); replaces `Date.now`.
24
- * - `fsImpl` — Node `fs` shim; replaces the imported `fs`.
25
- * - `killFn` — `(pid, signal) => void`; replaces `process.kill`,
26
- * so a test can make `pidDead` deterministic without
27
- * fabricating a real dead PID.
28
- * - `sleepFn` — `(ms) => Promise<void>`; replaces the real
29
- * `setTimeout`-backed sleep, so the poll loop can spin
30
- * with no wall-clock waits.
31
- * All four default to the real implementations, so production callers
32
- * (`acquire(epicId, { repoRoot, timeoutMs })`) see no behavior change.
33
- */
34
-
35
- import { execFileSync } from 'node:child_process';
36
- import fs from 'node:fs';
37
- import path from 'node:path';
38
-
39
- const POLL_INTERVAL_MS = 250;
40
-
41
- /**
42
- * Resolve the *common* gitdir for a given working directory.
43
- *
44
- * In a linked worktree (`git worktree add ...`), `<repoRoot>/.git` is a
45
- * one-line gitlink **file**, not a directory. `path.join(repoRoot, '.git')`
46
- * therefore points at the gitlink file and any `mkdir`/`openSync` against
47
- * it fails with `EEXIST: file already exists`.
48
- *
49
- * Resolution order:
50
- * 1. If `<repoRoot>/.git` is already a directory, return it. Covers the
51
- * main-checkout case and the test fixtures, which create a bare
52
- * `.git/` under a temp root — no need to spawn git for those.
53
- * 2. Otherwise (gitlink file, or `.git` absent), shell out to
54
- * `git rev-parse --git-common-dir`. In a worktree this returns the
55
- * parent repo's `.git/`, so lock files placed there are shared
56
- * across every worktree racing on the same Epic — which is the
57
- * correct semantics for an epic-merge mutex.
58
- * 3. If neither succeeds, fall back to `<repoRoot>/.git`. Lock
59
- * acquisition will then surface the underlying error to the
60
- * operator with the literal path that failed.
61
- */
62
- export function resolveGitCommonDir(repoRoot, fsImpl = fs) {
63
- const local = path.join(repoRoot, '.git');
64
- try {
65
- if (fsImpl.statSync(local).isDirectory()) return local;
66
- } catch {
67
- // .git does not exist — fall through to git rev-parse.
68
- }
69
- try {
70
- const out = execFileSync('git', ['rev-parse', '--git-common-dir'], {
71
- cwd: repoRoot,
72
- encoding: 'utf8',
73
- stdio: ['ignore', 'pipe', 'ignore'],
74
- }).trim();
75
- if (out) return path.isAbsolute(out) ? out : path.resolve(repoRoot, out);
76
- } catch {
77
- // not a git repo, or git is unavailable — fall through.
78
- }
79
- return local;
80
- }
81
-
82
- function lockPathFor(epicId, repoRoot, fsImpl = fs) {
83
- return path.join(
84
- resolveGitCommonDir(repoRoot, fsImpl),
85
- `epic-${epicId}.merge.lock`,
86
- );
87
- }
88
-
89
- function isProcessRunning(pid, killFn = process.kill) {
90
- if (!Number.isFinite(pid) || pid <= 0) return false;
91
- try {
92
- // Signal 0 does not deliver a signal; it just checks existence.
93
- killFn(pid, 0);
94
- return true;
95
- } catch (err) {
96
- // ESRCH = no such process. EPERM = exists but we can't signal — still alive.
97
- return err.code === 'EPERM';
98
- }
99
- }
100
-
101
- function readLockMeta(filePath, fsImpl = fs) {
102
- try {
103
- const raw = fsImpl.readFileSync(filePath, 'utf8');
104
- const parsed = JSON.parse(raw);
105
- return {
106
- pid: Number(parsed.pid),
107
- acquiredAt: Number(parsed.acquiredAt),
108
- };
109
- } catch {
110
- return null;
111
- }
112
- }
113
-
114
- function tryStealStale(filePath, timeoutMs, seams) {
115
- const { fsImpl, nowFn, killFn } = seams;
116
- let stats;
117
- try {
118
- stats = fsImpl.statSync(filePath);
119
- } catch {
120
- return false;
121
- }
122
-
123
- const meta = readLockMeta(filePath, fsImpl);
124
- // Corrupted lock file (null meta): we can't verify the writer's PID and
125
- // the age comparison is unsafe on Windows where NTFS mtime vs Date.now()
126
- // can disagree by hundreds of milliseconds, falsely flipping `ancient`
127
- // true at short timeouts. Treat the file as held; the caller times out.
128
- // A truly stuck corrupted lock has to be cleared manually — that's the
129
- // safer failure mode than wrongly stealing a lock another process owns.
130
- if (!meta) return false;
131
-
132
- const ageMs = nowFn() - stats.mtimeMs;
133
- const pidDead = !isProcessRunning(meta.pid, killFn);
134
- const ancient = ageMs > timeoutMs * 2;
135
-
136
- if (pidDead || ancient) {
137
- try {
138
- fsImpl.unlinkSync(filePath);
139
- return true;
140
- } catch {
141
- return false;
142
- }
143
- }
144
- return false;
145
- }
146
-
147
- function defaultSleep(ms) {
148
- return new Promise((resolve) => setTimeout(resolve, ms));
149
- }
150
-
151
- // Inner polling loop. Acquires the lock file with `wx` (atomic
152
- // create-or-error); on EEXIST it tries to steal a stale lock, then
153
- // either times out or sleeps and retries. Kept separate from
154
- // `acquireEpicMergeLock` so the public function's cyclomatic complexity
155
- // stays flat under CRAP.
156
- async function pollForLock(epicId, filePath, timeoutMs, seams) {
157
- const { fsImpl, nowFn, sleepFn } = seams;
158
- const started = nowFn();
159
- while (true) {
160
- try {
161
- const fd = fsImpl.openSync(filePath, 'wx');
162
- const acquiredAt = nowFn();
163
- fsImpl.writeSync(
164
- fd,
165
- JSON.stringify({ pid: process.pid, acquiredAt }, null, 2),
166
- );
167
- fsImpl.closeSync(fd);
168
- return { epicId, filePath, acquiredAt };
169
- } catch (err) {
170
- if (err.code !== 'EEXIST') throw err;
171
- if (tryStealStale(filePath, timeoutMs, seams)) continue;
172
- if (nowFn() - started >= timeoutMs) {
173
- const meta = readLockMeta(filePath, fsImpl);
174
- const detail = meta
175
- ? ` (held by pid ${meta.pid} since ${new Date(meta.acquiredAt).toISOString()})`
176
- : '';
177
- throw new Error(
178
- `acquireEpicMergeLock timed out after ${timeoutMs}ms for epic ${epicId}${detail}`,
179
- );
180
- }
181
- await sleepFn(POLL_INTERVAL_MS);
182
- }
183
- }
184
- }
185
-
186
- /**
187
- * Acquire an exclusive Epic merge lock.
188
- *
189
- * @param {number|string} epicId
190
- * @param {object} opts
191
- * @param {string} opts.repoRoot Repo working dir; the lock lands
192
- * in its common `.git/`.
193
- * @param {number} [opts.timeoutMs=60000] Poll-until deadline.
194
- * @param {() => number} [opts.nowFn] Clock seam (ms epoch).
195
- * @param {object} [opts.fsImpl] Node `fs` shim.
196
- * @param {(pid:number, signal:number)=>void} [opts.killFn]
197
- * `process.kill` shim for the
198
- * pid-liveness probe.
199
- * @param {(ms:number)=>Promise<void>} [opts.sleepFn]
200
- * Poll-interval sleep shim.
201
- * @returns {Promise<{ epicId: number|string, filePath: string, acquiredAt: number }>}
202
- * @throws {Error} on timeout.
203
- */
204
- export async function acquireEpicMergeLock(
205
- epicId,
206
- {
207
- repoRoot,
208
- timeoutMs = 60_000,
209
- nowFn = Date.now,
210
- fsImpl = fs,
211
- killFn = process.kill.bind(process),
212
- sleepFn = defaultSleep,
213
- } = {},
214
- ) {
215
- if (!repoRoot) throw new Error('acquireEpicMergeLock: repoRoot is required');
216
-
217
- const filePath = lockPathFor(epicId, repoRoot, fsImpl);
218
- // Ensure the .git directory exists (it will, in a real repo, but the
219
- // tests use a temp dir and need us to be forgiving).
220
- fsImpl.mkdirSync(path.dirname(filePath), { recursive: true });
221
-
222
- const seams = { fsImpl, nowFn, killFn, sleepFn };
223
- return pollForLock(epicId, filePath, timeoutMs, seams);
224
- }
225
-
226
- /**
227
- * Release a previously-acquired Epic merge lock.
228
- *
229
- * @param {{ filePath: string }} handle
230
- * @param {object} [fsImpl] Node `fs` shim for tests.
231
- */
232
- export function releaseEpicMergeLock(handle, fsImpl = fs) {
233
- if (!handle?.filePath) return;
234
- try {
235
- fsImpl.unlinkSync(handle.filePath);
236
- } catch (err) {
237
- if (err.code !== 'ENOENT') throw err;
238
- }
239
- }
240
-
241
- const LOCK_FILE_PREFIX = 'epic-';
242
- const LOCK_FILE_SUFFIX = '.merge.lock';
243
-
244
- // Extract the epic id from a lock filename, or `null` when `entry` isn't a
245
- // `epic-*.merge.lock` file. Split out of `findForeignActiveEpicLock` so
246
- // that function stays a flat filter+map instead of a nested-conditional
247
- // loop body (Story #4460).
248
- function parseLockFileEpicId(entry) {
249
- if (
250
- !entry.startsWith(LOCK_FILE_PREFIX) ||
251
- !entry.endsWith(LOCK_FILE_SUFFIX)
252
- ) {
253
- return null;
254
- }
255
- return entry.slice(
256
- LOCK_FILE_PREFIX.length,
257
- entry.length - LOCK_FILE_SUFFIX.length,
258
- );
259
- }
260
-
261
- // Read `<dir>/<entry>`'s lock meta and report it only when the recorded
262
- // pid is still alive — a foreign lock whose pid is dead (or whose meta is
263
- // unreadable/corrupt) is stale debris, not an active holder, mirroring the
264
- // pid-liveness half of `tryStealStale`'s heuristic.
265
- function readLiveLock(dir, entry, otherEpicId, fsImpl, killFn) {
266
- const filePath = path.join(dir, entry);
267
- const meta = readLockMeta(filePath, fsImpl);
268
- if (!meta || !isProcessRunning(meta.pid, killFn)) return null;
269
- return {
270
- epicId: otherEpicId,
271
- filePath,
272
- pid: meta.pid,
273
- acquiredAt: meta.acquiredAt,
274
- };
275
- }
276
-
277
- /**
278
- * Scan the shared common `.git/` dir for a *different* epic's live
279
- * merge lock (Story #4460 — cross-epic shared-checkout guard).
280
- *
281
- * `acquireEpicMergeLock` only ever contends against locks for the *same*
282
- * `epicId` (its own lock filename). This helper is the cross-epic
283
- * counterpart: it lists every `epic-*.merge.lock` file in the common
284
- * gitdir, skips the caller's own `epicId` namespace, and returns the
285
- * first foreign lock whose recorded `pid` is still alive.
286
- *
287
- * @param {number|string} epicId Caller's own epic id (excluded from the scan).
288
- * @param {{
289
- * repoRoot: string,
290
- * fsImpl?: object,
291
- * killFn?: (pid:number, signal:number)=>void,
292
- * }} opts
293
- * @returns {{ epicId: string, filePath: string, pid: number, acquiredAt: number }|null}
294
- */
295
- export function findForeignActiveEpicLock(
296
- epicId,
297
- { repoRoot, fsImpl = fs, killFn = process.kill.bind(process) } = {},
298
- ) {
299
- if (!repoRoot) {
300
- throw new Error('findForeignActiveEpicLock: repoRoot is required');
301
- }
302
- const dir = resolveGitCommonDir(repoRoot, fsImpl);
303
- let entries;
304
- try {
305
- entries = fsImpl.readdirSync(dir);
306
- } catch {
307
- return null;
308
- }
309
-
310
- const foreignEntries = entries
311
- .map((entry) => ({ entry, otherEpicId: parseLockFileEpicId(entry) }))
312
- .filter(
313
- ({ otherEpicId }) =>
314
- otherEpicId !== null && String(otherEpicId) !== String(epicId),
315
- );
316
-
317
- for (const { entry, otherEpicId } of foreignEntries) {
318
- const live = readLiveLock(dir, entry, otherEpicId, fsImpl, killFn);
319
- if (live) return live;
320
- }
321
- return null;
322
- }