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
@@ -1,20 +1,22 @@
1
1
  /**
2
2
  * plan-critics-evaluate.js — shared critic-dispatch evaluation for the
3
- * collapsed /plan flow (#4496 fix 6; extracted from the `plan-critics.js`
4
- * CLI so the persist surface folds the same evaluation in as a pre-write
5
- * phase).
3
+ * collapsed /plan flow (#4496 fix 6).
6
4
  *
7
- * Two consumers:
8
- * - `plan-persist.js` (via `runPlanPersist`) evaluates the dispatch
9
- * conditions as a deterministic pre-write phase, prints the verdicts,
10
- * and records every skip on the plan-metrics ledger, so the headless
11
- * path never pays a standalone CLI turn for the same decision.
12
- * - `plan-critics.js` the standalone CLI survives one release as a
13
- * thin shim over this module for the attended pre-gate evaluation
14
- * (the verdict folds into gate #2's view before the persist runs).
5
+ * One consumer: the `plan-critics.js` CLI, which `/plan` runs between its
6
+ * Author and Persist steps. The CLI loads the draft artifacts, calls this
7
+ * module, prints the verdict as JSON, and records every skip on the
8
+ * plan-metrics ledger; the workflow dispatches a fresh-context critic
9
+ * sub-agent on a `dispatch: true` verdict and folds the findings into a
10
+ * re-author round before persist.
11
+ *
12
+ * Story #4592 moved that evaluation here from `run-plan-persist.js`, which
13
+ * ran it after authoring was finished and immediately before
14
+ * `createStoryIssues` — the one point where a `dispatch: true` verdict has
15
+ * no re-author loop to route to. Persist no longer evaluates critics; this
16
+ * module has exactly one evaluation point.
15
17
  *
16
18
  * Pure evaluation: no file I/O, no GitHub calls, no ledger writes — the
17
- * callers own artifact loading and skip recording.
19
+ * caller owns artifact loading and skip recording.
18
20
  *
19
21
  * @module lib/orchestration/plan-critics-evaluate
20
22
  */
@@ -24,6 +26,7 @@ import {
24
26
  evaluateConsolidationDispatch,
25
27
  evaluatePremortemDispatch,
26
28
  } from './plan-critic-conditions.js';
29
+ import { evaluateTextHygiene } from './plan-text-hygiene.js';
27
30
 
28
31
  /**
29
32
  * Resolve the planning risk heuristics list from the canonical config
@@ -41,30 +44,33 @@ function resolveRiskHeuristics(config = {}) {
41
44
 
42
45
  /**
43
46
  * Evaluate the consolidation + pre-mortem critic dispatch conditions over
44
- * the authored planning artifacts (design §4 / #4474 PR6 conditions,
45
- * unchanged):
47
+ * the authored planning artifacts (#4474 PR6 conditions, unchanged):
46
48
  *
47
49
  * - Consolidation: skipped outright when `tickets` is null/absent (the
48
50
  * single-delivery shape authors no draft tickets); otherwise the
49
51
  * deterministic precondition + size/divergence conditions.
50
- * - Pre-mortem: risk verdict overall level high, OR ticket count at least
51
- * half `maxTickets`, OR any `planning.riskHeuristics` phrase matching
52
- * the plan text.
52
+ * - Pre-mortem: ticket count at least half `maxTickets`, OR any
53
+ * `planning.riskHeuristics` phrase matching the plan text. Story #4542
54
+ * retired its authored-risk-level condition with the verdict itself.
55
+ * - Text hygiene (Story #4599, advisory-only): deterministic body lints
56
+ * (dangling-citation / open-question / slicing-mass) over the draft
57
+ * stories. It has no `dispatch` semantics and spawns nothing — its
58
+ * `findings[]` are re-author-round input, and the consolidation /
59
+ * premortem dispatch verdicts are untouched by it.
53
60
  *
54
61
  * @param {{
55
62
  * techSpecContent: string,
56
- * riskVerdict: { summary?: string },
57
63
  * tickets?: Array<object>|null,
58
64
  * config?: object,
59
65
  * }} args
60
66
  * @returns {{
61
67
  * consolidation: { critic: string, dispatch: boolean, reasons: string[] },
62
68
  * premortem: { critic: string, dispatch: boolean, reasons: string[] },
69
+ * textHygiene: { critic: string, findings: Array<object> },
63
70
  * }}
64
71
  */
