mandrel 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. package/.agents/README.md +59 -28
  2. package/.agents/agents/acceptance-critic.md +20 -9
  3. package/.agents/agents/story-worker.md +45 -48
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +60 -46
  6. package/.agents/docs/agentrc-reference.json +8 -13
  7. package/.agents/docs/configuration.md +33 -57
  8. package/.agents/docs/execution-reference.md +39 -10
  9. package/.agents/docs/quality-gates.md +17 -19
  10. package/.agents/docs/workflows.md +6 -6
  11. package/.agents/instructions.md +64 -79
  12. package/.agents/rules/ci-remediation.md +3 -3
  13. package/.agents/rules/gherkin-standards.md +10 -0
  14. package/.agents/rules/git-conventions-reference.md +42 -51
  15. package/.agents/schemas/acceptance-eval-verdict.schema.json +2 -2
  16. package/.agents/schemas/agentrc.schema.json +35 -46
  17. package/.agents/schemas/audit-rules.json +59 -1
  18. package/.agents/schemas/audit-rules.schema.json +33 -1
  19. package/.agents/schemas/lifecycle/README.md +1 -2
  20. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  21. package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
  22. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
  23. package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
  24. package/.agents/schemas/signal-event.schema.json +3 -3
  25. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  26. package/.agents/schemas/validation-evidence.schema.json +1 -1
  27. package/.agents/scripts/acceptance-eval.js +24 -68
  28. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  29. package/.agents/scripts/bootstrap.js +3 -3
  30. package/.agents/scripts/check-dead-exports.js +43 -104
  31. package/.agents/scripts/check-doc-links.js +2 -2
  32. package/.agents/scripts/check-lifecycle-lint.js +1 -1
  33. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  34. package/.agents/scripts/deliver-recover.js +122 -0
  35. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  36. package/.agents/scripts/evidence-gate.js +20 -50
  37. package/.agents/scripts/generate-skills-index.js +17 -1
  38. package/.agents/scripts/generate-workflows-doc.js +4 -4
  39. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  40. package/.agents/scripts/lib/audit-suite/selector.js +323 -23
  41. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
  42. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  43. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  44. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  45. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  46. package/.agents/scripts/lib/checks/core-bare-clean.js +4 -1
  47. package/.agents/scripts/lib/checks/index.js +1 -1
  48. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  49. package/.agents/scripts/lib/checks/state.js +17 -248
  50. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +3 -3
  51. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  52. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  53. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  54. package/.agents/scripts/lib/cli-args.js +23 -2
  55. package/.agents/scripts/lib/close-validation/gates.js +13 -13
  56. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  57. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  58. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  59. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  60. package/.agents/scripts/lib/config/acceptance-eval.js +2 -2
  61. package/.agents/scripts/lib/config/delivery-routing.js +7 -6
  62. package/.agents/scripts/lib/config/explain.js +10 -16
  63. package/.agents/scripts/lib/config/github.js +7 -5
  64. package/.agents/scripts/lib/config/limits.js +15 -25
  65. package/.agents/scripts/lib/config/quality.js +11 -14
  66. package/.agents/scripts/lib/config/runners.js +8 -21
  67. package/.agents/scripts/lib/config/temp-paths.js +18 -56
  68. package/.agents/scripts/lib/config-settings-schema-delivery.js +34 -16
  69. package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
  70. package/.agents/scripts/lib/config-settings-schema.js +48 -22
  71. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  72. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  73. package/.agents/scripts/lib/duplicate-search.js +38 -7
  74. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  75. package/.agents/scripts/lib/format-generated-json.js +97 -0
  76. package/.agents/scripts/lib/framework-version.js +19 -189
  77. package/.agents/scripts/lib/gh-exec.js +8 -0
  78. package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
  79. package/.agents/scripts/lib/git-utils.js +0 -14
  80. package/.agents/scripts/lib/json-utils.js +1 -2
  81. package/.agents/scripts/lib/label-constants.js +0 -15
  82. package/.agents/scripts/lib/label-taxonomy.js +1 -12
  83. package/.agents/scripts/lib/observability/active-story-env.js +42 -163
  84. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  85. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  86. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  87. package/.agents/scripts/lib/observability/source-classifier.js +2 -2
  88. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  89. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  90. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  91. package/.agents/scripts/lib/orchestration/ceremony-routing.js +43 -45
  92. package/.agents/scripts/lib/orchestration/change-set.js +103 -0
  93. package/.agents/scripts/lib/orchestration/code-review.js +70 -191
  94. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
  95. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  96. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  97. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  98. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  99. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  100. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +9 -11
  101. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  102. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  103. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +7 -3
  104. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  105. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  106. package/.agents/scripts/lib/orchestration/merge-block-class.js +76 -20
  107. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  108. package/.agents/scripts/lib/orchestration/plan-context.js +116 -33
  109. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +26 -36
  110. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +31 -22
  111. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  112. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
  113. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
  114. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  115. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -100
  116. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
  117. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
  118. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  119. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +230 -0
  120. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
  121. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -2
  122. package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
  123. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  124. package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
  125. package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
  126. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  127. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  128. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  129. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  130. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  131. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  132. package/.agents/scripts/lib/orchestration/run-epilogue.js +374 -16
  133. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
  134. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  135. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  136. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +4 -13
  137. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  138. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +72 -30
  139. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  140. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  141. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +12 -8
  142. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  143. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  144. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +264 -43
  145. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  146. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  147. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +104 -279
  148. package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +191 -0
  149. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +120 -0
  150. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  151. package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
  152. package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
  153. package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
  154. package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
  155. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  156. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
  157. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
  158. package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
  159. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
  160. package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
  161. package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
  162. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  163. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  164. package/.agents/scripts/lib/planning-corpus.js +12 -286
  165. package/.agents/scripts/lib/preflight-runner.js +2 -2
  166. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  167. package/.agents/scripts/lib/signals/index.js +4 -17
  168. package/.agents/scripts/lib/signals/read.js +35 -35
  169. package/.agents/scripts/lib/signals/schema.js +8 -11
  170. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  171. package/.agents/scripts/lib/signals/write.js +0 -1
  172. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  173. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  174. package/.agents/scripts/lib/story-adjacency.js +8 -7
  175. package/.agents/scripts/lib/story-body/story-body.js +81 -13
  176. package/.agents/scripts/lib/templates/decomposer-prompts.js +15 -16
  177. package/.agents/scripts/lib/test-env.js +14 -1
  178. package/.agents/scripts/lib/test-tiers.js +0 -3
  179. package/.agents/scripts/lib/ticket-body-sections.js +0 -14
  180. package/.agents/scripts/lib/validation-evidence.js +31 -59
  181. package/.agents/scripts/lib/wave-runner/live-probe.js +315 -0
  182. package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
  183. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  184. package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
  185. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  186. package/.agents/scripts/plan-context.js +38 -7
  187. package/.agents/scripts/plan-critics.js +203 -0
  188. package/.agents/scripts/plan-persist.js +145 -35
  189. package/.agents/scripts/plan-run-epilogue.js +83 -38
  190. package/.agents/scripts/post-structured-comment.js +0 -38
  191. package/.agents/scripts/pr-watch-with-update.js +43 -22
  192. package/.agents/scripts/providers/github/compose.js +0 -1
  193. package/.agents/scripts/providers/github/errors.js +0 -19
  194. package/.agents/scripts/providers/github/issues.js +1 -11
  195. package/.agents/scripts/providers/github/mappers.js +5 -0
  196. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  197. package/.agents/scripts/providers/github/tickets.js +33 -153
  198. package/.agents/scripts/providers/github.js +17 -6
  199. package/.agents/scripts/quality-preview.js +13 -6
  200. package/.agents/scripts/resolve-stories.js +236 -0
  201. package/.agents/scripts/run-coverage.js +4 -1
  202. package/.agents/scripts/run-lint.js +2 -2
  203. package/.agents/scripts/run-verify.js +31 -2
  204. package/.agents/scripts/signals-view.js +9 -10
  205. package/.agents/scripts/single-story-close.js +173 -18
  206. package/.agents/scripts/single-story-confirm-merge.js +288 -15
  207. package/.agents/scripts/single-story-init.js +6 -10
  208. package/.agents/scripts/stories-wave-tick.js +380 -53
  209. package/.agents/scripts/story-plan.js +3 -3
  210. package/.agents/scripts/update-ticket-state.js +8 -50
  211. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  212. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  213. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  214. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  215. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  216. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  217. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  218. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  219. package/.agents/skills/core/idea-refinement/SKILL.md +3 -3
  220. package/.agents/skills/core/scope-triage/SKILL.md +3 -0
  221. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  222. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  223. package/.agents/skills/skills.index.json +2 -12
  224. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  225. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  226. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  227. package/.agents/workflows/audit-architecture.md +3 -4
  228. package/.agents/workflows/audit-clean-code.md +4 -4
  229. package/.agents/workflows/audit-documentation.md +4 -5
  230. package/.agents/workflows/audit-lighthouse.md +8 -0
  231. package/.agents/workflows/audit-navigability.md +10 -0
  232. package/.agents/workflows/audit-performance.md +2 -3
  233. package/.agents/workflows/audit-quality.md +8 -9
  234. package/.agents/workflows/audit-security.md +1 -2
  235. package/.agents/workflows/audit-seo.md +10 -0
  236. package/.agents/workflows/audit-ux-ui.md +7 -0
  237. package/.agents/workflows/deliver.md +133 -45
  238. package/.agents/workflows/git-cleanup.md +2 -2
  239. package/.agents/workflows/git-deliver.md +1 -1
  240. package/.agents/workflows/helpers/acceptance-self-eval.md +34 -17
  241. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -12
  242. package/.agents/workflows/helpers/code-review.md +14 -12
  243. package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
  244. package/.agents/workflows/helpers/deliver-story.md +209 -118
  245. package/.agents/workflows/helpers/parallel-tooling.md +2 -2
  246. package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
  247. package/.agents/workflows/plan.md +239 -19
  248. package/.agents/workflows/qa-assist.md +6 -6
  249. package/.agents/workflows/qa-explore.md +3 -3
  250. package/.agents/workflows/qa-run.md +1 -5
  251. package/bin/mandrel.js +12 -1
  252. package/docs/CHANGELOG.md +62 -0
  253. package/lib/cli/registry.js +262 -19
  254. package/lib/cli/sync-agents.js +157 -0
  255. package/lib/cli/sync-commands.js +115 -6
  256. package/lib/cli/sync.js +168 -6
  257. package/lib/cli/update.js +105 -8
  258. package/lib/cli/version-helpers.js +131 -0
  259. package/lib/migrations/README.md +7 -5
  260. package/lib/migrations/index.js +17 -9
  261. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  262. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  263. package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +154 -0
  264. package/package.json +2 -2
  265. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  266. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  267. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  268. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  269. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  270. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  271. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  272. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  273. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  274. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  275. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  276. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  277. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  278. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  279. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  280. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  281. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  282. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  283. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  284. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  285. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  286. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  287. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  288. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  289. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  290. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  291. package/.agents/schemas/risk-verdict.schema.json +0 -53
  292. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  293. package/.agents/scripts/analyze-execution.js +0 -444
  294. package/.agents/scripts/check-prepush-recovery.js +0 -90
  295. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  296. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  297. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -187
  298. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  299. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  300. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  301. package/.agents/scripts/lib/orchestration/audit-lens-routing.js +0 -128
  302. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
  303. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  304. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  305. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  306. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  307. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  308. package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
  309. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  310. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  311. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  312. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -21
  313. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  314. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  315. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  316. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -397
  317. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  318. package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
  319. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  320. package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
  321. package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
  322. package/.agents/scripts/resolve-plan-run.js +0 -117
  323. package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
