mandrel 1.58.0 → 1.60.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 (319) hide show
  1. package/.agents/README.md +100 -98
  2. package/.agents/docs/SDLC.md +140 -141
  3. package/.agents/docs/configuration.md +16 -16
  4. package/.agents/docs/workflows.md +7 -8
  5. package/.agents/instructions.md +12 -11
  6. package/.agents/personas/architect.md +1 -1
  7. package/.agents/personas/product.md +1 -1
  8. package/.agents/personas/project-manager.md +14 -14
  9. package/.agents/personas/technical-writer.md +1 -1
  10. package/.agents/rules/changelog-style.md +5 -5
  11. package/.agents/rules/git-conventions.md +3 -3
  12. package/.agents/schemas/agentrc.schema.json +3 -3
  13. package/.agents/schemas/audit-rules.json +20 -0
  14. package/.agents/schemas/dispatch-manifest.json +4 -4
  15. package/.agents/schemas/epic-spec.schema.json +15 -45
  16. package/.agents/schemas/lifecycle/README.md +1 -1
  17. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +1 -1
  18. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +1 -1
  19. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +1 -1
  20. package/.agents/schemas/validation-evidence.schema.json +1 -1
  21. package/.agents/scripts/README.md +1 -1
  22. package/.agents/scripts/acceptance-eval.js +21 -4
  23. package/.agents/scripts/acceptance-spec-reconciler.js +2 -2
  24. package/.agents/scripts/analyze-execution.js +2 -2
  25. package/.agents/scripts/assert-branch.js +1 -3
  26. package/.agents/scripts/audit-to-stories.js +1 -1
  27. package/.agents/scripts/bootstrap.js +1 -1
  28. package/.agents/scripts/check-arch-cycles.js +360 -0
  29. package/.agents/scripts/check-doc-links.js +2 -3
  30. package/.agents/scripts/coverage-capture.js +24 -3
  31. package/.agents/scripts/diagnose-friction.js +1 -1
  32. package/.agents/scripts/dispatcher.js +2 -2
  33. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  34. package/.agents/scripts/epic-audit-prepare.js +3 -3
  35. package/.agents/scripts/epic-deliver-note-intervention.js +2 -2
  36. package/.agents/scripts/epic-deliver-preflight.js +11 -9
  37. package/.agents/scripts/epic-deliver-prepare.js +13 -5
  38. package/.agents/scripts/epic-execute-record-wave.js +5 -5
  39. package/.agents/scripts/epic-plan-healthcheck.js +6 -10
  40. package/.agents/scripts/epic-plan-spec-validate.js +1 -1
  41. package/.agents/scripts/epic-reconcile.js +11 -29
  42. package/.agents/scripts/evidence-gate.js +2 -2
  43. package/.agents/scripts/generate-workflows-doc.js +1 -1
  44. package/.agents/scripts/git-rebase-and-resolve.js +1 -1
  45. package/.agents/scripts/hierarchy-gate.js +40 -24
  46. package/.agents/scripts/lib/ITicketingProvider.js +1 -1
  47. package/.agents/scripts/lib/audit-suite/selector.js +1 -1
  48. package/.agents/scripts/lib/audit-to-stories/seed-epic-from-findings.js +2 -2
  49. package/.agents/scripts/lib/baseline-snapshot.js +7 -7
  50. package/.agents/scripts/lib/baselines/kinds/coverage.js +33 -149
  51. package/.agents/scripts/lib/baselines/kinds/duplication.js +27 -116
  52. package/.agents/scripts/lib/baselines/kinds/kind-factory.js +192 -0
  53. package/.agents/scripts/lib/baselines/kinds/lighthouse.js +34 -133
  54. package/.agents/scripts/lib/baselines/kinds/maintainability.js +31 -124
  55. package/.agents/scripts/lib/baselines/kinds/mutation.js +25 -111
  56. package/.agents/scripts/lib/baselines/maintainability-baseline-io.js +59 -0
  57. package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +37 -0
  58. package/.agents/scripts/lib/baselines/writer.js +1 -1
  59. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  60. package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
  61. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +1 -1
  62. package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
  63. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +1 -1
  64. package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
  65. package/.agents/scripts/lib/close-validation/commands.js +188 -0
  66. package/.agents/scripts/lib/close-validation/gates.js +235 -0
  67. package/.agents/scripts/lib/close-validation/process.js +101 -0
  68. package/.agents/scripts/lib/close-validation/projections/maintainability.js +1 -1
  69. package/.agents/scripts/lib/close-validation/runner.js +325 -0
  70. package/.agents/scripts/lib/close-validation/telemetry.js +70 -0
  71. package/.agents/scripts/lib/codebase-snapshot.js +1 -1
  72. package/.agents/scripts/lib/config/explain.js +1 -1
  73. package/.agents/scripts/lib/config/quality.js +6 -6
  74. package/.agents/scripts/lib/config/runners.js +2 -2
  75. package/.agents/scripts/lib/config/runtime.js +1 -1
  76. package/.agents/scripts/lib/config/temp-paths.js +2 -2
  77. package/.agents/scripts/lib/config-resolver.js +2 -5
  78. package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
  79. package/.agents/scripts/lib/config-settings-schema-quality.js +1 -1
  80. package/.agents/scripts/lib/config-settings-schema.js +3 -3
  81. package/.agents/scripts/lib/coverage-capture.js +147 -4
  82. package/.agents/scripts/lib/cpu-pool.js +14 -0
  83. package/.agents/scripts/lib/crap-utils.js +6 -11
  84. package/.agents/scripts/lib/duplicate-search.js +1 -1
  85. package/.agents/scripts/lib/dynamic-workflow/capability.js +1 -1
  86. package/.agents/scripts/lib/dynamic-workflow/documentation-report-contract.js +87 -0
  87. package/.agents/scripts/lib/epic-plan-clarity.js +1 -1
  88. package/.agents/scripts/lib/epic-plan-ideation.js +1 -1
  89. package/.agents/scripts/lib/feedback-loop/memory-freshness.js +1 -1
  90. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
  91. package/.agents/scripts/lib/findings/classify-finding.js +1 -1
  92. package/.agents/scripts/lib/findings/promote-finding.js +10 -10
  93. package/.agents/scripts/lib/git-utils.js +24 -22
  94. package/.agents/scripts/lib/label-constants.js +3 -4
  95. package/.agents/scripts/lib/label-taxonomy.js +3 -8
  96. package/.agents/scripts/lib/maintainability-engine.js +1 -1
  97. package/.agents/scripts/lib/maintainability-utils.js +4 -187
  98. package/.agents/scripts/lib/observability/perf-report-readers.js +32 -23
  99. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +81 -7
  100. package/.agents/scripts/lib/orchestration/code-review.js +95 -82
  101. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +8 -9
  102. package/.agents/scripts/lib/orchestration/dependency-analyzer.js +3 -3
  103. package/.agents/scripts/lib/orchestration/detectors-phase.js +2 -2
  104. package/.agents/scripts/lib/orchestration/dispatch-engine.js +30 -38
  105. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +14 -37
  106. package/.agents/scripts/lib/orchestration/epic-cleanup.js +1 -1
  107. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +22 -22
  108. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +1 -1
  109. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +7 -21
  110. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +3 -3
  111. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +2 -2
  112. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +206 -58
  113. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +1 -1
  114. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +27 -3
  115. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +1 -1
  116. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +28 -8
  117. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +1 -1
  118. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +3 -3
  119. package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +4 -4
  120. package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +3 -3
  121. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +13 -41
  122. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +7 -7
  123. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +2 -3
  124. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +2 -8
  125. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +4 -4
  126. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +103 -0
  127. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +22 -64
  128. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +38 -76
  129. package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +4 -4
  130. package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +10 -10
  131. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +8 -20
  132. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +7 -15
  133. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +72 -41
  134. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +2 -4
  135. package/.agents/scripts/lib/orchestration/file-assumptions.js +6 -5
  136. package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +1 -1
  137. package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +2 -2
  138. package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +1 -1
  139. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +144 -0
  140. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +1 -1
  141. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +3 -3
  142. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +1 -1
  143. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +1 -1
  144. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +1 -1
  145. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +1 -1
  146. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +1 -1
  147. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
  148. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +1 -1
  149. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +1 -1
  150. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +8 -8
  151. package/.agents/scripts/lib/orchestration/manifest-builder.js +5 -5
  152. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +2 -2
  153. package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +5 -5
  154. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +3 -3
  155. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +3 -3
  156. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +7 -7
  157. package/.agents/scripts/lib/orchestration/preflight-cache.js +36 -13
  158. package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +1 -1
  159. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +1 -1
  160. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +2 -2
  161. package/.agents/scripts/lib/orchestration/retro-runner.js +3 -3
  162. package/.agents/scripts/lib/orchestration/review-depth.js +1 -1
  163. package/.agents/scripts/lib/orchestration/review-providers/codex.js +5 -60
  164. package/.agents/scripts/lib/orchestration/review-providers/native.js +7 -6
  165. package/.agents/scripts/lib/orchestration/review-providers/parse-findings.js +105 -0
  166. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +7 -59
  167. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +2 -4
  168. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +1 -1
  169. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +1 -1
  170. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -4
  171. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +32 -35
  172. package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +1 -2
  173. package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
  174. package/.agents/scripts/lib/orchestration/spec-renderer.js +36 -73
  175. package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
  176. package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +451 -503
  177. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +8 -2
  178. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +47 -2
  179. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +2 -2
  180. package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +1 -1
  181. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +358 -54
  182. package/.agents/scripts/lib/orchestration/story-close/phases/close.js +1 -1
  183. package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +3 -2
  184. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +32 -5
  185. package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +5 -18
  186. package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +3 -3
  187. package/.agents/scripts/lib/orchestration/story-close-recovery.js +33 -16
  188. package/.agents/scripts/lib/orchestration/story-reachability.js +47 -0
  189. package/.agents/scripts/lib/orchestration/task-body-validator.js +6 -6
  190. package/.agents/scripts/lib/orchestration/ticket-lease.js +1 -1
  191. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +4 -35
  192. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -10
  193. package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -70
  194. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +44 -73
  195. package/.agents/scripts/lib/orchestration/ticketing/reads.js +16 -7
  196. package/.agents/scripts/lib/orchestration/ticketing/state.js +53 -439
  197. package/.agents/scripts/lib/orchestration/ticketing/transition.js +471 -0
  198. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  199. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +3 -3
  200. package/.agents/scripts/lib/orchestration/wave-record-projection.js +2 -8
  201. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
  202. package/.agents/scripts/lib/preflight-runner.js +1 -1
  203. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +4 -5
  204. package/.agents/scripts/lib/presentation/manifest-builder.js +28 -34
  205. package/.agents/scripts/lib/presentation/manifest-formatter.js +3 -4
  206. package/.agents/scripts/lib/presentation/manifest-helpers.js +1 -1
  207. package/.agents/scripts/lib/presentation/manifest-procedures.js +4 -4
  208. package/.agents/scripts/lib/presentation/manifest-render-waves.js +4 -23
  209. package/.agents/scripts/lib/presentation/manifest-renderer.js +1 -1
  210. package/.agents/scripts/lib/presentation/manifest-story-views.js +2 -11
  211. package/.agents/scripts/lib/project-root.js +17 -0
  212. package/.agents/scripts/lib/signals/schema.js +1 -1
  213. package/.agents/scripts/lib/spec/index.js +1 -1
  214. package/.agents/scripts/lib/spec/loader.js +2 -2
  215. package/.agents/scripts/lib/spec/state.js +7 -16
  216. package/.agents/scripts/lib/story-adjacency.js +76 -0
  217. package/.agents/scripts/lib/story-init/context-resolver.js +3 -3
  218. package/.agents/scripts/lib/story-init/state-transitioner.js +2 -2
  219. package/.agents/scripts/lib/story-init/task-graph-builder.js +7 -7
  220. package/.agents/scripts/lib/story-lifecycle.js +9 -9
  221. package/.agents/scripts/lib/story-plan.js +1 -1
  222. package/.agents/scripts/lib/templates/decomposer-prompts.js +59 -52
  223. package/.agents/scripts/lib/transpile.js +93 -0
  224. package/.agents/scripts/lib/wave-runner/tick.js +4 -153
  225. package/.agents/scripts/lib/workers/crap-worker.js +1 -1
  226. package/.agents/scripts/lib/workers/maintainability-report-worker.js +1 -1
  227. package/.agents/scripts/lib/worktree/lifecycle/creation.js +20 -2
  228. package/.agents/scripts/lib/worktree/lifecycle/force-drain.js +90 -0
  229. package/.agents/scripts/lib/worktree/lifecycle/reap.js +26 -8
  230. package/.agents/scripts/lib/worktree/node-modules-strategy.js +74 -0
  231. package/.agents/scripts/lifecycle-emit-story-dispatch.js +1 -1
  232. package/.agents/scripts/lifecycle-emit.js +1 -1
  233. package/.agents/scripts/providers/github/board-add.js +1 -1
  234. package/.agents/scripts/providers/github/errors.js +1 -1
  235. package/.agents/scripts/providers/github/mappers.js +2 -2
  236. package/.agents/scripts/providers/github/tickets.js +114 -10
  237. package/.agents/scripts/resync-status-column.js +1 -1
  238. package/.agents/scripts/retro-run.js +2 -2
  239. package/.agents/scripts/run-lint.js +10 -1
  240. package/.agents/scripts/run-tests.js +24 -4
  241. package/.agents/scripts/single-story-init.js +1 -1
  242. package/.agents/scripts/stories-wave-tick.js +13 -10
  243. package/.agents/scripts/story-close.js +1 -1
  244. package/.agents/scripts/story-init.js +162 -26
  245. package/.agents/scripts/story-phase.js +5 -5
  246. package/.agents/scripts/story-plan.js +3 -3
  247. package/.agents/scripts/sync-branch-from-base.js +2 -2
  248. package/.agents/scripts/validate-docs-freshness.js +1 -1
  249. package/.agents/scripts/wave-tick.js +1 -1
  250. package/.agents/skills/core/analyze-execution/SKILL.md +2 -2
  251. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +21 -26
  252. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -56
  253. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +4 -4
  254. package/.agents/skills/core/hydrate-context/SKILL.md +2 -2
  255. package/.agents/skills/core/idea-refinement/SKILL.md +4 -4
  256. package/.agents/skills/core/knowledge-transfer/SKILL.md +2 -2
  257. package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +1 -1
  258. package/.agents/skills/core/scope-triage/SKILL.md +9 -10
  259. package/.agents/skills/core/using-agent-skills/SKILL.md +1 -1
  260. package/.agents/skills/skills.index.json +7 -7
  261. package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +1 -1
  262. package/.agents/templates/agent-protocol.md +2 -2
  263. package/.agents/workflows/agents-update.md +2 -2
  264. package/.agents/workflows/audit-architecture.md +2 -2
  265. package/.agents/workflows/audit-clean-code.md +2 -2
  266. package/.agents/workflows/audit-dependencies.md +1 -1
  267. package/.agents/workflows/audit-devops.md +1 -1
  268. package/.agents/workflows/audit-documentation.md +226 -0
  269. package/.agents/workflows/audit-lighthouse.md +1 -1
  270. package/.agents/workflows/audit-performance.md +2 -2
  271. package/.agents/workflows/audit-privacy.md +1 -1
  272. package/.agents/workflows/audit-quality.md +2 -2
  273. package/.agents/workflows/audit-security.md +2 -2
  274. package/.agents/workflows/audit-seo.md +1 -1
  275. package/.agents/workflows/audit-sre.md +1 -1
  276. package/.agents/workflows/audit-to-stories.md +10 -10
  277. package/.agents/workflows/audit-ux-ui.md +1 -1
  278. package/.agents/workflows/deliver.md +85 -0
  279. package/.agents/workflows/explain.md +3 -3
  280. package/.agents/workflows/git-merge-pr.md +1 -1
  281. package/.agents/workflows/git-pr-all.md +13 -10
  282. package/.agents/workflows/git-push.md +6 -3
  283. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  284. package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
  285. package/.agents/workflows/helpers/code-review.md +5 -5
  286. package/.agents/workflows/{epic-deliver.md → helpers/deliver-epic.md} +59 -66
  287. package/.agents/workflows/{story-deliver.md → helpers/deliver-stories.md} +25 -25
  288. package/.agents/workflows/helpers/diagnose.md +1 -1
  289. package/.agents/workflows/helpers/epic-audit.md +6 -6
  290. package/.agents/workflows/helpers/epic-deliver-story.md +28 -39
  291. package/.agents/workflows/helpers/epic-plan-decompose.md +23 -23
  292. package/.agents/workflows/helpers/epic-plan-spec.md +6 -6
  293. package/.agents/workflows/helpers/epic-testing.md +3 -3
  294. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  295. package/.agents/workflows/{epic-plan.md → helpers/plan-epic.md} +84 -84
  296. package/.agents/workflows/{story-plan.md → helpers/plan-story.md} +43 -43
  297. package/.agents/workflows/helpers/signals.md +1 -1
  298. package/.agents/workflows/helpers/single-story-deliver.md +12 -11
  299. package/.agents/workflows/helpers/worktree-lifecycle.md +18 -18
  300. package/.agents/workflows/onboard.md +21 -20
  301. package/.agents/workflows/plan.md +89 -0
  302. package/.agents/workflows/qa-explore.md +1 -1
  303. package/.agents/workflows/qa-run-harness.md +1 -1
  304. package/README.md +17 -20
  305. package/docs/CHANGELOG.md +1149 -0
  306. package/lib/cli/__tests__/update-changelog-surface.test.js +357 -0
  307. package/lib/cli/__tests__/update-reexec.test.js +513 -0
  308. package/lib/cli/init.js +338 -0
  309. package/lib/cli/update.js +413 -52
  310. package/package.json +3 -1
  311. package/.agents/scripts/lib/auto-refresh-baselines.js +0 -308
  312. package/.agents/scripts/lib/close-validation.js +0 -897
  313. package/.agents/scripts/lib/orchestration/cascade-grouping.js +0 -275
  314. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter.js +0 -69
  315. package/.agents/scripts/lib/orchestration/reconciler.js +0 -137
  316. package/.agents/scripts/lib/orchestration/story-close/format-autofix-scoped.js +0 -221
  317. package/.agents/scripts/lib/orchestration/story-close/format-autofix-shared.js +0 -123
  318. package/.agents/scripts/lib/task-utils.js +0 -26
  319. package/.agents/scripts/story-deliver-prepare.js +0 -267