65
72
  export function evaluatePlanCritics({
66
73
  techSpecContent,
67
- riskVerdict,
68
74
  tickets = null,
69
75
  config = {},
70
76
  }) {
@@ -84,16 +90,19 @@ export function evaluatePlanCritics({
84
90
  });
85
91
 
86
92
  const premortem = evaluatePremortemDispatch({
87
- riskVerdict,
88
93
  ticketCount: ticketList?.length ?? 0,
89
94
  maxTickets: getLimits(config).maxTickets,
90
95
  riskHeuristics: resolveRiskHeuristics(config),
91
96
  planText: [
92
97
  techSpecContent ?? '',
93
98
  ticketList ? JSON.stringify(ticketList) : '',
94
- riskVerdict?.summary ?? '',
95
99
  ].join('\n'),
96
100
  });
97
101
 
98
- return { consolidation, premortem };
102
+ const textHygiene = {
103
+ critic: 'text-hygiene',
104
+ findings: evaluateTextHygiene({ draftStories: ticketList }).findings,
105
+ };
106
+
107
+ return { consolidation, premortem, textHygiene };
99
108
  }
@@ -5,7 +5,7 @@
5
5
  * phase is removed, every plan CLI invocation stamps an entry/exit record so
6
6
  * the current 12-phase baseline is captured on disk. Each record is one
7
7
  * newline-terminated JSON line appended to
8
- * `temp/epic-<id>/plan-metrics.json` (per-Epic plan CLIs) or
8
+ * `temp/run-<id>/plan-metrics.json` (per-Epic plan CLIs) or
9
9
  * `temp/standalone/plan-metrics.json` (the standalone `story-plan.js` path
10
10
  * and Epic-less healthcheck runs — same standalone routing the friction
11
11
  * ledger uses).
@@ -61,7 +61,7 @@ import path from 'node:path';
61
61
 
62
62
  import {
63
63
  anchorTempRoot,
64
- epicArtifactPath,
64
+ runArtifactPath,
65
65
  tempRootFrom,
66
66
  } from '../config/temp-paths.js';
67
67
  import { Logger } from '../Logger.js';
@@ -95,7 +95,7 @@ export function planMetricsPath(epicId, config) {
95
95
  PLAN_METRICS_BASENAME,
96
96
  );
97
97
  }
98
- return epicArtifactPath(epicId, PLAN_METRICS_BASENAME, config);
98
+ return runArtifactPath(epicId, PLAN_METRICS_BASENAME, config);
99
99
  }
100
100
 
101
101
  /**
@@ -314,7 +314,8 @@ export async function readPlanMetrics(epicId, config) {
314
314
 
315
315
  /**
316
316
  * Roll a read ledger up into the compact summary surfaced by the persist
317
- * summary and `analyze-execution.js`. Returns `null` when there is nothing
317
+ * summary. (Story #4545 deleted its second consumer, `analyze-execution.js`,
318
+ * with the execution-analysis surface.) Returns `null` when there is nothing
318
319
  * to summarize (missing ledger or zero parseable entries).
319
320
  *
320
321
  * Critic-skip records (kind: 'critic-skip') are counted separately from
@@ -322,7 +323,15 @@ export async function readPlanMetrics(epicId, config) {
322
323
  * them down, so the skip-audit trail is visible in the persist summary
323
324
  * without inflating the turns-per-plan proxy.
324
325
  *
326
+ * Pass `opts.since` (an ISO-8601 instant) to scope the roll-up to one plan
327
+ * run (Story #4541). The Epic-less ledger at `temp/standalone/` is shared by
328
+ * every plan the repo has ever run, so an unfiltered summary reported
329
+ * lifetime totals under a line the reader takes to describe the invocation
330
+ * in front of them. Records are timestamped `startedAt` (invocations) or
331
+ * `at` (critic skips); either at-or-after `since` is in scope.
332
+ *
325
333
  * @param {{ entries: object[], malformedLines?: number }} ledger
334
+ * @param {{ since?: string|null }} [opts]
326
335
  * @returns {{
327
336
  * invocations: number,
328
337
  * failures: number,
@@ -337,8 +346,31 @@ export async function readPlanMetrics(epicId, config) {
337
346
  * malformedLines: number,
338
347
  * }|null}
339
348
  */
