mandrel 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/.agents/README.md +59 -28
  2. package/.agents/agents/acceptance-critic.md +9 -7
  3. package/.agents/agents/story-worker.md +41 -46
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +51 -44
  6. package/.agents/docs/agentrc-reference.json +8 -13
  7. package/.agents/docs/configuration.md +32 -56
  8. package/.agents/docs/execution-reference.md +39 -10
  9. package/.agents/docs/quality-gates.md +14 -16
  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/git-conventions-reference.md +42 -51
  14. package/.agents/schemas/agentrc.schema.json +34 -45
  15. package/.agents/schemas/audit-rules.json +59 -1
  16. package/.agents/schemas/audit-rules.schema.json +33 -1
  17. package/.agents/schemas/lifecycle/README.md +1 -2
  18. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  19. package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
  20. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
  21. package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
  22. package/.agents/schemas/signal-event.schema.json +3 -3
  23. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  24. package/.agents/schemas/validation-evidence.schema.json +1 -1
  25. package/.agents/scripts/acceptance-eval.js +22 -66
  26. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  27. package/.agents/scripts/bootstrap.js +3 -3
  28. package/.agents/scripts/check-dead-exports.js +43 -104
  29. package/.agents/scripts/check-doc-links.js +2 -2
  30. package/.agents/scripts/check-lifecycle-lint.js +1 -1
  31. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  32. package/.agents/scripts/deliver-recover.js +122 -0
  33. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  34. package/.agents/scripts/evidence-gate.js +20 -50
  35. package/.agents/scripts/generate-skills-index.js +17 -1
  36. package/.agents/scripts/generate-workflows-doc.js +4 -4
  37. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  38. package/.agents/scripts/lib/audit-suite/selector.js +323 -23
  39. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
  40. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  41. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  42. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  43. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  44. package/.agents/scripts/lib/checks/core-bare-clean.js +4 -1
  45. package/.agents/scripts/lib/checks/index.js +1 -1
  46. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  47. package/.agents/scripts/lib/checks/state.js +17 -248
  48. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +3 -3
  49. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  50. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  51. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  52. package/.agents/scripts/lib/cli-args.js +23 -2
  53. package/.agents/scripts/lib/close-validation/gates.js +13 -13
  54. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  55. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  56. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  57. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  58. package/.agents/scripts/lib/config/delivery-routing.js +7 -6
  59. package/.agents/scripts/lib/config/explain.js +10 -16
  60. package/.agents/scripts/lib/config/github.js +7 -5
  61. package/.agents/scripts/lib/config/limits.js +15 -25
  62. package/.agents/scripts/lib/config/quality.js +11 -14
  63. package/.agents/scripts/lib/config/runners.js +8 -21
  64. package/.agents/scripts/lib/config/temp-paths.js +18 -56
  65. package/.agents/scripts/lib/config-settings-schema-delivery.js +31 -13
  66. package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
  67. package/.agents/scripts/lib/config-settings-schema.js +48 -22
  68. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  69. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  70. package/.agents/scripts/lib/duplicate-search.js +38 -7
  71. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  72. package/.agents/scripts/lib/format-generated-json.js +97 -0
  73. package/.agents/scripts/lib/framework-version.js +19 -189
  74. package/.agents/scripts/lib/gh-exec.js +8 -0
  75. package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
  76. package/.agents/scripts/lib/git-utils.js +0 -14
  77. package/.agents/scripts/lib/json-utils.js +1 -2
  78. package/.agents/scripts/lib/label-constants.js +0 -15
  79. package/.agents/scripts/lib/label-taxonomy.js +1 -12
  80. package/.agents/scripts/lib/observability/active-story-env.js +42 -163
  81. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  82. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  83. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  84. package/.agents/scripts/lib/observability/source-classifier.js +2 -2
  85. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  86. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  87. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  88. package/.agents/scripts/lib/orchestration/ceremony-routing.js +43 -45
  89. package/.agents/scripts/lib/orchestration/code-review.js +58 -168
  90. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
  91. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  92. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  93. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  94. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  95. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  96. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +9 -11
  97. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  98. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  99. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +7 -3
  100. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  101. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  102. package/.agents/scripts/lib/orchestration/merge-block-class.js +76 -20
  103. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  104. package/.agents/scripts/lib/orchestration/plan-context.js +114 -24
  105. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +11 -22
  106. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +3 -7
  107. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  108. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
  109. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
  110. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  111. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -75
  112. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
  113. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
  114. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  115. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
  116. package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
  117. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  118. package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
  119. package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
  120. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  121. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  122. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  123. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  124. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  125. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  126. package/.agents/scripts/lib/orchestration/run-epilogue.js +374 -16
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
  128. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  129. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  130. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +3 -12
  131. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  132. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +72 -30
  133. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  134. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  135. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +12 -8
  136. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  137. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  138. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +264 -43
  139. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  140. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  141. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +15 -32
  142. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  143. package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
  144. package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
  145. package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
  146. package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
  147. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  148. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
  149. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
  150. package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
  151. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
  152. package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
  153. package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
  154. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  155. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  156. package/.agents/scripts/lib/planning-corpus.js +12 -286
  157. package/.agents/scripts/lib/preflight-runner.js +2 -2
  158. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  159. package/.agents/scripts/lib/signals/index.js +4 -17
  160. package/.agents/scripts/lib/signals/read.js +35 -35
  161. package/.agents/scripts/lib/signals/schema.js +8 -11
  162. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  163. package/.agents/scripts/lib/signals/write.js +0 -1
  164. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  165. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  166. package/.agents/scripts/lib/story-adjacency.js +8 -7
  167. package/.agents/scripts/lib/story-body/story-body.js +6 -5
  168. package/.agents/scripts/lib/templates/decomposer-prompts.js +7 -3
  169. package/.agents/scripts/lib/test-env.js +14 -1
  170. package/.agents/scripts/lib/test-tiers.js +0 -3
  171. package/.agents/scripts/lib/ticket-body-sections.js +0 -14
  172. package/.agents/scripts/lib/validation-evidence.js +31 -59
  173. package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
  174. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  175. package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
  176. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  177. package/.agents/scripts/plan-context.js +38 -6
  178. package/.agents/scripts/plan-persist.js +145 -35
  179. package/.agents/scripts/plan-run-epilogue.js +83 -38
  180. package/.agents/scripts/post-structured-comment.js +0 -38
  181. package/.agents/scripts/pr-watch-with-update.js +43 -22
  182. package/.agents/scripts/providers/github/compose.js +0 -1
  183. package/.agents/scripts/providers/github/errors.js +0 -19
  184. package/.agents/scripts/providers/github/issues.js +1 -11
  185. package/.agents/scripts/providers/github/mappers.js +5 -0
  186. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  187. package/.agents/scripts/providers/github/tickets.js +33 -153
  188. package/.agents/scripts/providers/github.js +17 -6
  189. package/.agents/scripts/resolve-stories.js +236 -0
  190. package/.agents/scripts/run-coverage.js +4 -1
  191. package/.agents/scripts/run-lint.js +2 -2
  192. package/.agents/scripts/run-verify.js +31 -2
  193. package/.agents/scripts/signals-view.js +9 -10
  194. package/.agents/scripts/single-story-close.js +173 -18
  195. package/.agents/scripts/single-story-confirm-merge.js +288 -15
  196. package/.agents/scripts/single-story-init.js +6 -10
  197. package/.agents/scripts/stories-wave-tick.js +79 -4
  198. package/.agents/scripts/story-plan.js +3 -3
  199. package/.agents/scripts/update-ticket-state.js +8 -50
  200. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  201. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  202. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  203. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  204. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  205. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  206. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  207. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  208. package/.agents/skills/core/idea-refinement/SKILL.md +3 -3
  209. package/.agents/skills/core/scope-triage/SKILL.md +3 -0
  210. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  211. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  212. package/.agents/skills/skills.index.json +2 -12
  213. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  214. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  215. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  216. package/.agents/workflows/audit-architecture.md +3 -4
  217. package/.agents/workflows/audit-clean-code.md +4 -4
  218. package/.agents/workflows/audit-documentation.md +4 -5
  219. package/.agents/workflows/audit-lighthouse.md +8 -0
  220. package/.agents/workflows/audit-navigability.md +10 -0
  221. package/.agents/workflows/audit-performance.md +2 -3
  222. package/.agents/workflows/audit-quality.md +8 -9
  223. package/.agents/workflows/audit-security.md +1 -2
  224. package/.agents/workflows/audit-seo.md +10 -0
  225. package/.agents/workflows/audit-ux-ui.md +7 -0
  226. package/.agents/workflows/deliver.md +98 -45
  227. package/.agents/workflows/git-cleanup.md +2 -2
  228. package/.agents/workflows/git-deliver.md +1 -1
  229. package/.agents/workflows/helpers/acceptance-self-eval.md +21 -13
  230. package/.agents/workflows/helpers/code-quality-guardrails.md +7 -7
  231. package/.agents/workflows/helpers/code-review.md +12 -10
  232. package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
  233. package/.agents/workflows/helpers/deliver-story.md +193 -118
  234. package/.agents/workflows/helpers/parallel-tooling.md +2 -2
  235. package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
  236. package/.agents/workflows/plan.md +184 -19
  237. package/.agents/workflows/qa-assist.md +6 -6
  238. package/.agents/workflows/qa-explore.md +3 -3
  239. package/.agents/workflows/qa-run.md +1 -5
  240. package/bin/mandrel.js +12 -1
  241. package/docs/CHANGELOG.md +40 -0
  242. package/lib/cli/registry.js +262 -19
  243. package/lib/cli/sync-agents.js +157 -0
  244. package/lib/cli/sync-commands.js +115 -6
  245. package/lib/cli/sync.js +168 -6
  246. package/lib/cli/update.js +105 -8
  247. package/lib/cli/version-helpers.js +131 -0
  248. package/lib/migrations/README.md +7 -5
  249. package/lib/migrations/index.js +12 -9
  250. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  251. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  252. package/package.json +1 -1
  253. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  254. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  255. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  256. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  257. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  258. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  259. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  260. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  261. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  262. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  263. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  264. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  265. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  266. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  267. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  268. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  269. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  270. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  271. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  272. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  273. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  274. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  275. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  276. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  277. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  278. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  279. package/.agents/schemas/risk-verdict.schema.json +0 -53
  280. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  281. package/.agents/scripts/analyze-execution.js +0 -444
  282. package/.agents/scripts/check-prepush-recovery.js +0 -90
  283. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  284. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  285. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -187
  286. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  287. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  288. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  289. package/.agents/scripts/lib/orchestration/audit-lens-routing.js +0 -128
  290. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
  291. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  292. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  293. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  294. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  295. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  296. package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
  297. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  298. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  299. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  300. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -21
  301. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  302. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  303. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  304. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -397
  305. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  306. package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
  307. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  308. package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
  309. package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
  310. package/.agents/scripts/resolve-plan-run.js +0 -117
  311. package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