@@ -118,10 +118,89 @@ export function findHoldersInPath(wtPath, opts = {}) {
118
118
  }));
119
119
  }
120
120
 
121
+ /**
122
+ * Pure: compute the set of pids that must never be killed — `selfPid` plus
123
+ * its full ancestor chain — from a process table of `{ pid, ppid }` rows.
124
+ * Cycle-guarded (a corrupt/raced table cannot loop forever). `selfPid` is
125
+ * always included even when the table is empty or missing its row, so the
126
+ * guard fails safe.
127
+ *
128
+ * @param {number} selfPid
129
+ * @param {Array<{ pid: number, ppid?: number }>} table
130
+ * @returns {Set<number>}
131
+ */
132
+ export function computeProtectedPids(selfPid, table) {
133
+ const protectedPids = new Set([selfPid]);
134
+ if (!Array.isArray(table) || table.length === 0) return protectedPids;
135
+ const parentOf = new Map();
136
+ for (const row of table) {
137
+ if (row && typeof row.pid === 'number' && typeof row.ppid === 'number') {
138
+ parentOf.set(row.pid, row.ppid);
139
+ }
140
+ }
141
+ let cursor = selfPid;
142
+ while (parentOf.has(cursor)) {
143
+ const ppid = parentOf.get(cursor);
144
+ if (protectedPids.has(ppid)) break; // cycle guard
145
+ protectedPids.add(ppid);
146
+ cursor = ppid;
147
+ }
148
+ return protectedPids;
149
+ }
150
+
151
+ /**
152
+ * Enumerate the full Windows process table as `{ pid, ppid }` rows so the
153
+ * kill set can exclude the invoking shell / orchestrator ancestry.
154
+ * Best-effort: any failure returns `[]` (callers still protect `selfPid`).
155
+ *
156
+ * @param {object} [opts]
157
+ * @param {Function} [opts.spawn] Injection point for tests (default `spawnSync`).
158
+ * @param {string} [opts.platform] Override `process.platform` for tests.
159
+ * @returns {Array<{ pid: number, ppid: number }>}
160
+ */
161
+ export function fetchProcessTable(opts = {}) {
162
+ const spawn = opts.spawn ?? spawnSync;
163
+ const platform = opts.platform ?? process.platform;
164
+ if (platform !== 'win32') return [];
165
+
166
+ const script =
167
+ 'Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId | ConvertTo-Json -Compress';
168
+ let res;
169
+ try {
170
+ res = spawn(
171
+ 'powershell.exe',
172
+ ['-NoProfile', '-NonInteractive', '-Command', script],
173
+ { encoding: 'utf8', timeout: 15_000 },
174
+ );
175
+ } catch {
176
+ return [];
177
+ }
178
+ if (!res || res.status !== 0 || !res.stdout) return [];
179
+ let parsed;
180
+ try {
181
+ parsed = JSON.parse(String(res.stdout).trim());
182
+ } catch {
183
+ return [];
184
+ }
185
+ const list = Array.isArray(parsed) ? parsed : [parsed];
186
+ return list
187
+ .filter((p) => p && typeof p.ProcessId === 'number')
188
+ .map((p) => ({
189
+ pid: p.ProcessId,
190
+ ppid: typeof p.ParentProcessId === 'number' ? p.ParentProcessId : -1,
191
+ }));
192
+ }
193
+
121
194
  /**
122
195
  * `taskkill /T /F /PID <pid>` for each holder. Returns the pids reported
123
196
  * as terminated. Per-pid failures are logged but do not throw — caller
124
197
  * decides whether the partial kill is enough to retry.
198
+ *
199
+ * Self-preservation (Story #4018): holders are matched by command-line
200
+ * substring, which can select the invoking shell or the orchestrator's own
201
+ * ancestor chain (any ancestor whose command line mentions the worktree
202
+ * path). Before any `taskkill /T /F`, the kill set excludes `selfPid` and
203
+ * its full ancestor chain — `/T` on an ancestor would kill this process too.
125
204
  */