@@ -0,0 +1,315 @@
1
+ /**
2
+ * lib/wave-runner/live-probe.js — the state-probing adapter that feeds the
3
+ * ready-set kernel from live GitHub state.
4
+ *
5
+ * `selectReadySet` (`./ready-set.js`) is deliberately a pure, side-effect-free
6
+ * kernel: callers hand it the live Story records, the done set, and the
7
+ * in-flight count, and it decides. Until now the only adapter was the
8
+ * flag-driven one (`stories-wave-tick.js --dag/--done/--in-flight`), which
9
+ * pushed the *gathering* of those inputs onto the caller — in practice onto
10
+ * the host LLM following `/deliver`'s prose, re-seeding `--done` and counting
11
+ * `--in-flight` by hand every beat. That is hand-maintained accounting on the
12
+ * one correctness-critical path where a mistake silently wedges a run (a
13
+ * dropped foreign blocker) or double-dispatches a Story (a miscounted slot).
14
+ *
15
+ * This module closes that gap by **probing** the same facts the host was
16
+ * transcribing:
17
+ *
18
+ * - **done** — an `agent::done` label OR a closed issue, the same predicate
19
+ * `classifyStory` already applies, evaluated over live state rather than a
20
+ * `--done` CSV the caller maintained across beats. Foreign blockers
21
+ * (outside the delivered set) are resolved too, which is what makes
22
+ * cross-run delivery work: a blocker that merged weeks ago in another run
23
+ * is simply done.
24
+ * - **in-flight** — derived from live `agent::executing` / `agent::closing`
25
+ * labels, **unioned with the ids the host says it has dispatched**
26
+ * (`--dispatched`). The label alone is not sufficient: the kernel's
27
+ * contract counts "executing / closing / dispatched-not-yet-labelled" as
28
+ * in-flight, and `single-story-init.js` flips `agent::executing` at step 6
29
+ * of 6 — *after* a 3–6 minute worktree install. For that whole window a
30
+ * dispatched Story still reads `agent::ready`, so a label-only derivation
31
+ * re-emits it in the next beat's `ready[]` and the host dispatches it a
32
+ * second time onto the same branch and worktree (Story #4601).
33
+ * - **blocked** — the ids carrying `agent::blocked`. `classifyStory` has
34
+ * always returned this class; nothing consumed it, so a blocked Story was
35
+ * neither done, ready, nor in-flight and the beat reported a permanent
36
+ * "waiting" (Story #4601).
37
+ *
38
+ * It is an **adapter, not a kernel change**: it gathers inputs and hands them
39
+ * to `selectReadySet` unchanged. The kernel stays pure and flag-driven, and
40
+ * the legacy flag mode stays byte-compatible.
41
+ *
42
+ * The graph resolution is **not** reimplemented here — it reuses
43
+ * `resolve-stories.js`'s machinery wholesale (body `depends_on` ∪ native
44
+ * `blocked_by` edges, foreign-blocker resolution, `files[]` footprints), so
45
+ * the probe and `/deliver`'s step-1 resolution cannot disagree about what
46
+ * depends on what.
47
+ *
48
+ * @module lib/wave-runner/live-probe
49
+ */
50
+
51
+ import {
52
+ fetchStories,
53
+ readNativeEdges,
54
+ resolveForeignDone,
55
+ resolveStoriesProvider,
56
+ } from '../../resolve-stories.js';
57
+ import { AGENT_LABELS } from '../label-constants.js';
58
+ import { buildStoriesEnvelope } from '../orchestration/resolve-stories.js';
59
+ import { classifyStory, storyIdOf } from './ready-set.js';
60
+
61
+ /**
62
+ * Identify the Stories that currently occupy a dispatch slot, as an id set.
63
+ *
64
+ * Two sources, unioned — which is exactly the kernel's stated contract
65
+ * ("executing / closing / dispatched-not-yet-labelled"):
66
+ *
67
+ * 1. **Live labels.** `classifyStory` folds `agent::executing` and
68
+ * `agent::closing` into one `executing` class — both are in-flight and
69
+ * neither may be re-dispatched.
70
+ * 2. **`dispatched`** — ids the host has spawned. This closes the init
71
+ * window: `single-story-init.js` flips `agent::executing` last, after a
72
+ * 3–6 minute install, so between spawn and flip a dispatched Story reads
73
+ * `agent::ready` and a label-only derivation hands it back as ready.
74
+ *
75
+ * `dispatched` is deliberately **not** the `--done`-style accounting probe
76
+ * mode retired. Three properties keep it from becoming one:
77
+ *
78
+ * - **It is a set union, not a counter.** Re-passing an id that has since
79
+ * picked up its `agent::executing` label cannot double-count a slot.
80
+ * - **Live state overrules the claim.** An id the host still lists but that
81
+ * now classifies `done` (or `blocked`) is dropped, so a stale entry can
82
+ * never occupy a slot forever and starve the run.
83
+ * - **Therefore the host's correct strategy is monotonic append**: pass
84
+ * every id you have dispatched this run and never reason about removing
85
+ * one. There is no drop-a-slot decision to get wrong — the probe subtracts
86
+ * reality from the claim. Forgetting an id degrades to the pre-#4601
87
+ * behaviour rather than to something worse.
88
+ *
89
+ * Ids outside the probed set are ignored: they are not part of this run and
90
+ * must not consume its cap.
91
+ *
92
+ * @param {Array<{id?: number, number?: number, labels?: string[], state?: string}>} storyRecords
93
+ * @param {Iterable<number>} [dispatched] Ids the host has spawned.
94
+ * @returns {Set<number>} In-flight Story ids.
95
+ */
96
+ function deriveInFlightIds(storyRecords, dispatched = []) {
97
+ const claimed = new Set(dispatched);
98
+ const inFlight = new Set();
99
+ for (const rec of storyRecords) {
100
+ const id = storyIdOf(rec);
101
+ if (id === null) continue;
102
+ const cls = classifyStory(rec);
103
+ if (cls === 'executing' || (claimed.has(id) && cls === 'ready')) {
104
+ inFlight.add(id);
105
+ }
106
+ }
107
+ return inFlight;
108
+ }
109
+
110
+ /**
111
+ * The ids carrying `agent::blocked`.
112
+ *
113
+ * `classifyStory` has always returned a `blocked` class, but no adapter
114
+ * consumed it: a blocked Story was never done, never ready, and never counted
115
+ * in-flight, so `detectWedge` dropped it (its "undone work with no unmet
116
+ * blockers would have been dispatched" invariant is precisely what probe mode
117
+ * broke) and the beat reported exit 0 / `ready: []` / `wedged: null` forever.
118
+ * `/deliver` reads that as "waiting", so the `agent::blocked` HITL pause — the
119
+ * one runtime gate in the protocol — was never surfaced to the operator.
120
+ *
121
+ * @param {Array<{id?: number, number?: number, labels?: string[], state?: string}>} storyRecords
122
+ * @returns {number[]} Blocked Story ids, ascending.
123
+ */
124
+ function deriveBlockedIds(storyRecords) {
125
+ return storyRecords
126
+ .filter((rec) => classifyStory(rec) === 'blocked')
127
+ .map((rec) => storyIdOf(rec))
128
+ .filter((id) => id !== null)
129
+ .sort((a, b) => a - b);
130
+ }
131
+
132
+ /**
133
+ * Resolve the provider + repo coordinates the probe reads through.
134
+ *
135
+ * Shares `resolve-stories.js`'s provider seam, so probe mode authenticates and
136
+ * targets exactly the same repo `/deliver`'s resolution step does. Tests
137
+ * inject a stub provider instead of calling this.
138
+ *
139
+ * @param {object} [deps]
140
+ * @param {Function} [deps.resolveProvider] Injection seam for tests.
141
+ * @returns {{ provider: object, owner: string|undefined, repo: string|undefined }}
142
+ */
143
+ export function createProbeContext({
144
+ resolveProvider = resolveStoriesProvider,
145
+ } = {}) {
146
+ const { provider, config } = resolveProvider();
147
+ return { provider, owner: config?.github?.owner, repo: config?.github?.repo };
148
+ }
149
+
150
+ /**
151
+ * Probe live state for a set of Story ids and return the exact inputs
152
+ * `selectReadySet` consumes.
153
+ *
154
+ * Mirrors `resolve-stories.js`'s two-pass envelope build: a provisional pass
155
+ * yields the DAG whose foreign dependency ids are then resolved against live
156
+ * issue state, and the second pass folds those satisfied foreign blockers into
157
+ * `done[]`. Skipping that pass would withhold any Story whose blocker landed
158
+ * outside the delivered set — the cross-run wedge the resolver exists to fix.
159
+ *
160
+ * @param {object} args
161
+ * @param {number[]} args.ids Story ids in the run.
162
+ * @param {object} args.provider GitHub provider (stubbed in tests).
163
+ * @param {string} [args.owner]
164
+ * @param {string} [args.repo]
165
+ * @param {boolean} [args.native=true] Read native `blocked_by` edges.
166
+ * @param {number[]} [args.dispatched=[]] Ids the host has spawned but may not
167
+ * yet have observed labelled `agent::executing` (see `deriveInFlightIds`).
168
+ * @param {(msg: string) => void} [args.warn]
169
+ * Each returned node carries its **live labels**. That is load-bearing, not
170
+ * decoration: `selectReadySet` classifies from labels, so a node stripped of
171
+ * them reads as `ready` and an `agent::executing` Story gets re-dispatched
172
+ * onto a second branch while its first run is still going. The resolver's DAG
173
+ * projection (`{id, dependsOn, files}`) drops labels because flag mode's
174
+ * caller tracked in-flight itself; probe mode must put them back.
175
+ *
176
+ * @returns {Promise<{
177
+ * nodes: Array<{id: number, dependsOn: number[], files: string[], labels: string[]}>,
178
+ * doneIds: Set<number>,
179
+ * inFlight: number,
180
+ * blockedIds: number[]
181
+ * }>}
182
+ */
183
+ export async function probeLiveState({
184
+ ids,
185
+ provider,
186
+ owner,
187
+ repo,
188
+ native = true,
189
+ dispatched = [],
190
+ warn,
191
+ }) {
192
+ const stories = await fetchStories(provider, ids);
193
+ const nativeEdges = native
194
+ ? await readNativeEdges({ provider, stories, owner, repo })
195
+ : new Map();
196
+
197
+ const provisional = buildStoriesEnvelope({ stories, nativeEdges, warn });
198
+ const foreignDone = await resolveForeignDone({
199
+ provider,
200
+ dag: provisional.dag,
201
+ inSetIds: new Set(stories.map((s) => s.id)),
202
+ });
203
+ const envelope = buildStoriesEnvelope({
204
+ stories,
205
+ nativeEdges,
206
+ foreignDone,
207
+ warn: () => {},
208
+ });
209
+
210
+ const labelsById = new Map(stories.map((s) => [s.id, s.labels ?? []]));
211
+ const inFlightIds = deriveInFlightIds(stories, dispatched);
212
+ return {
213
+ nodes: envelope.dag.map((node) => ({
214
+ ...node,
215
+ labels: projectInFlightLabels(
216
+ labelsById.get(node.id) ?? [],
217
+ inFlightIds.has(node.id),
218
+ ),
219
+ })),
220
+ doneIds: new Set(envelope.done),
221
+ inFlight: inFlightIds.size,
222
+ blockedIds: deriveBlockedIds(stories),
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Project the in-flight fact onto a node's labels, synthesizing
228
+ * `agent::executing` for a Story that is dispatched but not yet labelled.
229
+ *
230
+ * This is the load-bearing half of the dispatch-window fix, and it is why
231
+ * `inFlight` alone is not enough. The two inputs do **different** jobs inside
232
+ * `selectReadySet`:
233
+ *
234
+ * - `inFlight` is only a **count**. It reserves capacity (`slots = cap −
235
+ * inFlight`) and nothing more.
236
+ * - **Eligibility is decided per-record by `classifyStory`**, from labels.
237
+ *
238
+ * So a dispatched-but-unlabelled Story counted only via `inFlight` still
239
+ * classifies `ready`, stays eligible, and — whenever a slot remains — is
240
+ * admitted to the very same beat that reserved a slot for it. It would be
241
+ * re-dispatched onto its own live branch, with the miscount merely reshaped
242
+ * rather than fixed. Handing the kernel the label makes it apply the rule it
243
+ * already has, and keeps the kernel itself untouched: the adapter's job is to
244
+ * supply the input the kernel's contract ("executing / closing / dispatched-
245
+ * not-yet-labelled") already specifies.
246
+ *
247
+ * @param {string[]} labels The Story's live labels.
248
+ * @param {boolean} inFlight Whether the Story occupies a dispatch slot.
249
+ * @returns {string[]} Labels, with `agent::executing` added when needed.
250
+ */
251
+ function projectInFlightLabels(labels, inFlight) {
252
+ if (!inFlight || classifyStory({ labels }) === 'executing') return labels;
253
+ return [...labels, AGENT_LABELS.EXECUTING];
254
+ }
255
+
256
+ /**
257
+ * Validate the mode-selecting flags, keeping probe mode and the legacy
258
+ * flag mode mutually exclusive.
259
+ *
260
+ * The exclusion is not pedantry: `--probe-live` derives `done` and `in-flight`
261
+ * from live state, so honouring a caller-supplied `--done` alongside it would
262
+ * silently reintroduce the hand-maintained accounting probe mode exists to
263
+ * retire — and quietly disagree with reality when the two differ.
264
+ *
265
+ * `--dispatched` is the deliberate exception, and it is **additive rather than
266
+ * authoritative**: it does not replace the derived in-flight set, it is unioned
267
+ * into it and then filtered by live state (see `deriveInFlightIds`). It carries
268
+ * the one fact the host knows and GitHub does not yet — "I spawned this id, the
269
+ * label has not appeared yet" — so it cannot disagree with reality the way an
270
+ * authoritative `--in-flight <n>` could. `--in-flight` therefore stays excluded.
271
+ *
272
+ * @param {object} flags
273
+ * @param {boolean} [flags.probeLive]
274
+ * @param {string} [flags.stories]
275
+ * @param {string} [flags.dag]
276
+ * @param {string} [flags.dagFile]
277
+ * @param {string} [flags.done]
278
+ * @param {string} [flags.inFlight]
279
+ * @param {string} [flags.dispatched]
280
+ * @returns {string|null} An error message, or `null` when the flags are valid.
281
+ */
282
+ export function validateProbeFlags({
283
+ probeLive,
284
+ stories,
285
+ dag,
286
+ dagFile,
287
+ done,
288
+ inFlight,
289
+ dispatched,
290
+ } = {}) {
291
+ if (!probeLive) {
292
+ if (dispatched != null) {
293
+ return '--dispatched requires --probe-live (it augments the live-derived in-flight set; flag mode uses --in-flight <n>)';
294
+ }
295
+ return stories
296
+ ? '--stories requires --probe-live (it names the run to probe from live state)'
297
+ : null;
298
+ }
299
+ const conflicting = [
300
+ dag ? '--dag' : null,
301
+ dagFile ? '--dag-file' : null,
302
+ done != null ? '--done' : null,
303
+ inFlight != null ? '--in-flight' : null,
304
+ ].filter(Boolean);
305
+ if (conflicting.length > 0) {
306
+ return (
307
+ `--probe-live is mutually exclusive with ${conflicting.join(', ')}: it resolves the graph ` +
308
+ `and derives done / in-flight from live state. Drop the flag(s), or use the legacy flag mode.`
309
+ );
310
+ }
311
+ if (!stories) {
312
+ return '--probe-live requires --stories <csv> of Story ids';
313
+ }
314
+ return null;
315
+ }
@@ -156,15 +156,37 @@ export function storyFootprint(story) {
156
156
  return out;
157
157
  }
158
158
 
159
+ /**
160
+ * Does a declared path contain a glob metacharacter? Mirrors the detection
161
+ * in `story-body.js#extractChangePaths`, whose `isGlob` flag documents an
162
+ * "unknown-width footprint" policy that was never implemented downstream.
163
+ *
164
+ * @param {string} path
165
+ * @returns {boolean}
166
+ */
167
+ function isGlobPath(path) {
168
+ return path.includes('*') || path.includes('?') || path.includes('{');
169
+ }
170
+
159
171
  /**
160
172
  * File-overlap co-dispatch guard. Returns `true` when two Stories' declared
161
- * file footprints intersect on at least one path — meaning they would race
162
- * the same file if dispatched onto parallel `story-<id>` branches in the
163
- * same beat. Two Stories that overlap MUST NOT both appear in one dispatch
164
- * set; one is withheld until the other clears.
173
+ * file footprints intersect — meaning they would race the same file if
174
+ * dispatched onto parallel `story-<id>` branches in the same beat. Two
175
+ * Stories that overlap MUST NOT both appear in one dispatch set; one is
176
+ * withheld until the other clears.
165
177
  *
166
- * An empty footprint on either side means "no known overlap" → `false`. A
167
- * Story that declares no files is therefore never withheld by this guard.
178
+ * Two deliberate asymmetries:
179
+ *
180
+ * - **An empty footprint means "no known overlap"** → `false`. A Story that
181
+ * declares no files is never withheld. This is permissive by necessity:
182
+ * an undeclared footprint carries no information, and withholding on
183
+ * absence would serialize every run.
184
+ * - **A glob footprint overlaps EVERYTHING** → `true` (Story #4539/#4540).
185
+ * Comparison is exact-string, so a Story declaring
186
+ * `.agents/scripts/lib/**` would not match another declaring
187
+ * `.agents/scripts/lib/story-adjacency.js` — the guard would silently
188
+ * pass two Stories that genuinely race. Unknown width is not the same as
189
+ * no width: fail safe by serializing.
168
190
  *
169
191
  * @param {StoryRecord} a
170
192
  * @param {StoryRecord} b
@@ -176,8 +198,12 @@ export function storiesOverlap(a, b) {
176
198
  const fb = storyFootprint(b);
177
199
  if (fb.size === 0) return false;
178
200
  for (const path of fa) {
201
+ if (isGlobPath(path)) return true;
179
202
  if (fb.has(path)) return true;
180
203
  }
204
+ for (const path of fb) {
205
+ if (isGlobPath(path)) return true;
206
+ }
181
207
  return false;
182
208
  }
183
209
 
@@ -7,7 +7,7 @@
7
7
  * on a Windows lock-class failure, the entry is appended to
8
8
  * `.worktrees/.pending-cleanup.json`. The plan-time `worktree-sweep.js`
9
9
  * reader (shipped in Epic #349) picks up the manifest on the next
10
- * `/epic-plan-spec` / `/epic-plan-decompose` runs and retries removal
10
+ * `/plan` run and retries removal
11
11
  * (`git worktree remove` then `fs.rm`) — by then the live file handles from Node / AV / the Windows
12
12
  * Search indexer are almost always gone. If `MAX_SWEEP_ATTEMPTS` elapses
13
13
  * without clearing, an `OPERATOR ACTION REQUIRED: persistent-lock` line
@@ -15,7 +15,7 @@
15
15
 
16
16
  import fs from 'node:fs';
17
17
  import { rm as fsPromisesRm } from 'node:fs/promises';
18
- import { isInsideWorktree, samePath, storyIdFromPath } from '../inspector.js';
18
+ import { isInsideWorktree, samePath } from '../inspector.js';
19
19
  import { sleepSync } from '../node-modules-strategy.js';
20
20
  import { checkMergeReachability } from './merge-reachability.js';
21
21
  import { recordPendingCleanup } from './pending-cleanup.js';
@@ -72,21 +72,58 @@ export async function isSafeToRemove(ctx, wtPath, opts = {}) {
72
72
  }
73
73
 
74
74
  /**
75
- * Returns true iff `branch` is already fully merged into `epicBranch`
76
- * (i.e. `merge-base --is-ancestor branch epicBranch` exits 0). A missing
77
- * epicBranch or a git failure both yield false so callers default to the
78
- * safe, non-forcing behavior.
75
+ * Returns true iff `branch`'s work is demonstrably already integrated into
76
+ * `baseRef`. Used only to license discarding a **dirty** tree, so it must
77
+ * err toward `false`.
78
+ *
79
+ * Two-phase:
80
+ * 1. `merge-base --is-ancestor` — cheap SHA reachability; true for
81
+ * fast-forward and merge-commit integration.
82
+ * 2. `git cherry <baseRef> <branch>` — compares **patch-ids** rather than
83
+ * SHAs, marking a commit `-` when an equivalent change already exists
84
+ * upstream. All-`-` (or empty) means every commit on the branch is
85
+ * present in the base under some SHA.
86
+ *
87
+ * **Known limit — this is not a general squash detector.** A squash collapses
88
+ * N commits into ONE new commit whose patch equals their *combined* diff, so
89
+ * no individual original commit has an upstream patch-id equivalent and
90
+ * `git cherry` marks them all `+`. Phase 2 therefore only recognises a
91
+ * squash-landed branch when the branch had a **single** commit (verified
92
+ * empirically, Story #4539). A multi-commit squash still reads as unmerged
93
+ * and the dirty tree is refused — the safe direction, and the same answer
94
+ * the previous ancestor-only check gave. Detecting the general case needs
95
+ * the PR's merged state, which this module has no client for.
96
+ *
97
+ * A missing ref or a git failure yields false, so callers default to the
98
+ * safe, non-forcing behavior. Module-private: `ensureSafeOrForceDiscard` is
99
+ * its only caller, and the symbol it replaced was exported with no consumer
100
+ * and then baselined as a dead export — repeating that would just hide a
101
+ * new corpse.
102
+ *
103
+ * @param {object} ctx
104
+ * @param {string} branch
105
+ * @param {string} baseRef
106
+ * @returns {boolean}
79
107
  */
80
- export function isStoryAlreadyMergedIntoEpic(ctx, branch, epicBranch) {
81
- if (!branch || !epicBranch) return false;
82
- const res = ctx.git.gitSpawn(
108
+ function isBranchMergedIntoBase(ctx, branch, baseRef) {
109
+ if (!branch || !baseRef) return false;
110
+ const ancestor = ctx.git.gitSpawn(
83
111
  ctx.repoRoot,
84
112
  'merge-base',
85
113
  '--is-ancestor',
86
114
  branch,
87
- epicBranch,
115
+ baseRef,
88
116
  );
89
- return res.status === 0;
117
+ if (ancestor.status === 0) return true;
118
+
119
+ const cherry = ctx.git.gitSpawn(ctx.repoRoot, 'cherry', baseRef, branch);
120
+ if (cherry.status !== 0) return false;
121
+ const lines = (cherry.stdout ?? '')
122
+ .split('\n')
123
+ .map((l) => l.trim())
124
+ .filter(Boolean);
125
+ // Every line starting with '+' is a commit with no upstream equivalent.
126
+ return lines.every((line) => line.startsWith('-'));
90
127
  }
91
128
 
92
129
  /**
@@ -470,28 +507,40 @@ function checkReapPreconditions(ctx, _storyId, opts, wtPath) {
470
507
  ok: false,
471
508
  result: { removed: false, reason: 'not-a-worktree', path: wtPath },
472
509
  };
473
- if (storyIdFromPath(wtPath, ctx.worktreeRoot) !== null && !opts.epicBranch) {
474
- return {
475
- ok: false,
476
- result: { removed: false, reason: 'epic-branch-required', path: wtPath },
477
- };
478
- }
510
+ // Story #4539 removed an `epic-branch-required` gate here: a
511
+ // `story-<id>` worktree used to be unreapable unless the caller supplied
512
+ // an Epic integration branch. v2 has no Epic branch, and the only v2
513
+ // caller (the close path) never passed one — so EVERY close silently
514
+ // failed to reap while reporting success, and cleanup fell to the next
515
+ // boot-sweep.
516
+ //
517
+ // Nothing is lost by dropping it. The close path reaps AFTER pushing
518
+ // `story-<id>` to origin and opening the PR, so the work is durable
519
+ // off-machine; and the real safety net is unchanged — `isSafeToRemove`
520
+ // still refuses a dirty tree (`uncommitted-changes`), which is what
521
+ // actually protects unsaved work.
479
522
  return { ok: true };
480
523
  }
481
524
 
482
525
  async function ensureSafeOrForceDiscard(ctx, storyId, wtPath, opts) {
526
+ const baseRef = opts.baseRef ?? opts.epicBranch ?? null;
483
527
  const safety = await isSafeToRemove(ctx, wtPath, {
484
- epicBranch: opts.epicBranch ?? null,
528
+ epicBranch: baseRef,
485
529
  });
486
530
  if (safety.safe) return { ok: true, discardedPaths: null };
487
531
 
488
532
  const discardAfterMerge = opts.discardAfterMerge !== false;
489
533
  const branchName = `story-${validateStoryId(storyId)}`;
534
+ // Discarding a dirty tree is only permissible when the branch's work is
535
+ // demonstrably already integrated. See `isBranchMergedIntoBase` for what
536
+ // that can and cannot prove — notably a multi-commit squash reads as
537
+ // unmerged, so the discard is refused and the tree survives. Refusing is
538
+ // the safe direction: the cost is a stale worktree, not lost work.
490
539
  const canForceReap =
491
540
  discardAfterMerge &&
492
541
  safety.reason === 'uncommitted-changes' &&
493
- opts.epicBranch &&
494
- isStoryAlreadyMergedIntoEpic(ctx, branchName, opts.epicBranch);
542
+ baseRef &&
543
+ isBranchMergedIntoBase(ctx, branchName, baseRef);
495
544
  if (!canForceReap) {
496
545
  ctx.logger.warn(
497
546
  `reap-skipped storyId=${storyId} reason=${safety.reason} path=${wtPath}`,
@@ -7,7 +7,7 @@
7
7
  * - `creation.js` — `ensure`
8
8
  * - `registry-sync.js` — `pathFor`, `list`, `prune`, `getWorktreeList`,
9
9
  * `invalidateWorktreeCache`, `findByPath`
10
- * - `reap.js` — `isSafeToRemove`, `isStoryAlreadyMergedIntoEpic`,
10
+ * - `reap.js` — `isSafeToRemove`,
11
11
  * `removeWorktreeWithRecovery`, `reap`
12
12
  * - `gc.js` — `gc`
13
13
  * - `drift-detection.js` — `sweepStaleLocks`
@@ -26,7 +26,6 @@ export { sweepStaleLocks } from './lifecycle/drift-detection.js';
26
26
  export { gc } from './lifecycle/gc.js';
27
27
  export {
28
28
  isSafeToRemove,
29
- isStoryAlreadyMergedIntoEpic,
30
29
  reap,
31
30
  removeWorktreeWithRecovery,
32
31
  } from './lifecycle/reap.js';
@@ -18,8 +18,14 @@
18
18
  * Stories. Envelope carries `sourceTickets[]`.
19
19
  *
20
20
  * Flags:
21
+ * --out <path> Also write the envelope to <path> (parent dirs created).
22
+ * `/plan` points this at `<plan-dir>/plan-context.json`,
23
+ * which is where `plan-persist.js` auto-discovers the
24
+ * `--tickets` source ids from (Story #4554). Without a
25
+ * captured envelope persist cannot know a `--tickets` run
26
+ * happened, and superseding degrades to the
27
+ * `--source-tickets` flag.
21
28
  * --pretty Pretty-print the JSON envelope.
22
- * --full-context Bypass the planning-context budget (unbounded body).
23
29
  *
24
30
  * stdout is reserved for the JSON envelope (Story #2278 discipline):
25
31
  * `routeAllOutputToStderr()` runs before any pipeline code so a captured
@@ -34,13 +40,15 @@
34
40
  // first import so the check runs before any third-party-importing sibling
35
41
  // module is evaluated (Story #3432).
36
42
  import './lib/runtime-deps/ensure-installed.js';
43
+ import { mkdir, writeFile } from 'node:fs/promises';
44
+ import path from 'node:path';
37
45
  import { parseArgs } from 'node:util';
38
46
  import { runAsCli } from './lib/cli-utils.js';
39
47
  import {
40
48
  resolveConfig,
41
49
  validateOrchestrationConfig,
42
50
  } from './lib/config-resolver.js';
43
- import { routeAllOutputToStderr } from './lib/Logger.js';
51
+ import { Logger, routeAllOutputToStderr } from './lib/Logger.js';
44
52
  import { buildPlanContext } from './lib/orchestration/plan-context.js';
45
53
  import { recordPlanInvocation } from './lib/orchestration/plan-metrics.js';
46
54
  import { createProvider } from './lib/provider-factory.js';
@@ -84,8 +92,8 @@ export async function emitPlanContext({
84
92
  provider,
85
93
  config,
86
94
  settings,
87
- fullContext = false,
88
95
  pretty = false,
96
+ outPath = null,
89
97
  cwd,
90
98
  stdout = process.stdout,
91
99
  }) {
@@ -98,24 +106,48 @@ export async function emitPlanContext({
98
106
  provider,
99
107
  config,
100
108
  settings,
101
- fullContext,
102
109
  cwd,
103
110
  });
104
111
  const json = pretty
105
112
  ? JSON.stringify(envelope, null, 2)
106
113
  : JSON.stringify(envelope);
107
114
  stdout.write(`${json}\n`);
115
+ if (outPath) await writeEnvelopeFile(outPath, json);
108
116
  return envelope;
109
117
  }
110
118
 
119
+ /**
120
+ * Persist the envelope to `--out` so `plan-persist.js` can derive the
121
+ * `--tickets` source ids from it without an operator re-typing them.
122
+ *
123
+ * Writing is part of emitting, not a best-effort extra: a failed write means
124
+ * persist will silently see no source tickets, so it throws rather than
125
+ * warning past the problem.
126
+ *
127
+ * @param {string} outPath
128
+ * @param {string} json
129
+ */
130
+ async function writeEnvelopeFile(outPath, json) {
131
+ const resolved = path.resolve(outPath);
132
+ try {
133
+ await mkdir(path.dirname(resolved), { recursive: true });
134
+ await writeFile(resolved, `${json}\n`, 'utf8');
135
+ } catch (err) {
136
+ throw new Error(
137
+ `[plan-context] cannot write envelope to ${resolved}: ${err.message}`,
138
+ );
139
+ }
140
+ Logger.info(`[plan-context] wrote envelope to ${resolved}`);
141
+ }
142
+
111
143
  async function main() {
112
144
  const { values } = parseArgs({
113
145
  options: {
114
146
  seed: { type: 'string' },
115
147
  'seed-file': { type: 'string' },
116
148
  tickets: { type: 'string' },
149
+ out: { type: 'string' },
117
150
  pretty: { type: 'boolean', default: false },
118
- 'full-context': { type: 'boolean', default: false },
119
151
  },
120
152
  strict: true,
121
153
  });
@@ -173,7 +205,6 @@ async function main() {
173
205
  {
174
206
  cli: 'plan-context',
175
207
  mode,
176
- epicId: null,
177
208
  config,
178
209
  },
179
210
  () =>
@@ -185,8 +216,8 @@ async function main() {
185
216
  provider,
186
217
  config,
187
218
  settings,
188
- fullContext: values['full-context'],
189
219
  pretty: values.pretty,
220
+ outPath: values.out || null,
190
221
  }),
191
222
  );
192
223
  }