@@ -3,15 +3,12 @@
3
3
  *
4
4
  * Tech Spec #819 §"Evidence record (Story 7)". Each successful gate run
5
5
  * writes a record keyed by `{ gateName, commitSha, commandConfigHash }` to
6
- * a per-Epic-tree path under the resolved `tempRoot`:
6
+ * a storyId-anchored path under the resolved `tempRoot`:
7
7
  *
8
- * - Epic-scoped (scopeId === epicId):
9
- * `<tempRoot>/epic-<epicId>/validation-evidence.json`
10
- * - Story-scoped (scopeId === storyId):
11
- * `<tempRoot>/epic-<epicId>/story-<storyId>/validation-evidence.json`
8
+ * `<tempRoot>/standalone/stories/story-<storyId>/validation-evidence.json`
12
9
  *
13
- * Both paths sit inside the per-Epic durable workspace (Epic #1030, Stories
14
- * #1039 + #1054) and are gitignored via `temp/`.
10
+ * The path is gitignored via `temp/`. v2.0.0 removed the Epic tier along
11
+ * with the Epic-keyed `<tempRoot>/epic-<epicId>/…` keyspace.
15
12
  *
16
13
  * A subsequent caller can `shouldSkip(...)` to learn whether the same gate
17
14
  * has already passed against the current HEAD with an identical
@@ -34,7 +31,7 @@ import path from 'node:path';
34
31
  import { fileURLToPath } from 'node:url';
35
32
  import Ajv from 'ajv/dist/2020.js';
36
33
  import addFormats from 'ajv-formats';
37
- import { epicTempDir, storyTempDir } from './config/temp-paths.js';
34
+ import { storyTempDir } from './config/temp-paths.js';
38
35
 
39
36
  export const SCHEMA_VERSION = 1;
40
37
  const DEFAULT_TEMP_DIR = 'temp';
@@ -97,37 +94,28 @@ function requirePositiveInt(value, label) {
97
94
 
98
95
  /**
99
96
  * Compute the absolute path of the evidence file for `scopeId` under the
100
- * per-Epic temp tree.
97
+ * storyId-anchored temp tree.
101
98
  *
102
- * The resolution rule mirrors `lib/config/temp-paths.js`:
103
- * - `scopeId === epicId` → `<tempRoot>/epic-<epicId>/validation-evidence.json`
104
- * - `scopeId !== epicId` → treated as a Story id → `<tempRoot>/epic-<epicId>/story-<scopeId>/validation-evidence.json`
105
- *
106
- * **Standalone keyspace (Story #4250).** When `opts.standalone === true`,
99
+ * **Standalone keyspace (Story #4250).** `opts.standalone` must be `true`:
107
100
  * the Story has no parent Epic, so the evidence file is anchored on the
108
101
  * Story id alone at
109
102
  * `<tempRoot>/standalone/stories/story-<scopeId>/validation-evidence.json`
110
- * (the `storyTempDir(null, sid)` layout from Story #2874). In this mode
111
- * `epicId` is ignored — callers MUST NOT feed a `0`/`null` epicId into the
112
- * Epic-keyed branch (the historical bug this keyspace replaces). Outside
113
- * standalone mode `epicId` remains required.
103
+ * (the `storyTempDir(null, sid)` layout from Story #2874).
114
104
  *
115
- * The legacy flat `temp/validation-evidence-<scopeId>.json` layout is no
116
- * longer supported — Epic-scoped callers must thread the Epic id through
117
- * (Epic #1030 follow-up to Story #1054). The synthetic config bag passed to
118
- * `epicTempDir` / `storyTempDir` keeps the resolver from doing a disk-bound
119
- * `.agentrc.json` lookup; bare callers can pass `tempDir` via `opts` to
120
- * override the default `'temp'`.
105
+ * The legacy flat `temp/validation-evidence-<scopeId>.json` layout and the
106
+ * Epic-keyed `<tempRoot>/epic-<epicId>/…` layout are both gone. The
107
+ * synthetic config bag passed to `storyTempDir` keeps the resolver from
108
+ * doing a disk-bound `.agentrc.json` lookup; bare callers can pass
109
+ * `tempDir` via `opts` to override the default `'temp'`.
121
110
  *
122
111
  * @param {number|string} scopeId
123
- * @param {{ cwd?: string, tempDir?: string, epicId?: number|string|null, standalone?: boolean }} opts
112
+ * @param {{ cwd?: string, tempDir?: string, standalone?: boolean }} opts
124
113
  * @returns {string}
125
114
  */
126
115
  export function evidencePath(scopeId, opts = {}) {
127
- const standalone = opts.standalone === true;
128
- if (!standalone && opts.epicId == null) {
116
+ if (opts.standalone !== true) {
129
117
  throw new Error(
130
- '[validation-evidence] evidencePath requires opts.epicId (Epic-scoped path resolution) unless opts.standalone is set.',
118
+ '[validation-evidence] evidencePath requires opts.standalone (the storyId-anchored keyspace is the only keyspace).',
131
119
  );
132
120
  }
133
121
  const { cwd, tempDir } = resolveOpts(opts);
@@ -143,18 +131,9 @@ export function evidencePath(scopeId, opts = {}) {
143
131
  ? tempDir
144
132
  : path.join(cwd, tempDir);
145
133
  const configBag = { project: { paths: { tempRoot: absTempRoot } } };
146
- let dir;
147
- if (standalone) {
148
- // Story #4250 — storyId-anchored standalone keyspace. `null` is the
149
- // standalone-story sentinel `storyTempDir` accepts (Story #2874).
150
- dir = storyTempDir(null, scope, configBag);
151
- } else {
152
- const epicId = requirePositiveInt(opts.epicId, 'epicId');
153
- dir =
154
- scope === epicId
155
- ? epicTempDir(epicId, configBag)
156
- : storyTempDir(epicId, scope, configBag);
157
- }
134
+ // Story #4250 — storyId-anchored standalone keyspace. `null` is the
135
+ // standalone-story sentinel `storyTempDir` accepts (Story #2874).
136
+ const dir = storyTempDir(null, scope, configBag);
158
137
  return path.join(dir, EVIDENCE_FILENAME);
159
138
  }
160
139
 
@@ -189,19 +168,17 @@ function emptyDoc(scopeId) {
189
168
  * cross-scopeId cases — callers don't have to branch on those failure
190
169
  * modes; they manifest as `shouldSkip()` returning `skip: false`.
191
170
  *
192
- * `opts.epicId` is required so the per-Epic-tree path can be resolved,
193
- * unless `opts.standalone === true` (Story #4250) routes to the
194
- * storyId-anchored standalone keyspace.
171
+ * `opts.standalone` is required (Story #4250) it routes to the
172
+ * storyId-anchored keyspace.
195
173
  *
196
174
  * @param {number|string} scopeId
197
- * @param {{ cwd?: string, tempDir?: string, epicId?: number|string|null, standalone?: boolean, fs?: object }} opts
175
+ * @param {{ cwd?: string, tempDir?: string, standalone?: boolean, fs?: object }} opts
198
176
  * @returns {{ storyId: number, schemaVersion: number, records: object[] }}
199
177
  */
200
178
  export function loadEvidence(scopeId, opts = {}) {
201
179
  const resolved = resolveOpts(opts);
202
180
  const file = evidencePath(scopeId, {
203
181
  ...resolved,
204
- epicId: opts.epicId,
205
182
  standalone: opts.standalone,
206
183
  });
207
184
  if (!resolved.fs.existsSync(file)) return emptyDoc(scopeId);
@@ -223,9 +200,8 @@ export function loadEvidence(scopeId, opts = {}) {
223
200
  * Validates the resulting document against the schema before writing — a
224
201
  * malformed write throws so the bug surfaces immediately.
225
202
  *
226
- * `opts.epicId` is required so the per-Epic-tree path can be resolved,
227
- * unless `opts.standalone === true` (Story #4250) routes to the
228
- * storyId-anchored standalone keyspace.
203
+ * `opts.standalone` is required (Story #4250) it routes to the
204
+ * storyId-anchored keyspace.
229
205
  *
230
206
  * @param {{
231
207
  * storyId: number|string,
@@ -235,7 +211,7 @@ export function loadEvidence(scopeId, opts = {}) {
235
211
  * exitCode?: number,
236
212
  * durationMs?: number|null,
237
213
  * }} input
238
- * @param {{ cwd?: string, tempDir?: string, epicId?: number|string|null, standalone?: boolean, fs?: object, now?: Function }} opts
214
+ * @param {{ cwd?: string, tempDir?: string, standalone?: boolean, fs?: object, now?: Function }} opts
239
215
  * @returns {object} The persisted record.
240
216
  */
241
217
  export function recordPass(
@@ -258,7 +234,6 @@ export function recordPass(
258
234
  const resolved = resolveOpts(opts);
259
235
  const evidenceOpts = {
260
236
  ...resolved,
261
- epicId: opts.epicId,
262
237
  standalone: opts.standalone,
263
238
  };
264
239
  const doc = loadEvidence(storyId, evidenceOpts);
@@ -297,13 +272,12 @@ export function recordPass(
297
272
  * with a machine-readable `reason` so callers can log why the skip didn't
298
273
  * fire.
299
274
  *
300
- * `opts.epicId` is required so the per-Epic-tree path can be resolved,
301
- * unless `opts.standalone === true` (Story #4250) routes to the
302
- * storyId-anchored standalone keyspace. `opts` is forwarded verbatim to
275
+ * `opts.standalone` is required (Story #4250) it routes to the
276
+ * storyId-anchored keyspace. `opts` is forwarded verbatim to
303
277
  * `loadEvidence`, so `standalone` flows through unchanged.
304
278
  *
305
279
  * @param {{ storyId: number|string, gateName: string, currentSha: string, configHash: string }} input
306
- * @param {{ cwd?: string, tempDir?: string, epicId?: number|string|null, standalone?: boolean, fs?: object }} opts
280
+ * @param {{ cwd?: string, tempDir?: string, standalone?: boolean, fs?: object }} opts
307
281
  * @returns {{ skip: boolean, reason: string, record?: object }}
308
282
  */
309
283
  export function shouldSkip(
@@ -340,19 +314,17 @@ export function shouldSkip(
340
314
  * start of each Story so a re-run always starts clean. Idempotent —
341
315
  * absent file is not an error.
342
316
  *
343
- * `opts.epicId` is required so the per-Epic-tree path can be resolved,
344
- * unless `opts.standalone === true` (Story #4250) routes to the
345
- * storyId-anchored standalone keyspace.
317
+ * `opts.standalone` is required (Story #4250) it routes to the
318
+ * storyId-anchored keyspace.
346
319
  *
347
320
  * @param {number|string} scopeId
348
- * @param {{ cwd?: string, tempDir?: string, epicId?: number|string|null, standalone?: boolean, fs?: object }} opts
321
+ * @param {{ cwd?: string, tempDir?: string, standalone?: boolean, fs?: object }} opts
349
322
  * @returns {{ cleared: boolean, path: string }}
350
323
  */
351
324
  export function forceClear(scopeId, opts = {}) {
352
325
  const resolved = resolveOpts(opts);
353
326
  const file = evidencePath(scopeId, {
354
327
  ...resolved,
355
- epicId: opts.epicId,
356
328
  standalone: opts.standalone,
357
329
  });
358
330
  if (!resolved.fs.existsSync(file)) return { cleared: false, path: file };
@@ -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
  });
@@ -185,8 +217,8 @@ async function main() {
185
217
  provider,
186
218
  config,
187
219
  settings,
188
- fullContext: values['full-context'],
189
220
  pretty: values.pretty,
221
+ outPath: values.out || null,
190
222
  }),
191
223
  );
192
224
  }