126
205
  export function terminateHolders(holders, opts = {}) {
127
206
  const spawn = opts.spawn ?? spawnSync;
@@ -130,9 +209,20 @@ export function terminateHolders(holders, opts = {}) {
130
209
  if (platform !== 'win32') return [];
131
210
  if (!Array.isArray(holders) || holders.length === 0) return [];
132
211
 
212
+ const selfPid = opts.selfPid ?? process.pid;
213
+ const protectedPids =
214
+ opts.protectedPids ??
215
+ computeProtectedPids(selfPid, fetchProcessTable({ spawn, platform }));
216
+
133
217
  const killed = [];
134
218
  for (const h of holders) {
135
219
  if (!h || typeof h.pid !== 'number') continue;
220
+ if (protectedPids.has(h.pid)) {
221
+ logger.warn(
222
+ `force-drain: skipping pid=${h.pid} name=${h.name ?? '?'} — self/ancestor of this process (never killed)`,
223
+ );
224
+ continue;
225
+ }
136
226
  let res;
137
227
  try {
138
228
  res = spawn('taskkill.exe', ['/T', '/F', '/PID', String(h.pid)], {
@@ -134,7 +134,7 @@ async function fsRmWithRetry(
134
134
  return { success: true, attempts: attempt };
135
135
  } catch (err) {
136
136
  lastErr = err;
137
- if (attempt < maxRetries) {
137
+ if (attempt < maxRetries && retryDelay > 0) {
138
138
  await new Promise((r) => setTimeout(r, retryDelay));
139
139
  }
140
140
  }
@@ -160,8 +160,8 @@ function handleRemoveFailure(
160
160
  classification,
161
161
  attempt,
162
162
  maxAttempts,
163
+ { retryDelaysMs = [0, 150, 350, 700, 1200, 2000], sleepFn = sleepSync } = {},
163
164
  ) {
164
- const retryDelaysMs = [0, 150, 350, 700, 1200, 2000];
165
165
  const { isLockLike, isCwdLike } = classification;
166
166
  if ((isLockLike || isCwdLike) && attempt < maxAttempts) {
167
167
  const delay = retryDelaysMs[attempt] ?? 300;
@@ -169,13 +169,13 @@ function handleRemoveFailure(
169
169
  ctx.logger.warn(
170
170
  `worktree.reap remove hit ${reasonClass} error; retrying in ${delay}ms (${attempt}/${maxAttempts})`,
171
171
  );
172
- sleepSync(delay);
172
+ sleepFn(delay);
173
173
  return 'continue';
174
174
  }
175
175
  return 'break';
176
176
  }
177
177
 
178
- async function runGitWorktreeRemoveLoop(ctx, wtPath) {
178
+ async function runGitWorktreeRemoveLoop(ctx, wtPath, retryOpts = {}) {
179
179
  const maxAttempts = ctx.platform === 'win32' ? 6 : 2;
180
180
  let lastReason = 'worktree-remove-failed';
181
181
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -193,6 +193,7 @@ async function runGitWorktreeRemoveLoop(ctx, wtPath) {
193
193
  classification,
194
194
  attempt,
195
195
  maxAttempts,
196
+ retryOpts,
196
197
  );
197
198
  if (action === 'break') break;
198
199
  }
@@ -205,6 +206,7 @@ function tryForceRemoveFallback(
205
206
  lastReason,
206
207
  forceRemoveBackoffMs,
207
208
  maxAttempts,
209
+ sleepFn = sleepSync,
208
210
  ) {
209
211
  if (!(WINDOWS_LOCK_RE.test(lastReason) || WINDOWS_CWD_RE.test(lastReason))) {
210
212
  return { handled: false, lastReason };
@@ -212,7 +214,7 @@ function tryForceRemoveFallback(
212
214
  ctx.logger.warn(
213
215
  `worktree.reap remove exhausted Windows lock retry; retrying with --force in ${forceRemoveBackoffMs}ms path=${wtPath}`,
214
216
  );
215
- sleepSync(forceRemoveBackoffMs);
217
+ sleepFn(forceRemoveBackoffMs);
216
218
  const forced = ctx.git.gitSpawn(
217
219
  ctx.repoRoot,
218
220
  'worktree',
@@ -248,8 +250,9 @@ async function tryStage15WindowsFsRm({
248
250
  push,
249
251
  lastReason,
250
252
  priorAttempts,
253
+ sleepFn = sleepSync,
251
254
  }) {
252
- sleepSync(forceRemoveBackoffMs);
255
+ sleepFn(forceRemoveBackoffMs);
253
256
  try {
254
257
  await fsRm(wtPath, {
255
258
  recursive: true,
@@ -304,6 +307,7 @@ async function handleFsRmFailure({
304
307
  lastReason,
305
308
  forceRemoveBackoffMs,
306
309
  fsRm,
310
+ sleepFn,
307
311
  }) {
308
312
  // Stage 1.5 — coverage-leak quiesce + extended fs.rm budget (Windows only).
309
313
  if (ctx.platform === 'win32') {
@@ -316,6 +320,7 @@ async function handleFsRmFailure({
316
320
  push,
317
321
  lastReason,
318
322
  priorAttempts: rmResult.attempts,
323
+ sleepFn,
319
324
  });
320
325
  if (stage15) return stage15;
321
326
  }
@@ -347,7 +352,12 @@ async function handleFsRmFailure({
347
352
  export async function removeWorktreeWithRecovery(ctx, wtPath, opts = {}) {
348
353
  const { storyId = null, branch = null, push = false } = opts;
349
354
  const forceRemoveBackoffMs = opts.forceRemoveBackoffMs ?? 3000;
350
- const removeLoop = await runGitWorktreeRemoveLoop(ctx, wtPath);
355
+ const retryDelay = opts.retryDelay ?? 200;
356
+ const { retryDelaysMs, sleepFn } = opts;
357
+ const removeLoop = await runGitWorktreeRemoveLoop(ctx, wtPath, {
358
+ ...(retryDelaysMs ? { retryDelaysMs } : {}),
359
+ ...(sleepFn ? { sleepFn } : {}),
360
+ });
351
361
  if (removeLoop.removed) return { removed: true };
352
362
  let { lastReason } = removeLoop;
353
363
  if (ctx.platform === 'win32' && opts.forceRemoveFallback !== false) {
@@ -357,6 +367,7 @@ export async function removeWorktreeWithRecovery(ctx, wtPath, opts = {}) {
357
367
  lastReason,
358
368
  forceRemoveBackoffMs,
359
369
  removeLoop.maxAttempts,
370
+ sleepFn,
360
371
  );
361
372
  if (fallback.handled) return fallback.result;
362
373
  lastReason = fallback.lastReason;
@@ -367,7 +378,7 @@ export async function removeWorktreeWithRecovery(ctx, wtPath, opts = {}) {
367
378
  const fsRm = ctx.fsRm ?? fsPromisesRm;
368
379
  const rmResult = await fsRmWithRetry(fsRm, wtPath, {
369
380
  maxRetries: 5,
370
- retryDelay: 200,
381
+ retryDelay,
371
382
  });
372
383
  if (!rmResult.success) {
373
384
  return handleFsRmFailure({
@@ -380,6 +391,7 @@ export async function removeWorktreeWithRecovery(ctx, wtPath, opts = {}) {
380
391
  lastReason,
381
392
  forceRemoveBackoffMs,
382
393
  fsRm,
394
+ sleepFn,
383
395
  });
384
396
  }
385
397
  finalizeGitWorktreeRemove(ctx);
@@ -571,6 +583,12 @@ export async function reap(ctx, storyId, opts = {}) {
571
583
  storyId: storyIdN,
572
584
  branch,
573
585
  push: opts.push === true,
586
+ ...(opts.retryDelaysMs ? { retryDelaysMs: opts.retryDelaysMs } : {}),
587
+ ...(opts.retryDelay !== undefined ? { retryDelay: opts.retryDelay } : {}),
588
+ ...(opts.sleepFn ? { sleepFn: opts.sleepFn } : {}),
589
+ ...(opts.forceRemoveBackoffMs !== undefined
590
+ ? { forceRemoveBackoffMs: opts.forceRemoveBackoffMs }
591
+ : {}),
574
592
  });
575
593
  if (!removeResult.removed) {
576
594
  return {
@@ -119,6 +119,80 @@ export function selectInstallCommand(strategy, wtPath, fsLike = fs) {
119
119
  return { cmd: 'npm', args: ['ci'] };
120
120
  }
121
121
 
122
+ /**
123
+ * Per-package-manager "install completed" marker files written into
124
+ * `node_modules/` by the install command itself. Their presence (and
125
+ * freshness relative to the lockfile) is the cheapest reliable signal that a
126
+ * prior install ran to completion — a failed/interrupted install leaves
127
+ * `node_modules` partially populated without (or with a stale) marker.
128
+ */
129
+ const INSTALL_MARKERS = [
130
+ '.package-lock.json', // npm ci / npm install
131
+ '.modules.yaml', // pnpm
132
+ '.yarn-state.yml', // yarn berry (node-modules linker)
133
+ '.yarn-integrity', // yarn classic
134
+ ];
135
+
136
+ const LOCKFILES = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'];
137
+
138
+ function safeMtimeMs(fsLike, p) {
139
+ try {
140
+ return fsLike.statSync(p).mtimeMs;
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Pure: probe whether a **reused** worktree already carries a completed,
148
+ * up-to-date install. Worktree reuse must not blindly report
149
+ * `skipped/worktree-reused` — when the prior run's install *failed*, that
150
+ * status defeats the install retry exactly when it matters
151
+ * (`deriveInstallAction('skipped')` treats it as "nothing to do").
152
+ *
153
+ * Returns the same shape as `installDependencies`:
154
+ * - `{ status: 'skipped', reason: 'worktree-reused' }` — a completed
155
+ * install was detected (or the strategy never installs per-tree);
156
+ * safe to skip.
157
+ * - `{ status: 'failed', reason }` — missing/incomplete/stale install
158
+ * detected; callers should retry the install.
159
+ *
160
+ * @param {string} strategy One of `per-worktree | pnpm-store | symlink`.
161
+ * @param {string} wtPath Absolute worktree path.
162
+ * @param {{ existsSync: Function, statSync: Function }} [fsLike] Injectable for tests.
163
+ * @returns {{ status: 'skipped' | 'failed', reason: string }}
164
+ */
165
+ export function probeReusedInstall(strategy, wtPath, fsLike = fs) {
166
+ // `symlink` re-points node_modules at a donor — no per-tree install to probe.
167
+ if (strategy === 'symlink') {
168
+ return { status: 'skipped', reason: 'worktree-reused' };
169
+ }
170
+ if (!fsLike.existsSync(path.join(wtPath, 'package.json'))) {
171
+ return { status: 'skipped', reason: 'no-package-json' };
172
+ }
173
+ const nmPath = path.join(wtPath, 'node_modules');
174
+ if (!fsLike.existsSync(nmPath)) {
175
+ return { status: 'failed', reason: 'reuse-node-modules-missing' };
176
+ }
177
+ const marker = INSTALL_MARKERS.map((m) => path.join(nmPath, m)).find((p) =>
178
+ fsLike.existsSync(p),
179
+ );
180
+ if (!marker) {
181
+ return { status: 'failed', reason: 'reuse-install-incomplete' };
182
+ }
183
+ const markerMtime = safeMtimeMs(fsLike, marker);
184
+ const lockfile = LOCKFILES.map((l) => path.join(wtPath, l)).find((p) =>
185
+ fsLike.existsSync(p),
186
+ );
187
+ if (lockfile && markerMtime !== null) {
188
+ const lockMtime = safeMtimeMs(fsLike, lockfile);
189
+ if (lockMtime !== null && lockMtime > markerMtime) {
190
+ return { status: 'failed', reason: 'reuse-node-modules-stale' };
191
+ }
192
+ }
193
+ return { status: 'skipped', reason: 'worktree-reused' };
194
+ }
195
+
122
196
  /** Pure: retry policy keyed off the chosen command. pnpm gets 3× + 5min. */
123
197
  export function installRetryPolicy(cmd) {
124
198
  const isPnpm = cmd === 'pnpm';
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Thin host-loop CLI that appends a single `story.dispatch.start`
7
7
  * NDJSON record to `temp/epic-<id>/lifecycle.ndjson`. Invoked by
8
- * `/epic-deliver` Phase 2 immediately BEFORE each per-Story Agent
8
+ * `/deliver` Phase 2 immediately BEFORE each per-Story Agent
9
9
  * tool call so the lifecycle ledger durably records every dispatch
10
10
  * attempt. The `wave-tick.js` reconciler (Story #2891 Task #2901)
11
11
  * then derives `nextAction['in-flight']` from this ledger to surface
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Replaces the three single-purpose emit shims
8
8
  * (`epic-deliver-finalize.js`, `epic-deliver-automerge.js`,
9
- * `epic-deliver-cleanup.js`) that the `/epic-deliver` workflow markdown
9
+ * `epic-deliver-cleanup.js`) that the `/deliver` workflow markdown
10
10
  * invoked in Phase 6, 7.5, and 8. Those shims each did exactly one
11
11
  * thing: construct a bus and emit one event. Collapsing them into a
12
12
  * single argv-driven CLI lets the workflow stay declarative ("fire
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Story #3822 — single source of truth for the post-create board-add
5
5
  * step. Issues created through any create path (`createTicket`,
6
- * `createIssue` — which backs `/story-plan` persist and the `/epic-plan`
6
+ * `createIssue` — which backs `/plan` persist and the `/plan`
7
7
  * Phase 4 Epic open) must land on the configured Projects V2 board
8
8
  * without relying on GitHub's "Auto-add to project" built-in workflow,
9
9
  * which is off by default on fresh boards and cannot be enabled via API.
@@ -109,7 +109,7 @@ export function classifyGithubError(err) {
109
109
  // callers (paginateRest, getTicket, getNativeSubIssues, …) absorb the same
110
110
  // jittered exponential backoff on transient GitHub errors instead of
111
111
  // bubbling a one-shot 502/429/ECONNRESET that kills a longer pipeline
112
- // (e.g. the /epic-deliver Phase E retro).
112
+ // (e.g. the /deliver Phase E retro).
113
113
 
114
114
  export const TRANSIENT_RETRY_DEFAULTS = Object.freeze({
115
115
  maxAttempts: 6,
@@ -57,7 +57,7 @@ export function issueToEpic(issue) {
57
57
  export function subIssueNodeToTicket(node) {
58
58
  // Story #3097 (Wave-0 additive, Epic #3078 Strategy B) — return `null`
59
59
  // for absent sub-issue nodes instead of dereferencing properties on
60
- // `null`/`undefined`. In 3-tier mode a Story can legitimately have zero
60
+ // `null`/`undefined`. In 2-tier mode a Story can legitimately have zero
61
61
  // Task children, which surfaces as an empty / missing sub-issue node
62
62
  // when callers iterate the GraphQL response and pass each entry through
63
63
  // this mapper. The legacy 4-tier path also benefits — a transient
@@ -83,7 +83,7 @@ export function subIssueNodeToTicket(node) {
83
83
  * any null/undefined entries. Story #3097 (Wave-0 additive, Epic #3078
84
84
  * Strategy B) — gives callers a single Storyless-tolerant entry point so
85
85
  * the existing per-node mappers can stay strict for the 4-tier path while
86
- * the 3-tier path (Storyless: a Story with zero child Tasks) gets a
86
+ * the 2-tier path (Storyless: a Story with zero child Tasks) gets a
87
87
  * well-defined empty-array result.
88
88
  *
89
89
  * @param {Array<object|null|undefined>|null|undefined} nodes
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import { parseBlockedBy, parseBlocks } from '../../lib/dependency-parser.js';
25
+ import { Logger } from '../../lib/Logger.js';
25
26
  import { addIssueToBoard } from './board-add.js';
26
27
  import { createInlineTicketCache } from './cache.js';
27
28
  import { withTransientRetry } from './errors.js';
@@ -33,8 +34,16 @@ import {
33
34
  } from './request-helpers.js';
34
35
 
35
36
  /**
36
- * Compose the final markdown body for a created ticket. Under the 3-tier
37
- * hierarchy (Epic Feature Story), `body` is always a string supplied
37
+ * GitHub Search API hard ceiling is 1000 results per query; at
38
+ * `per_page=100` that is 10 pages. An Epic never has anywhere near 1000
39
+ * children, so hitting this cap means the query is degenerate — we stop
40
+ * rather than throw (the regex post-filter keeps results correct).
41
+ */
42
+ const SEARCH_PAGE_CAP = 10;
43
+
44
+ /**
45
+ * Compose the final markdown body for a created ticket. Under the 2-tier
46
+ * hierarchy (Epic → Story), `body` is always a string supplied
38
47
  * by the caller (the decomposer, the spec planner, or the reconciler-apply
39
48
  * engine). This helper appends the canonical orchestrator footer
40
49
  * (`parent: #<n>` / `Epic: #<m>` / `blocked by #<x>`) byte-stable with the
@@ -100,6 +109,14 @@ export class TicketGateway {
100
109
  this.repo = repo;
101
110
  this._hooks = hooks;
102
111
  this._cache = cache ?? createInlineTicketCache();
112
+ /**
113
+ * Per-instance memo of `getTickets(epicId, filters)` results (Story
114
+ * #3988). The planning-state-manager fetches the same child list twice
115
+ * per planning pass; without this memo each fetch re-pays the full
116
+ * search/list round-trip. Invalidated on every write surface.
117
+ * @type {Map<string, object[]>}
118
+ */
119
+ this._listCache = new Map();
103
120
  }
104
121
 
105
122
  /**
@@ -142,29 +159,113 @@ export class TicketGateway {
142
159
  }
143
160
 
144
161
  /**
145
- * @field-manifest /repos/{owner}/{repo}/issues?state=...&labels=...:
146
- * number, body, labels, state, pull_request
162
+ * Run one Search API query (`/search/issues`) to completion, returning
163
+ * the raw issue items. Search responses are `{ total_count, items }`
164
+ * envelopes rather than bare arrays, so this paginates manually instead
165
+ * of going through `paginateRest`.
166
+ */
167
+ async _searchIssues(query) {
168
+ const items = [];
169
+ for (let page = 1; page <= SEARCH_PAGE_CAP; page++) {
170
+ const params = new URLSearchParams({
171
+ q: query,
172
+ per_page: '100',
173
+ page: String(page),
174
+ });
175
+ const result = await withTransientRetry(
176
+ () =>
177
+ this._gh.api({
178
+ method: 'GET',
179
+ endpoint: `/search/issues?${params}`,
180
+ }),
181
+ { label: `searchIssues page ${page}`, onRetry: defaultRetryWarn },
182
+ );
183
+ const parsed = parseApiJson(result);
184
+ const batch = Array.isArray(parsed?.items) ? parsed.items : [];
185
+ items.push(...batch);
186
+ if (batch.length < 100) break;
187
+ }
188
+ return items;
189
+ }
190
+
191
+ /**
192
+ * Server-side narrowed child lookup (Story #3988): two Search API
193
+ * queries — `"Epic: #N" in:body` and `"parent: #N" in:body` — deduped
194
+ * by issue number. Replaces the repo-wide `state=all` pagination that
195
+ * cost ~1 spawn per 100 repo issues and hard-failed past the
196
+ * `paginateRest` page cap. Search tokenization can over-match (e.g.
197
+ * `#10` vs `#100`), so callers MUST keep the word-boundary regex
198
+ * post-filter.
199
+ */
200
+ async _searchEpicChildren(epicId, filters) {
201
+ const qualifiers = [`repo:${this.owner}/${this.repo}`, 'is:issue'];
202
+ const state = filters.state ?? 'all';
203
+ if (state === 'open' || state === 'closed') {
204
+ qualifiers.push(`state:${state}`);
205
+ }
206
+ if (filters.label) qualifiers.push(`label:"${filters.label}"`);
207
+ const base = qualifiers.join(' ');
208
+
209
+ const [epicRefs, parentRefs] = await Promise.all([
210
+ this._searchIssues(`${base} "Epic: #${epicId}" in:body`),
211
+ this._searchIssues(`${base} "parent: #${epicId}" in:body`),
212
+ ]);
213
+
214
+ const byNumber = new Map();
215
+ for (const issue of [...epicRefs, ...parentRefs]) {
216
+ if (!byNumber.has(issue.number)) byNumber.set(issue.number, issue);
217
+ }
218
+ return Array.from(byNumber.values());
219
+ }
220
+
221
+ /**
222
+ * Repo-wide listing fallback — the pre-#3988 shape. Only used when the
223
+ * Search API path fails (search outage, search-specific rate limit).
147
224
  */
148
225
  /* node:coverage ignore next */
149
- async getTickets(epicId, filters = {}) {
226
+ async _listAllIssues(filters) {
150
227
  const params = new URLSearchParams({ state: filters.state ?? 'all' });
151
228
  if (filters.label) params.set('labels', filters.label);
152
-
153
229
  const endpoint = `/repos/${this.owner}/${this.repo}/issues?${params}`;
154
- const issues = await paginateRest(this._gh, endpoint);
230
+ return paginateRest(this._gh, endpoint);
231
+ }
232
+
233
+ /**
234
+ * @field-manifest /search/issues?q=...: number, id, node_id, title,
235
+ * body, labels, state, pull_request
236
+ * @field-manifest /repos/{owner}/{repo}/issues?state=...&labels=...:
237
+ * number, body, labels, state, pull_request
238
+ */
239
+ async getTickets(epicId, filters = {}) {
240
+ const memoKey = `${epicId}|${filters.state ?? 'all'}|${filters.label ?? ''}`;
241
+ if (this._listCache.has(memoKey)) return this._listCache.get(memoKey);
242
+
243
+ let issues;
244
+ try {
245
+ issues = await this._searchEpicChildren(epicId, filters);
246
+ } catch (err) {
247
+ const msg = typeof err?.message === 'string' ? err.message : String(err);
248
+ Logger.warn(
249
+ `[TicketGateway] search-based getTickets(#${epicId}) failed (${msg}); ` +
250
+ 'falling back to repo-wide issue listing',
251
+ );
252
+ issues = await this._listAllIssues(filters);
253
+ }
155
254
 
156
255
  // Word-boundary regex prevents #1 matching #10, #100, etc.
157
256
  const epicRefRe = new RegExp(
158
257
  `(?:Epic:\\s*#${epicId}|parent:\\s*#${epicId})(?:\\s|$|[,.)\\]])`,
159
258
  );
160
259
 
161
- return issues
260
+ const tickets = issues
162
261
  .filter((issue) => {
163
262
  if (issue.pull_request) return false;
164
263
  const body = issue.body ?? '';
165
264
  return epicRefRe.test(body);
166
265
  })
167
266
  .map(issueToListItem);
267
+ this._listCache.set(memoKey, tickets);
268
+ return tickets;
168
269
  }
169
270
 
170
271
  /* node:coverage ignore next */
@@ -187,6 +288,7 @@ export class TicketGateway {
187
288
 
188
289
  invalidateTicket(ticketId) {
189
290
  this._cache.invalidate(ticketId);
291
+ this._listCache.clear();
190
292
  }
191
293
 
192
294
  // ---------------------------------------------------------------------------
@@ -226,6 +328,7 @@ export class TicketGateway {
226
328
  },
227
329
  });
228
330
  const issue = parseApiJson(result);
331
+ this._listCache.clear();
229
332
 
230
333
  let subIssueLinked = false;
231
334
  let subIssueError = null;
@@ -258,8 +361,8 @@ export class TicketGateway {
258
361
  /**
259
362
  * Create a **bare** issue — no `parent: #N` footer composition and no
260
363
  * sub-issue link. Serves the standalone create paths that bypass
261
- * `createTicket`'s Story-shaped body rendering: the `/story-plan`
262
- * persist step and the `/epic-plan` Phase 4 Epic open
364
+ * `createTicket`'s Story-shaped body rendering: the `/plan`
365
+ * persist step and the `/plan` Phase 4 Epic open
263
366
  * (`openEpicFromOnePager`'s `createIssue` port).
264
367
  *
265
368
  * After the POST, the new issue is added to the configured Project V2
@@ -288,6 +391,7 @@ export class TicketGateway {
288
391
  body: { title, body, labels },
289
392
  });
290
393
  const issue = parseApiJson(result);
394
+ this._listCache.clear();
291
395
 
292
396
  const boardAdd = await addIssueToBoard({
293
397
  nodeId: issue.node_id,
@@ -5,7 +5,7 @@
5
5
  * resync-status-column.js — re-assert the GitHub Projects v2 Status
6
6
  * column for a ticket after auto-merge has fired (Story #2845).
7
7
  *
8
- * The `/single-story-deliver` and `/story-deliver` workflow docs call
8
+ * The `/single-story-deliver` and `/deliver` workflow docs call
9
9
  * this CLI after Step 5 confirms `state: "MERGED"` so the orchestrator
10
10
  * wins the race against the GitHub built-in `Pull request merged`
11
11
  * workflow, which would otherwise overwrite Status to whatever value
@@ -2,9 +2,9 @@
2
2
  /* node:coverage ignore file */
3
3
 
4
4
  /**
5
- * retro-run.js — execute the `/epic-deliver` Phase 6 retro from a CLI.
5
+ * retro-run.js — execute the `/deliver` Phase 6 retro from a CLI.
6
6
  *
7
- * Phase 6 of `/epic-deliver` posts the Epic retro by invoking the
7
+ * Phase 6 of `/deliver` posts the Epic retro by invoking the
8
8
  * in-process retro module (`lib/orchestration/retro-runner.js`'s
9
9
  * `runRetro`). That module is a library entry point — it has no CLI
10
10
  * wrapper and hard-requires both a GitHub `provider` and a lifecycle
@@ -64,11 +64,20 @@ const tasks = [
64
64
  // only the canonical `<axis>::<value>` separator from
65
65
  // `lib/label-constants.js` appears. Closes the drift gap that
66
66
  // let the original `type/epic` typo land at
67
- // `.agents/workflows/epic-plan.md:49`.
67
+ // `.agents/workflows/helpers/plan-epic.md:49`.
68
68
  name: 'label-vocabulary',
69
69
  cmd: 'node',
70
70
  args: ['.agents/scripts/lint-label-vocabulary.js'],
71
71
  },
72
+ {
73
+ // Architecture cycle ratchet (Story #3991). Detects directed import
74
+ // cycles under `.agents/scripts/` and fails on any cycle not in the
75
+ // committed allowlist (`baselines/arch-cycles.json`). Mirrors the
76
+ // ratchet-down semantics of `check-dead-exports.js`.
77
+ name: 'arch-cycles',
78
+ cmd: 'node',
79
+ args: ['.agents/scripts/check-arch-cycles.js'],
80
+ },
72
81
  ];
73
82
 
74
83
  function runTask({ name, cmd, args }) {
@@ -84,11 +84,31 @@ export const TEST_RUNNER_FLAGS = Object.freeze([
84
84
  * quoting). Budgeting the targets to 8 000 chars keeps every spawn's full
85
85
  * command line at roughly a quarter of the ceiling — ample headroom for the
86
86
  * exe path and any pass-through `--test-name-pattern` args — while keeping
87
- * the chunk count (and thus the extra `node` start-ups) low. POSIX limits are
88
- * far higher, so this only ever splits work that would otherwise fail on
89
- * Windows.
87
+ * the chunk count (and thus the extra `node` start-ups) low.
88
+ *
89
+ * `MAX_TARGET_CHARS` is the **Windows** budget. On POSIX hosts `ARG_MAX` is
90
+ * far higher (~256 KB on macOS, ~1 MB on Linux), so applying the Windows
91
+ * budget there needlessly serializes the quick tier into several sequential
92
+ * `node --test` spawns — each chunk pays a fresh runner start-up, and cores
93
+ * idle at every chunk's tail. `POSIX_MAX_TARGET_CHARS` (100 000) lets the
94
+ * whole quick-tier target list (~33 000 chars today) collapse into a single
95
+ * spawn on POSIX while staying far below `ARG_MAX`.
96
+ * `resolveMaxTargetChars` picks the budget per platform; the Windows
97
+ * semantics of `MAX_TARGET_CHARS` are unchanged.
90
98
  */
91
99
  export const MAX_TARGET_CHARS = 8000;
100
+ export const POSIX_MAX_TARGET_CHARS = 100_000;
101
+
102
+ /**
103
+ * Resolve the per-spawn target-character budget for the host platform.
104
+ * The `platform` parameter is injected in tests.
105
+ *
106
+ * @param {NodeJS.Platform} [platform]
107
+ * @returns {number}
108
+ */
109
+ export function resolveMaxTargetChars(platform = process.platform) {
110
+ return platform === 'win32' ? MAX_TARGET_CHARS : POSIX_MAX_TARGET_CHARS;
111
+ }
92
112
 
93
113
  /**
94
114
  * Partition an ordered list of test-file targets into chunks whose joined
@@ -144,7 +164,7 @@ export function runTestSuite({
144
164
  spawn = spawnSync,
145
165
  cleanup = cleanupRepoTestTempArtifacts,
146
166
  listTargets = listTestFilesForTier,
147
- maxTargetChars = MAX_TARGET_CHARS,
167
+ maxTargetChars = resolveMaxTargetChars(),
148
168
  } = {}) {
149
169
  const { tier, rest } = parseTierArgv(argv);
150
170
  const targets = listTargets(tier, cwd);