340
- export function summarizePlanMetrics(ledger) {
341
- const entries = ledger?.entries ?? [];
349
+ /**
350
+ * Timestamp a ledger record is ordered by: `startedAt` for invocation
351
+ * records, `at` for critic-skip records.
352
+ *
353
+ * @param {object} entry
354
+ * @returns {string|null}
355
+ */
356
+ function recordTimestamp(entry) {
357
+ const stamp =
358
+ entry?.kind === PLAN_METRICS_KIND_CRITIC_SKIP ? entry.at : entry.startedAt;
359
+ return typeof stamp === 'string' ? stamp : null;
360
+ }
361
+
362
+ export function summarizePlanMetrics(ledger, opts = {}) {
363
+ const all = ledger?.entries ?? [];
364
+ const since = typeof opts.since === 'string' ? opts.since : null;
365
+ // ISO-8601 UTC strings sort lexicographically in time order, so a string
366
+ // compare is a correct (and allocation-free) instant compare here.
367
+ const entries =
368
+ since === null
369
+ ? all
370
+ : all.filter((e) => {
371
+ const stamp = recordTimestamp(e);
372
+ return stamp !== null && stamp >= since;
373
+ });
342
374
  if (entries.length === 0) return null;
343
375
  const byCli = {};
344
376
  const byMode = {};
@@ -6,7 +6,11 @@
6
6
  */
7
7
 
8
8
  import { Logger } from '../../Logger.js';
9
- import { renderHardConflictError } from '../ticket-validator-conflicts.js';
9
+ import {
10
+ renderFanOutEvidence,
11
+ renderFanOutRemedy,
12
+ renderHardConflictError,
13
+ } from '../ticket-validator-conflicts.js';
10
14
 
11
15
  /**
12
16
  * @param {object[]} findings
@@ -24,8 +28,9 @@ export function enforceFanOutGate(
24
28
  for (const f of fanOut) {
25
29
  Logger.warn(
26
30
  `[${tag}] Persisting a large-fan-out deletion: ` +
27
- `Task "${f.taskSlug}" deletes "${f.path}" with ${f.callSiteCount} ` +
28
- `call site(s) (threshold ${f.threshold}). Operator override --allow-large-fan-out.`,
31
+ `Story "${f.storySlug}" deletes "${f.path}" with ${f.callSiteCount} ` +
32
+ `importer(s) (threshold ${f.threshold}). Operator override --allow-large-fan-out.` +
33
+ renderFanOutEvidence(f),
29
34
  );
30
35
  }
31
36
  return;
@@ -33,13 +38,18 @@ export function enforceFanOutGate(
33
38
  const lines = fanOut
34
39
  .map(
35
40
  (f) =>
36
- ` - Task "${f.taskSlug}" (Story "${f.storySlug}") deletes "${f.path}" — ${f.callSiteCount} call site(s) (threshold ${f.threshold})`,
41
+ ` - Story "${f.storySlug}" deletes "${f.path}" — ` +
42
+ `${f.callSiteCount} importer(s) (threshold ${f.threshold})` +
43
+ renderFanOutEvidence(f),
37
44
  )
38
45
  .join('\n');
46
+ // Each finding carries its own remedy — a rename-shaped deletion has no
47
+ // subsystems to split across, so a blanket "split it up" footer would be
48
+ // wrong advice for it (Story #4547).
49
+ const remedies = [...new Set(fanOut.map((f) => renderFanOutRemedy(f)))];
39
50
  throw new Error(
40
51
  `[${tag}] ${fanOut.length} Task(s) declare large-fan-out deletions:\n${lines}\n\n` +
41
- `Split each deletion into a subsystem-by-subsystem migration across multiple Stories, ` +
42
- `or rerun --allow-large-fan-out after confirming the deletion is intentional.`,
52
+ remedies.join('\n\n'),
43
53
  );
44
54
  }
45
55
 
@@ -2,16 +2,19 @@
2
2
  * persist-helpers.js — pure helper surface for the flat Story `/plan` persist.
3
3
  *
4
4
  * Exports:
5
+ * - `resolveBaseBranchRef(config)` — the one place the persist gates learn
6
+ * which ref to probe.
5
7
  * - `validateTickets(tickets, config)` — runs the cross-link, model-capacity,
6
8
  * freshness, and task-body validators in one pass. Capacity settings are
7
9
  * explicit inputs so the validator and decomposer share one live delivery
8
10
  * envelope instead of silently falling back to framework defaults.
9
- * - `makeDefaultFanOutCounter({ baseBranchRef, cwd })` — production
11
+ * - `makeDefaultFanOutCounter({ baseBranchRef, cwd, git })` — production
10
12
  * fan-out probe used by the conflict policy.
11
13
  *
12
14
  * @module lib/orchestration/plan-persist/persist-helpers
13
15
  */
14
16
 
17
+ import posix from 'node:path/posix';
15
18
  import { resolveListValue } from '../../config/shared.js';
16
19
  import { gitSpawn } from '../../git-utils.js';
17
20
  import { validateTaskBodies } from '../task-body-validator.js';
@@ -19,32 +22,156 @@ import { validateAndNormalizeTickets } from '../ticket-validator.js';
19
22
  import { DEFAULT_REGISTRY_PATTERNS } from '../ticket-validator-conflicts.js';
20
23
 
21
24
  /**
22
- * Default fan-out counter counts distinct files at `baseBranchRef` that
23
- * reference the basename (without extension) of the deleted path. Uses
24
- * `git grep -l` for a streaming-friendly probe; an empty grep returns
25
- * exit code 1 which we map to a count of 0.
25
+ * Extensions an import specifier may elide. Probed longest-path-first when
26
+ * resolving an extensionless specifier back onto a concrete repo path.
27
+ */
28
+ const RESOLVABLE_EXTENSIONS = ['', '.js', '.mjs', '.cjs', '.jsx', '.json'];
29
+
30
+ /** Every quoted string on a candidate line — the specifier lives in one. */
31
+ const QUOTED_RE = /['"]([^'"\n]+)['"]/g;
32
+
33
+ function escapeRegExp(value) {
34
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
35
+ }
36
+
37
+ /**
38
+ * The specifier tails an importer of `path` could plausibly write as the
39
+ * final segment of its quoted specifier: the basename, the basename minus
40
+ * its extension, and — for a directory-index module — the directory name
41
+ * (`./foo` resolving to `foo/index.js`).
42
+ *
43
+ * This is a *candidate* net only. It is deliberately generous because the
44
+ * resolution pass below re-checks every hit against the real path; a tail
45
+ * that over-matches costs one extra resolve, a tail that under-matches
46
+ * loses a genuine importer.
47
+ */
48
+ function specifierTails(path) {
49
+ const base = posix.basename(path);
50
+ const ext = posix.extname(base);
51
+ const stem = ext ? base.slice(0, -ext.length) : base;
52
+ const tails = new Set([base, stem]);
53
+ if (stem === 'index') {
54
+ const dir = posix.basename(posix.dirname(path));
55
+ if (dir && dir !== '.') tails.add(dir);
56
+ }
57
+ return [...tails].filter((t) => t.length > 0);
58
+ }
59
+
60
+ /**
61
+ * ERE matching a line that carries an `import` / `export … from` /
62
+ * `require(` / dynamic `import(` whose quoted specifier *ends* at one of
63
+ * `tails`. The `(^|/)`-equivalent guard (`([^'"]*\/)?`) is what keeps
64
+ * `notification.js` from matching `push-notification.js`.
65
+ */
66
+ function buildProbePattern(tails) {
67
+ const alt = tails.map(escapeRegExp).join('|');
68
+ return `(from|require|import)[[:space:]]*\\(?[[:space:]]*['"]([^'"]*/)?(${alt})['"]`;
69
+ }
70
+
71
+ /**
72
+ * Resolve a relative specifier written in `importerPath` back onto a repo
73
+ * path and report whether it names `deletedPath`.
74
+ *
75
+ * **Known boundary:** only relative (`./`, `../`) specifiers resolve. A
76
+ * consumer repo that imports its own modules through bare specifiers or a
77
+ * path alias (`#lib/x`, `@app/x`, a `tsconfig` `paths` entry) would
78
+ * under-count, because resolving those needs the resolver config this probe
79
+ * deliberately does not read. Mandrel's own internal imports are all
80
+ * relative. Under-counting is the *quiet* failure direction — it argues for
81
+ * a deletion rather than against one — so if alias-importing consumers
82
+ * appear, this is the place to teach the probe their resolver.
83
+ */
84
+ function specifierResolvesTo(importerPath, specifier, deletedPath) {
85
+ if (!specifier.startsWith('./') && !specifier.startsWith('../')) return false;
86
+ const resolved = posix.normalize(
87
+ posix.join(posix.dirname(importerPath), specifier),
88
+ );
89
+ for (const ext of RESOLVABLE_EXTENSIONS) {
90
+ if (`${resolved}${ext}` === deletedPath) return true;
91
+ if (ext && `${resolved}/index${ext}` === deletedPath) return true;
92
+ }
93
+ return false;
94
+ }
95
+
96
+ /**
97
+ * Quote one argv entry so the reported probe is **runnable as emitted**.
26
98
  *
27
- * Story #2962. Injected via opts in tests; this default runs in production.
99
+ * The probe is the operator's route to checking the number, so it has to
100
+ * survive a paste into a shell. Unquoted, the ERE's `(`, `|`, `[[:space:]]`
101
+ * and `?` are glob/grouping metacharacters: zsh fails the paste with
102
+ * `no matches found` *and exits 0*, which reads as "zero importers" — the
103
+ * gate's own audit trail would then argue for the deletion it is meant to
104
+ * question (Story #4547).
28
105
  */
29
- export function makeDefaultFanOutCounter({ baseBranchRef, cwd }) {
106
+ function shellQuote(value) {
107
+ if (/^[A-Za-z0-9_./-]+$/.test(value)) return value;
108
+ return `'${value.replaceAll("'", "'\\''")}'`;
109
+ }
110
+
111
+ /**
112
+ * Parse one `git grep -n` output line of the form `<ref>:<path>:<lineno>:<text>`.
113
+ */
114
+ function parseGrepLine(line, baseBranchRef) {
115
+ const prefix = `${baseBranchRef}:`;
116
+ if (!line.startsWith(prefix)) return null;
117
+ const rest = line.slice(prefix.length);
118
+ const pathEnd = rest.indexOf(':');
119
+ if (pathEnd === -1) return null;
120
+ const path = rest.slice(0, pathEnd);
121
+ const afterPath = rest.slice(pathEnd + 1);
122
+ const lineEnd = afterPath.indexOf(':');
123
+ if (lineEnd === -1) return null;
124
+ return { path, text: afterPath.slice(lineEnd + 1) };
125
+ }
126
+
127
+ /**
128
+ * Default fan-out probe — resolves the *importers* of the deleted module at
129
+ * `baseBranchRef`, and reports the referencing files alongside the exact
130
+ * probe that produced them.
131
+ *
132
+ * Two-stage, because accuracy and cost pull in opposite directions:
133
+ *
134
+ * 1. `git grep -n -E` narrows the tree to lines whose quoted import /
135
+ * require specifier could name the module (final-segment match).
136
+ * 2. Each candidate specifier is resolved against its own importer's
137
+ * directory and compared to the deleted path. Only a real resolution
138
+ * counts.
139
+ *
140
+ * The predecessor (Story #2962) grepped the basename stem as a bare word
141
+ * across the whole tree, so a module named `notification` or `options`
142
+ * reported dozens of call sites drawn from prose, schemas, and unrelated
143
+ * modules — and the gate that fired on that number told the operator to
144
+ * split a migration that did not exist. It also returned 0 without probing
145
+ * for any stem under three characters, under-reporting in silence. Both are
146
+ * gone: coupling is measured by resolution, not vocabulary (Story #4547).
147
+ *
148
+ * @returns {(arg: { path: string }) => { count: number, files: string[], probe: string }}
149
+ */
150
+ export function makeDefaultFanOutCounter({ baseBranchRef, cwd, git } = {}) {
151
+ const spawn = git?.gitSpawn ?? gitSpawn;
30
152
  return ({ path }) => {
31
- const lastSlash = path.lastIndexOf('/');
32
- const base = lastSlash === -1 ? path : path.slice(lastSlash + 1);
33
- const dotIdx = base.lastIndexOf('.');
34
- const stem = dotIdx > 0 ? base.slice(0, dotIdx) : base;
35
- if (stem.length < 3) return 0;
36
- const result = gitSpawn(
37
- cwd ?? process.cwd(),
38
- 'grep',
39
- '-l',
40
- '--fixed-strings',
41
- stem,
42
- baseBranchRef,
43
- );
44
- if (result.status !== 0) return 0;
45
- const lines = result.stdout.split('\n').filter((l) => l.trim().length > 0);
46
- // Exclude the deleted file itself from the call-site count.
47
- return lines.filter((l) => !l.endsWith(`:${path}`)).length;
153
+ const tails = specifierTails(path);
154
+ const pattern = buildProbePattern(tails);
155
+ const args = ['grep', '-n', '-E', '--full-name', pattern, baseBranchRef];
156
+ const probe = `git ${args.map(shellQuote).join(' ')}`;
157
+ const result = spawn(cwd ?? process.cwd(), ...args);
158
+ // git grep exits 1 on "no matches" — an empty result, not a failure.
159
+ if (result.status !== 0) return { count: 0, files: [], probe };
160
+ const files = new Set();
161
+ for (const line of result.stdout.split('\n')) {
162
+ if (line.trim().length === 0) continue;
163
+ const hit = parseGrepLine(line, baseBranchRef);
164
+ // The deleted module's own self-references are not call sites.
165
+ if (!hit || hit.path === path) continue;
166
+ for (const match of hit.text.matchAll(QUOTED_RE)) {
167
+ if (specifierResolvesTo(hit.path, match[1], path)) {
168
+ files.add(hit.path);
169
+ break;
170
+ }
171
+ }
172
+ }
173
+ const sorted = [...files].sort();
174
+ return { count: sorted.length, files: sorted, probe };
48
175
  };
49
176
  }
50
177
 
@@ -72,8 +199,29 @@ function resolveConflictPolicy(cfg) {
72
199
  return policy;
73
200
  }
74
201
 
202
+ /**
203
+ * Resolve the ref the persist gates probe against.
204
+ *
205
+ * The canonical resolved config carries the base branch at
206
+ * `project.baseBranch` (`lib/config-resolver.js` defaults it to `main`).
207
+ * This helper used to read `config.baseBranch` — a key the resolver never
208
+ * produces — so every freshness / file-assumption / fan-out probe silently
209
+ * targeted the literal `main` regardless of configuration. Benign in a repo
210
+ * whose base branch *is* `main`; wrong for any consumer that configured
211
+ * something else (Story #4541).
212
+ *
213
+ * The flat `config.baseBranch` fallback is retained for the legacy
214
+ * `settings`-bag callers that pass `{ baseBranch, paths, planning }`.
215
+ *
216
+ * @param {object} [config] Resolved config, or a legacy settings bag.
217
+ * @returns {string}
218
+ */
219
+ export function resolveBaseBranchRef(config) {
220
+ return config?.project?.baseBranch ?? config?.baseBranch ?? 'main';
221
+ }
222
+
75
223
  export function validateTickets(tickets, config, opts = {}) {
76
- const baseBranchRef = config?.baseBranch ?? 'main';
224
+ const baseBranchRef = resolveBaseBranchRef(config);
77
225
  const conflictPolicy = resolveConflictPolicy(config);
78
226
  if (typeof opts.fanOutCounter === 'function') {
79
227
  conflictPolicy.fanOutCounter = opts.fanOutCounter;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * plan-context-source.js — locate and read the `plan-context.js` envelope so
3
+ * persist can derive the `/plan --tickets` source ids from the run that
4
+ * actually fetched them (Story #4554).
5
+ *
6
+ * This lives beside the persist ops rather than inside `plan-persist.js` so
7
+ * the discovery + failure policy is directly testable: the CLI is a thin
8
+ * `parseArgs` shell, and the interesting behaviour here is exactly the part
9
+ * that decides whether a `--tickets` run can quietly lose its source set.
10
+ *
11
+ * @module lib/orchestration/plan-persist/plan-context-source
12
+ */
13
+
14
+ import { readFile } from 'node:fs/promises';
15
+ import path from 'node:path';
16
+
17
+ import { Logger } from '../../Logger.js';
18
+
19
+ /**
20
+ * Filename the `/plan` interrogate step writes its envelope to inside
21
+ * `--plan-dir` (`plan-context.js --out <plan-dir>/plan-context.json`).
22
+ */
23
+ export const PLAN_CONTEXT_FILENAME = 'plan-context.json';
24
+
25
+ /**
26
+ * Decide where to look for the envelope.
27
+ *
28
+ * An explicit `--plan-context` path wins; otherwise the conventional file
29
+ * inside `--plan-dir`. With neither, there is nothing to read — the caller
30
+ * gets `null` and must warn (see `loadPlanContextEnvelope`).
31
+ *
32
+ * @param {string|null|undefined} explicitPath `--plan-context`.
33
+ * @param {string|null|undefined} planDir `--plan-dir`.
34
+ * @returns {{ path: string, explicit: boolean }|null}
35
+ */
36
+ export function resolvePlanContextPath(explicitPath, planDir) {
37
+ if (explicitPath) {
38
+ return { path: path.resolve(explicitPath), explicit: true };
39
+ }
40
+ if (planDir) {
41
+ return {
42
+ path: path.join(path.resolve(planDir), PLAN_CONTEXT_FILENAME),
43
+ explicit: false,
44
+ };
45
+ }
46
+ return null;
47
+ }
48
+
49
+ /**
50
+ * The advice printed whenever persist has no envelope to derive ids from.
51
+ * Single-homed so the two no-envelope paths cannot drift apart.
52
+ */
53
+ const CAPTURE_HINT =
54
+ 'Re-run step 1 with `node .agents/scripts/plan-context.js … --out ' +
55
+ '<plan-dir>/plan-context.json` and pass --plan-dir, or pass ' +
56
+ '--source-tickets explicitly.';
57
+
58
+ /**
59
+ * Read the `plan-context.js` envelope.
60
+ *
61
+ * Failure policy — the point is that a `--tickets` run can never *quietly*
62
+ * lose its source set, so every no-envelope path is audible:
63
+ *
64
+ * - **No path at all** (neither `--plan-dir` nor `--plan-context`): warn.
65
+ * Persist cannot tell a legitimate `--seed` run from a `--tickets` run
66
+ * whose envelope was never captured, so it says so rather than returning a
67
+ * silent `null`.
68
+ * - **Explicit `--plan-context` missing**: throw. The operator named a file
69
+ * and meant it.
70
+ * - **Auto-discovered file simply absent**: warn and degrade to
71
+ * `--source-tickets`. A `--seed` run legitimately has no envelope, so
72
+ * absence alone is not fatal.
73
+ * - **Present but unparseable**: throw either way. A corrupt envelope is not
74
+ * the same as no envelope, and reading it as "no source tickets" is exactly
75
+ * the vacuous pass this module exists to prevent.
76
+ *
77
+ * @param {{ path: string, explicit: boolean }|null} planContext
78
+ * @returns {Promise<object|null>} Parsed envelope, or null when absent.
79
+ */
80
+ export async function loadPlanContextEnvelope(planContext) {
81
+ if (!planContext) {
82
+ Logger.warn(
83
+ '[plan-persist] no --plan-dir or --plan-context given, so no ' +
84
+ 'plan-context envelope was read. If this was a `/plan --tickets` ' +
85
+ 'run, its source tickets can only come from --source-tickets and ' +
86
+ `will NOT be closed otherwise. ${CAPTURE_HINT}`,
87
+ );
88
+ return null;
89
+ }
90
+
91
+ let raw;
92
+ try {
93
+ raw = await readFile(planContext.path, 'utf8');
94
+ } catch (err) {
95
+ if (err?.code === 'ENOENT' && !planContext.explicit) {
96
+ Logger.warn(
97
+ `[plan-persist] no plan-context envelope at ${planContext.path} — ` +
98
+ 'source tickets can only come from --source-tickets. ' +
99
+ CAPTURE_HINT,
100
+ );
101
+ return null;
102
+ }
103
+ throw new Error(
104
+ `Cannot read plan-context envelope ${planContext.path}: ${err.message}`,
105
+ );
106
+ }
107
+
108
+ try {
109
+ return JSON.parse(raw);
110
+ } catch (err) {
111
+ throw new Error(
112
+ `Failed to parse plan-context envelope "${planContext.path}" as JSON: ` +
113
+ `${err.message}. ${CAPTURE_HINT}`,
114
+ );
115
+ }
116
+ }