mandrel 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/.agents/README.md +59 -28
  2. package/.agents/agents/acceptance-critic.md +9 -7
  3. package/.agents/agents/story-worker.md +41 -46
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +51 -44
  6. package/.agents/docs/agentrc-reference.json +8 -13
  7. package/.agents/docs/configuration.md +32 -56
  8. package/.agents/docs/execution-reference.md +39 -10
  9. package/.agents/docs/quality-gates.md +14 -16
  10. package/.agents/docs/workflows.md +6 -6
  11. package/.agents/instructions.md +64 -79
  12. package/.agents/rules/ci-remediation.md +3 -3
  13. package/.agents/rules/git-conventions-reference.md +42 -51
  14. package/.agents/schemas/agentrc.schema.json +34 -45
  15. package/.agents/schemas/audit-rules.json +59 -1
  16. package/.agents/schemas/audit-rules.schema.json +33 -1
  17. package/.agents/schemas/lifecycle/README.md +1 -2
  18. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  19. package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
  20. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
  21. package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
  22. package/.agents/schemas/signal-event.schema.json +3 -3
  23. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  24. package/.agents/schemas/validation-evidence.schema.json +1 -1
  25. package/.agents/scripts/acceptance-eval.js +22 -66
  26. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  27. package/.agents/scripts/bootstrap.js +3 -3
  28. package/.agents/scripts/check-dead-exports.js +43 -104
  29. package/.agents/scripts/check-doc-links.js +2 -2
  30. package/.agents/scripts/check-lifecycle-lint.js +1 -1
  31. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  32. package/.agents/scripts/deliver-recover.js +122 -0
  33. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  34. package/.agents/scripts/evidence-gate.js +20 -50
  35. package/.agents/scripts/generate-skills-index.js +17 -1
  36. package/.agents/scripts/generate-workflows-doc.js +4 -4
  37. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  38. package/.agents/scripts/lib/audit-suite/selector.js +323 -23
  39. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
  40. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  41. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  42. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  43. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  44. package/.agents/scripts/lib/checks/core-bare-clean.js +4 -1
  45. package/.agents/scripts/lib/checks/index.js +1 -1
  46. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  47. package/.agents/scripts/lib/checks/state.js +17 -248
  48. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +3 -3
  49. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  50. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  51. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  52. package/.agents/scripts/lib/cli-args.js +23 -2
  53. package/.agents/scripts/lib/close-validation/gates.js +13 -13
  54. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  55. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  56. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  57. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  58. package/.agents/scripts/lib/config/delivery-routing.js +7 -6
  59. package/.agents/scripts/lib/config/explain.js +10 -16
  60. package/.agents/scripts/lib/config/github.js +7 -5
  61. package/.agents/scripts/lib/config/limits.js +15 -25
  62. package/.agents/scripts/lib/config/quality.js +11 -14
  63. package/.agents/scripts/lib/config/runners.js +8 -21
  64. package/.agents/scripts/lib/config/temp-paths.js +18 -56
  65. package/.agents/scripts/lib/config-settings-schema-delivery.js +31 -13
  66. package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
  67. package/.agents/scripts/lib/config-settings-schema.js +48 -22
  68. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  69. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  70. package/.agents/scripts/lib/duplicate-search.js +38 -7
  71. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  72. package/.agents/scripts/lib/format-generated-json.js +97 -0
  73. package/.agents/scripts/lib/framework-version.js +19 -189
  74. package/.agents/scripts/lib/gh-exec.js +8 -0
  75. package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
  76. package/.agents/scripts/lib/git-utils.js +0 -14
  77. package/.agents/scripts/lib/json-utils.js +1 -2
  78. package/.agents/scripts/lib/label-constants.js +0 -15
  79. package/.agents/scripts/lib/label-taxonomy.js +1 -12
  80. package/.agents/scripts/lib/observability/active-story-env.js +42 -163
  81. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  82. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  83. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  84. package/.agents/scripts/lib/observability/source-classifier.js +2 -2
  85. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  86. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  87. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  88. package/.agents/scripts/lib/orchestration/ceremony-routing.js +43 -45
  89. package/.agents/scripts/lib/orchestration/code-review.js +58 -168
  90. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
  91. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  92. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  93. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  94. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  95. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  96. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +9 -11
  97. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  98. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  99. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +7 -3
  100. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  101. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  102. package/.agents/scripts/lib/orchestration/merge-block-class.js +76 -20
  103. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  104. package/.agents/scripts/lib/orchestration/plan-context.js +114 -24
  105. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +11 -22
  106. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +3 -7
  107. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  108. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
  109. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
  110. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  111. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -75
  112. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
  113. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
  114. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  115. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
  116. package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
  117. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  118. package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
  119. package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
  120. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  121. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  122. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  123. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  124. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  125. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  126. package/.agents/scripts/lib/orchestration/run-epilogue.js +374 -16
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
  128. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  129. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  130. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +3 -12
  131. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  132. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +72 -30
  133. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  134. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  135. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +12 -8
  136. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  137. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  138. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +264 -43
  139. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  140. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  141. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +15 -32
  142. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  143. package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
  144. package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
  145. package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
  146. package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
  147. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  148. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
  149. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
  150. package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
  151. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
  152. package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
  153. package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
  154. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  155. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  156. package/.agents/scripts/lib/planning-corpus.js +12 -286
  157. package/.agents/scripts/lib/preflight-runner.js +2 -2
  158. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  159. package/.agents/scripts/lib/signals/index.js +4 -17
  160. package/.agents/scripts/lib/signals/read.js +35 -35
  161. package/.agents/scripts/lib/signals/schema.js +8 -11
  162. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  163. package/.agents/scripts/lib/signals/write.js +0 -1
  164. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  165. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  166. package/.agents/scripts/lib/story-adjacency.js +8 -7
  167. package/.agents/scripts/lib/story-body/story-body.js +6 -5
  168. package/.agents/scripts/lib/templates/decomposer-prompts.js +7 -3
  169. package/.agents/scripts/lib/test-env.js +14 -1
  170. package/.agents/scripts/lib/test-tiers.js +0 -3
  171. package/.agents/scripts/lib/ticket-body-sections.js +0 -14
  172. package/.agents/scripts/lib/validation-evidence.js +31 -59
  173. package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
  174. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  175. package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
  176. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  177. package/.agents/scripts/plan-context.js +38 -6
  178. package/.agents/scripts/plan-persist.js +145 -35
  179. package/.agents/scripts/plan-run-epilogue.js +83 -38
  180. package/.agents/scripts/post-structured-comment.js +0 -38
  181. package/.agents/scripts/pr-watch-with-update.js +43 -22
  182. package/.agents/scripts/providers/github/compose.js +0 -1
  183. package/.agents/scripts/providers/github/errors.js +0 -19
  184. package/.agents/scripts/providers/github/issues.js +1 -11
  185. package/.agents/scripts/providers/github/mappers.js +5 -0
  186. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  187. package/.agents/scripts/providers/github/tickets.js +33 -153
  188. package/.agents/scripts/providers/github.js +17 -6
  189. package/.agents/scripts/resolve-stories.js +236 -0
  190. package/.agents/scripts/run-coverage.js +4 -1
  191. package/.agents/scripts/run-lint.js +2 -2
  192. package/.agents/scripts/run-verify.js +31 -2
  193. package/.agents/scripts/signals-view.js +9 -10
  194. package/.agents/scripts/single-story-close.js +173 -18
  195. package/.agents/scripts/single-story-confirm-merge.js +288 -15
  196. package/.agents/scripts/single-story-init.js +6 -10
  197. package/.agents/scripts/stories-wave-tick.js +79 -4
  198. package/.agents/scripts/story-plan.js +3 -3
  199. package/.agents/scripts/update-ticket-state.js +8 -50
  200. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  201. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  202. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  203. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  204. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  205. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  206. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  207. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  208. package/.agents/skills/core/idea-refinement/SKILL.md +3 -3
  209. package/.agents/skills/core/scope-triage/SKILL.md +3 -0
  210. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  211. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  212. package/.agents/skills/skills.index.json +2 -12
  213. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  214. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  215. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  216. package/.agents/workflows/audit-architecture.md +3 -4
  217. package/.agents/workflows/audit-clean-code.md +4 -4
  218. package/.agents/workflows/audit-documentation.md +4 -5
  219. package/.agents/workflows/audit-lighthouse.md +8 -0
  220. package/.agents/workflows/audit-navigability.md +10 -0
  221. package/.agents/workflows/audit-performance.md +2 -3
  222. package/.agents/workflows/audit-quality.md +8 -9
  223. package/.agents/workflows/audit-security.md +1 -2
  224. package/.agents/workflows/audit-seo.md +10 -0
  225. package/.agents/workflows/audit-ux-ui.md +7 -0
  226. package/.agents/workflows/deliver.md +98 -45
  227. package/.agents/workflows/git-cleanup.md +2 -2
  228. package/.agents/workflows/git-deliver.md +1 -1
  229. package/.agents/workflows/helpers/acceptance-self-eval.md +21 -13
  230. package/.agents/workflows/helpers/code-quality-guardrails.md +7 -7
  231. package/.agents/workflows/helpers/code-review.md +12 -10
  232. package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
  233. package/.agents/workflows/helpers/deliver-story.md +193 -118
  234. package/.agents/workflows/helpers/parallel-tooling.md +2 -2
  235. package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
  236. package/.agents/workflows/plan.md +184 -19
  237. package/.agents/workflows/qa-assist.md +6 -6
  238. package/.agents/workflows/qa-explore.md +3 -3
  239. package/.agents/workflows/qa-run.md +1 -5
  240. package/bin/mandrel.js +12 -1
  241. package/docs/CHANGELOG.md +40 -0
  242. package/lib/cli/registry.js +262 -19
  243. package/lib/cli/sync-agents.js +157 -0
  244. package/lib/cli/sync-commands.js +115 -6
  245. package/lib/cli/sync.js +168 -6
  246. package/lib/cli/update.js +105 -8
  247. package/lib/cli/version-helpers.js +131 -0
  248. package/lib/migrations/README.md +7 -5
  249. package/lib/migrations/index.js +12 -9
  250. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  251. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  252. package/package.json +1 -1
  253. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  254. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  255. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  256. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  257. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  258. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  259. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  260. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  261. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  262. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  263. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  264. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  265. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  266. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  267. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  268. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  269. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  270. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  271. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  272. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  273. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  274. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  275. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  276. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  277. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  278. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  279. package/.agents/schemas/risk-verdict.schema.json +0 -53
  280. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  281. package/.agents/scripts/analyze-execution.js +0 -444
  282. package/.agents/scripts/check-prepush-recovery.js +0 -90
  283. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  284. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  285. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -187
  286. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  287. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  288. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  289. package/.agents/scripts/lib/orchestration/audit-lens-routing.js +0 -128
  290. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
  291. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  292. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  293. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  294. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  295. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  296. package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
  297. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  298. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  299. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  300. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -21
  301. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  302. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  303. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  304. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -397
  305. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  306. package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
  307. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  308. package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
  309. package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
  310. package/.agents/scripts/resolve-plan-run.js +0 -117
  311. package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
@@ -36,16 +36,28 @@
36
36
  */
37
37
 
38
38
  import { parseArgs } from 'node:util';
39
+ import { parseSprintArgs } from './lib/cli-args.js';
39
40
  import { runAsCli } from './lib/cli-utils.js';
40
41
  import { resolveConfig } from './lib/config-resolver.js';
42
+ import { formatCliError } from './lib/error-redactor.js';
41
43
  import { gh as defaultGh } from './lib/gh-exec.js';
42
44
  import { getStoryBranch } from './lib/git-utils.js';
43
45
  import { Logger } from './lib/Logger.js';
46
+ import { emitTerminalFriction } from './lib/observability/runtime-friction.js';
47
+ import { MERGED_FLIP_FAILED_BLOCK_CLASS } from './lib/orchestration/lifecycle/emit-merge-flip-failed.js';
44
48
  import { parsePrNumber } from './lib/orchestration/single-story-close/phases/code-review.js';
49
+ import { runConfirmMergePhase as defaultRunConfirmMergePhase } from './lib/orchestration/single-story-close/phases/confirm-merge.js';
45
50
  import { parseCloseOptions } from './lib/orchestration/single-story-close/phases/options.js';
51
+ import { runPostLandTail } from './lib/orchestration/single-story-close/phases/post-land.js';
52
+ import {
53
+ buildTerminalEnvelope,
54
+ emitTerminalEnvelope,
55
+ exitCodeForTerminal,
56
+ NEXT_COMMANDS,
57
+ terminalFromWaitOutcome,
58
+ } from './lib/orchestration/story-deliver-terminal.js';
46
59
  import { createProvider } from './lib/provider-factory.js';
47
60
  import { confirmStoryMerged } from './lib/single-story/confirm-merge.js';
48
- import { withConfirmFollowUps } from './lib/single-story/confirm-merge-follow-ups.js';
49
61
 
50
62
  const progress = Logger.createProgress('single-story-confirm-merge', {
51
63
  stderr: true,
@@ -71,6 +83,28 @@ function readPrFlag() {
71
83
  }
72
84
  }
73
85
 
86
+ /**
87
+ * `--wait`: resume the bounded merge wait rather than probing once.
88
+ *
89
+ * This CLI serves two callers behind one command string. `confirmMerge` wants
90
+ * a fast idempotent flip for a merge that ALREADY happened (an operator-merge
91
+ * flow, or a `merged-flip-failed` retry) and must not stall. `resumeLand`
92
+ * wants to pick up a merge wait the close handed off at its per-invocation
93
+ * bound — that one must actually wait, and must be able to give up.
94
+ */
95
+ function readWaitFlag() {
96
+ try {
97
+ const { values } = parseArgs({
98
+ args: process.argv.slice(2),
99
+ options: { wait: { type: 'boolean', default: false } },
100
+ strict: false,
101
+ });
102
+ return values.wait === true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
74
108
  /**
75
109
  * Resolve the PR number for the Story branch when one was not passed on
76
110
  * the CLI. Probes `gh pr list --head <branch> --state all` (the merged PR
@@ -99,11 +133,107 @@ async function resolvePrNumber({ cwd, storyBranch, gh }) {
99
133
  }
100
134
  }
101
135
 
102
- function logConfirmResult(result) {
136
+ /**
137
+ * Story #4578 — async so the runtime-derived friction emit can be awaited.
138
+ * This CLI is the resume path for a parked worker (`--wait`), so a `pending`
139
+ * terminal here means a bounded wait expired again with the PR still in
140
+ * flight: the exact observable the retro was blind to. The emit is
141
+ * best-effort and cannot throw; awaiting it matters because `runAsCli`
142
+ * exits via `process.exit` the moment `main` resolves.
143
+ */
144
+ async function logConfirmResult(result, terminal, config) {
145
+ // Human-facing dump stays level-gated; the envelope is the machine
146
+ // contract and must survive AGENT_LOG_LEVEL=silent.
103
147
  Logger.info(
104
148
  `\n--- CONFIRM MERGE RESULT ---\n${JSON.stringify(result, null, 2)}\n--- END RESULT ---\n`,
105
149
  );
106
- return { success: true, result };
150
+ emitTerminalEnvelope(terminal);
151
+ await emitTerminalFriction({ envelope: terminal, config });
152
+ return { success: terminal.status !== 'failed', result, terminal };
153
+ }
154
+
155
+ /**
156
+ * Map a `confirmStoryMerged` envelope onto the shared terminal envelope
157
+ * (Story #4543) so this CLI and the in-close land path report one shape.
158
+ *
159
+ * The three confirm outcomes map cleanly:
160
+ * - `done` / `noop` (already-done) → `landed`. Both mean the merge is on
161
+ * the base branch and the Story is `agent::done`.
162
+ * - `pending` (`pr-open` / `no-pr`) → `pending`. Re-run once the merge
163
+ * lands; nothing is wrong.
164
+ * - `flip-failed` → `blocked` with the shared `merged-flip-failed` class:
165
+ * the merge landed, so this is a label-write fault, and the remedy is
166
+ * re-running this very command (it is idempotent).
167
+ * - `pr-not-merged` (closed without merging) → `blocked`. The PR is gone;
168
+ * a re-run cannot fix it, so it needs a human.
169
+ */
170
+ function buildConfirmTerminal({
171
+ storyId,
172
+ storyBranch,
173
+ baseBranch,
174
+ prNumber,
175
+ confirmation,
176
+ tail,
177
+ elapsedSeconds,
178
+ }) {
179
+ // `pr-not-merged` means the PR is CLOSED — reporting it as OPEN put a
180
+ // fact in the envelope that the blocked reason directly contradicts.
181
+ const prState = confirmation.merged
182
+ ? 'MERGED'
183
+ : confirmation.reason === 'pr-not-merged'
184
+ ? 'CLOSED'
185
+ : 'OPEN';
186
+ const pr =
187
+ Number.isInteger(prNumber) && prNumber > 0
188
+ ? { number: prNumber, state: prState }
189
+ : null;
190
+ const common = { storyId, storyBranch, baseBranch, pr, elapsedSeconds };
191
+
192
+ if (confirmation.action === 'done' || confirmation.action === 'noop') {
193
+ return buildTerminalEnvelope({
194
+ ...common,
195
+ status: 'landed',
196
+ phase: tail ? 'post-land' : 'done',
197
+ tail,
198
+ nextCommand: null,
199
+ });
200
+ }
201
+ if (confirmation.action === 'flip-failed') {
202
+ return buildTerminalEnvelope({
203
+ ...common,
204
+ status: 'blocked',
205
+ phase: 'confirm-merge',
206
+ blocked: {
207
+ blockClass: MERGED_FLIP_FAILED_BLOCK_CLASS,
208
+ reason:
209
+ 'merge confirmed but the agent::closing → agent::done label write failed',
210
+ frictionCommentId: null,
211
+ },
212
+ nextCommand: NEXT_COMMANDS.confirmMerge(storyId),
213
+ });
214
+ }
215
+ if (confirmation.reason === 'pr-not-merged') {
216
+ return buildTerminalEnvelope({
217
+ ...common,
218
+ status: 'blocked',
219
+ phase: 'confirm-merge',
220
+ blocked: {
221
+ blockClass: 'api-race-other',
222
+ reason: 'the PR was closed without merging (state=CLOSED)',
223
+ frictionCommentId: null,
224
+ },
225
+ nextCommand: NEXT_COMMANDS.recover(storyId),
226
+ });
227
+ }
228
+ return buildTerminalEnvelope({
229
+ ...common,
230
+ status: 'pending',
231
+ phase: 'confirm-merge',
232
+ nextCommand:
233
+ confirmation.reason === 'no-pr'
234
+ ? NEXT_COMMANDS.recover(storyId)
235
+ : NEXT_COMMANDS.confirmMerge(storyId),
236
+ });
107
237
  }
108
238
 
109
239
  async function resolveConfirmPrNumber({ prParam, cwd, storyBranch, gh }) {
@@ -122,11 +252,13 @@ export async function runConfirmMerge({
122
252
  storyId: storyIdParam,
123
253
  cwd: cwdParam,
124
254
  pr: prParam,
255
+ wait: waitParam,
125
256
  injectedProvider,
126
257
  injectedConfig,
127
258
  injectedGh,
128
259
  injectedNotify,
129
260
  injectedReadPrMergeState,
261
+ runConfirmMergePhaseFn = defaultRunConfirmMergePhase,
130
262
  } = {}) {
131
263
  const { storyId, cwd } = parseCloseOptions({
132
264
  storyIdParam,
@@ -135,14 +267,17 @@ export async function runConfirmMerge({
135
267
 
136
268
  if (!storyId) {
137
269
  throw new Error(
138
- 'Usage: node single-story-confirm-merge.js --story <STORY_ID> [--pr <n>] [--cwd <main-repo>]',
270
+ 'Usage: node single-story-confirm-merge.js --story <STORY_ID> [--pr <n>] [--wait] [--cwd <main-repo>]',
139
271
  );
140
272
  }
273
+ const wait = waitParam ?? readWaitFlag();
141
274
 
275
+ const startedAtMs = Date.now();
142
276
  const config = injectedConfig || resolveConfig({ cwd });
143
277
  const provider = injectedProvider || createProvider(config);
144
278
  const gh = injectedGh ?? defaultGh;
145
279
  const storyBranch = getStoryBranch(storyId);
280
+ const baseBranch = config.project?.baseBranch ?? 'main';
146
281
 
147
282
  progress('INIT', `Confirming merge for standalone Story #${storyId}...`);
148
283
 
@@ -157,13 +292,82 @@ export async function runConfirmMerge({
157
292
  'CONFIRM',
158
293
  `⚠️ No PR found for ${storyBranch}; cannot confirm merge. Story stays at agent::closing.`,
159
294
  );
160
- return logConfirmResult({
295
+ const noPr = {
161
296
  storyId,
162
297
  standalone: true,
163
298
  action: 'pending',
164
299
  reason: 'no-pr',
165
300
  merged: false,
301
+ };
302
+ return await logConfirmResult(
303
+ noPr,
304
+ buildConfirmTerminal({
305
+ storyId,
306
+ storyBranch,
307
+ baseBranch,
308
+ prNumber: null,
309
+ confirmation: noPr,
310
+ tail: null,
311
+ elapsedSeconds: Math.round((Date.now() - startedAtMs) / 1000),
312
+ }),
313
+ config,
314
+ );
315
+ }
316
+
317
+ // `--wait` (what `NEXT_COMMANDS.resumeLand` passes): resume the merge wait
318
+ // the close handed off at its per-invocation bound, by running the SAME
319
+ // phase the in-close path runs. Without this the resume was a single probe
320
+ // that reported `pending` and exited, so the cumulative `maxBudgetSeconds`
321
+ // give-up — the only thing that ever emits `merge.unlanded` and flips a
322
+ // wedged Story to `agent::blocked` — was reachable ONLY inside the original
323
+ // close invocation. A PR that wedged after the close returned could be
324
+ // resumed forever, always answering `pending`, never escalating to anyone.
325
+ //
326
+ // The phase anchors its cumulative budget at the PR's `createdAt`, so the
327
+ // resume does not restart the clock — exactly what the envelope's
328
+ // `waitBudget` contract already promised.
329
+ if (wait) {
330
+ const waitOutcome = await runConfirmMergePhaseFn({
331
+ cwd,
332
+ storyId,
333
+ storyBranch,
334
+ baseBranch,
335
+ prNumber,
336
+ prUrl: `${storyBranch} PR #${prNumber}`,
337
+ // The close already armed it; this CLI is resuming that wait, not
338
+ // deciding whether to arm.
339
+ autoMergeEnabled: true,
340
+ provider,
341
+ config,
342
+ progress,
343
+ injectedGh: gh,
344
+ injectedNotify,
345
+ readPrMergeStateFn: injectedReadPrMergeState,
166
346
  });
347
+ const terminal = terminalFromWaitOutcome({
348
+ waitOutcome,
349
+ storyId,
350
+ storyBranch,
351
+ baseBranch,
352
+ prNumber,
353
+ prUrl: null,
354
+ autoMergeEnabled: true,
355
+ // This CLI runs no close gates, so it reports none — `gates` is
356
+ // "each gate this invocation was responsible for".
357
+ gates: undefined,
358
+ elapsedSeconds: Math.round((Date.now() - startedAtMs) / 1000),
359
+ });
360
+ return await logConfirmResult(
361
+ {
362
+ storyId,
363
+ standalone: true,
364
+ action: waitOutcome.terminal,
365
+ resumed: true,
366
+ tail: waitOutcome.tail ?? null,
367
+ },
368
+ terminal,
369
+ config,
370
+ );
167
371
  }
168
372
 
169
373
  const confirmation = await confirmStoryMerged({
@@ -179,21 +383,90 @@ export async function runConfirmMerge({
179
383
  readPrMergeStateFn: injectedReadPrMergeState,
180
384
  });
181
385
 
182
- const result = await withConfirmFollowUps({
183
- confirmation,
386
+ // Story #4543 reach the SAME shared land tail the in-close path reaches
387
+ // (`phases/post-land.js`), rather than this CLI's own follow-ups-only
388
+ // wrapper. That wrapper was the whole reason the two landing surfaces
389
+ // diverged: it captured follow-ups and nothing else, while the resync and
390
+ // cleanup steps lived in prose the caller had to remember to run.
391
+ //
392
+ // Gated on `merged`, NOT on `action === 'done'`. `done` means "this run
393
+ // flipped the label"; a Story already at `agent::done` returns
394
+ // `action: 'noop', merged: true`, and gating on `done` skipped the tail for
395
+ // exactly that case — the belated-manual-confirm backfill this tail exists
396
+ // to make possible (see `story-follow-ups.js`, which documents the gap).
397
+ // Re-running is safe: every step is idempotent (the follow-ups comment is
398
+ // an upsert, ref cleanup and base fast-forward no-op when already done).
399
+ const tail =
400
+ confirmation.merged === true
401
+ ? await runPostLandTail({
402
+ storyId,
403
+ storyBranch,
404
+ baseBranch,
405
+ cwd,
406
+ provider,
407
+ config,
408
+ progress,
409
+ })
410
+ : null;
411
+
412
+ const terminal = buildConfirmTerminal({
184
413
  storyId,
414
+ storyBranch,
415
+ baseBranch,
185
416
  prNumber,
186
- provider,
187
- config,
188
- cwd,
189
- progress,
417
+ confirmation,
418
+ tail,
419
+ elapsedSeconds: Math.round((Date.now() - startedAtMs) / 1000),
190
420
  });
191
- if (result.action === 'done') {
192
- progress('DONE', `✅ Standalone Story #${storyId} → agent::done (merged).`);
421
+ if (confirmation.action === 'done') {
422
+ progress('DONE', `✅ Story #${storyId} → agent::done (merged).`);
423
+ }
424
+ return await logConfirmResult(
425
+ { ...confirmation, standalone: true, tail },
426
+ terminal,
427
+ config,
428
+ );
429
+ }
430
+
431
+ /**
432
+ * CLI entry — mirrors `single-story-close.js`: the exit code comes from the
433
+ * terminal envelope's status, so a caller can tell `pending` (the PR has not
434
+ * merged yet; re-run me) from `landed` without parsing stdout.
435
+ *
436
+ * The catch mirrors close's for the same reason: `confirmStoryMerged`'s PR
437
+ * read is a live `gh` call that throws on a transient API error, and this CLI
438
+ * is a *landing surface* — the one `pending` tells the operator to re-run. If
439
+ * a flaked read exited 1 with no envelope, the surface would be silent exactly
440
+ * where the envelope is the contract. Both landing surfaces emit one envelope
441
+ * or none at all; "none at all" is what Story #4543 removes.
442
+ */
443
+ async function main() {
444
+ try {
445
+ const outcome = await runConfirmMerge();
446
+ return exitCodeForTerminal(outcome?.terminal ?? { status: 'failed' });
447
+ } catch (err) {
448
+ const storyId = Number(parseSprintArgs().storyId);
449
+ // No story id → a usage error; there is nothing to report an envelope
450
+ // about, so let runAsCli surface it as a plain fatal.
451
+ if (!Number.isInteger(storyId) || storyId <= 0) throw err;
452
+ const terminal = buildTerminalEnvelope({
453
+ storyId,
454
+ status: 'failed',
455
+ phase: 'confirm-merge',
456
+ failure: { reason: String(err?.message ?? err) },
457
+ nextCommand: NEXT_COMMANDS.recover(storyId),
458
+ elapsedSeconds: 0,
459
+ });
460
+ Logger.error(
461
+ `[single-story-confirm-merge] Fatal error: ${formatCliError(err)}`,
462
+ );
463
+ emitTerminalEnvelope(terminal);
464
+ await emitTerminalFriction({ envelope: terminal });
465
+ return exitCodeForTerminal(terminal);
193
466
  }
194
- return logConfirmResult(result);
195
467
  }
196
468
 
197
- runAsCli(import.meta.url, runConfirmMerge, {
469
+ runAsCli(import.meta.url, main, {
198
470
  source: 'single-story-confirm-merge',
471
+ propagateExitCode: true,
199
472
  });
@@ -223,12 +223,10 @@ export async function reapMergedStoryBranches({
223
223
  /**
224
224
  * Fetch remote refs, reap merged story branches, and fast-forward the local
225
225
  * base branch so new story branches seed from origin's tip. Exported for
226
- * testing (owns the fast-forward cascade; mirrors `fetchMainRefs` +
227
- * `ensureEpicBranch` in `branch-initializer.js`).
226
+ * testing (owns the fast-forward cascade).
228
227
  *
229
- * Routes the `origin` fetch through `cachedGitFetch` so concurrent standalone
230
- * Story waves share the same per-process coalescing window that Epic-attached
231
- * stories get via `branch-initializer.js#fetchMainRefs`. Pass `fetchFn` to
228
+ * Routes the `origin` fetch through `cachedGitFetch` so concurrent Story
229
+ * waves share a per-process fetch-coalescing window. Pass `fetchFn` to
232
230
  * inject a stub in tests without touching real git.
233
231
  *
234
232
  * @param {object} opts
@@ -413,12 +411,10 @@ export async function provisionWorktree({
413
411
  }
414
412
 
415
413
  try {
416
- // Story #2874 standalone Stories have no parent Epic; pass
417
- // `epicId: null` so the helper omits CC_EPIC_ID from env + file
418
- // instead of throwing on a 0 sentinel. The trace hook keys its
419
- // standalone-trace branch on CC_EPIC_ID being absent.
414
+ // v2 Stories are standalone no parent Epic. The helper omits
415
+ // CC_EPIC_ID from env + file; the trace hook keys its standalone-trace
416
+ // branch on CC_EPIC_ID being absent.
420
417
  setActiveStoryEnv({
421
- epicId: null,
422
418
  storyId,
423
419
  workCwd,
424
420
  logger: {
@@ -37,7 +37,8 @@
37
37
  * totalStories: number,
38
38
  * concurrencyCap: number,
39
39
  * inFlight: number,
40
- * cycleError: string | null
40
+ * cycleError: string | null,
41
+ * wedged: { reason, stories: [{ id, unmetBlockers }] } | null
41
42
  * }
42
43
  *
43
44
  * The standalone loop calls this once per beat: after each Story closes it
@@ -54,7 +55,13 @@
54
55
  * deterministic config source (`delivery.deliverRunner.concurrencyCap`) and
55
56
  * one scheduling kernel with every `/deliver` multi-Story invocation.
56
57
  *
57
- * On cycle detection, exits with code 2 and sets cycleError in the envelope.
58
+ * Exit codes: 0 ok · 1 input error · 2 dependency cycle (`cycleError`) ·
59
+ * 3 wedged (`wedged`) — ready is empty, nothing is in flight, and undone
60
+ * Stories are waiting on blockers that are not done. A cycle is a
61
+ * self-referential DAG the operator must fix; a wedge is a well-formed DAG
62
+ * whose gates cannot be satisfied from the supplied `--done` set (usually a
63
+ * blocker outside the delivered set that has not landed). Both are distinct
64
+ * from the ordinary `ready: []` that means "waiting on in-flight work".
58
65
  */
59
66
 
60
67
  import { readFileSync } from 'node:fs';
@@ -68,6 +75,13 @@ import { AGENT_LABELS } from './lib/label-constants.js';
68
75
  import { buildStoryAdjacency } from './lib/story-adjacency.js';
69
76
  import { selectReadySet } from './lib/wave-runner/ready-set.js';
70
77
 
78
+ /**
79
+ * Exit code for a wedged run — deliberately distinct from the cycle exit (2)
80
+ * so a caller can tell "your DAG is self-referential" from "your DAG is fine
81
+ * but its gates can never be satisfied from this `--done` set".
82
+ */
83
+ export const WEDGED_EXIT_CODE = 3;
84
+
71
85
  const HELP = `Usage: node .agents/scripts/stories-wave-tick.js --dag '<json>' | --dag-file <path> [--concurrency <n>] [--done <csv>] [--in-flight <n>]
72
86
 
73
87
  Continuous ready-set planner for standalone Story delivery. Consumes a
@@ -102,13 +116,17 @@ Output envelope:
102
116
  "totalStories": 2,
103
117
  "concurrencyCap": 3,
104
118
  "inFlight": 0,
105
- "cycleError": null
119
+ "cycleError": null,
120
+ "wedged": null
106
121
  }
107
122
 
108
123
  Exit codes:
109
124
  0 - Success, ready set emitted
110
125
  1 - Invalid input (missing/malformed DAG, invalid --concurrency/--in-flight/--done)
111
126
  2 - Cycle detected in dependency graph
127
+ 3 - Wedged: ready is empty, nothing is in flight, and undone Stories are
128
+ waiting on blockers that are not done. Distinct from an ordinary empty
129
+ ready set (which means "waiting on in-flight work") and from a cycle.
112
130
  `;
113
131
 
114
132
  /**
@@ -322,6 +340,7 @@ export function buildReadySetEnvelope(
322
340
  concurrencyCap,
323
341
  inFlight,
324
342
  cycleError: null,
343
+ wedged: null,
325
344
  };
326
345
 
327
346
  if (totalStories === 0) {
@@ -365,7 +384,62 @@ export function buildReadySetEnvelope(
365
384
  globalCap: concurrencyCap,
366
385
  }).map((rec) => rec.id);
367
386
 
368
- return { envelope: { ...base, ready }, exitCode: 0 };
387
+ // Wedge detection (Story #4540). `ready: []` is normal while work is in
388
+ // flight — the loop is simply waiting. But ready-empty AND nothing in
389
+ // flight AND undone Stories remaining means no beat can ever make
390
+ // progress: the run is stuck, and the previous behaviour was to return
391
+ // exit 0 with an empty ready set forever, indistinguishable from
392
+ // "waiting". Name the stuck ids and their unmet blockers.
393
+ //
394
+ // Distinct from `cycleError`/exit 2: a cycle is a self-referential DAG,
395
+ // whereas this is a DAG whose gates are real but unsatisfiable from the
396
+ // supplied `done` set (typically a blocker outside the delivered set that
397
+ // has not landed).
398
+ const wedge = detectWedge({ nodes, doneIds, ready, inFlight });
399
+ if (wedge) {
400
+ return {
401
+ envelope: { ...base, ready, wedged: wedge },
402
+ exitCode: WEDGED_EXIT_CODE,
403
+ };
404
+ }
405
+
406
+ return { envelope: { ...base, ready, wedged: null }, exitCode: 0 };
407
+ }
408
+
409
+ /**
410
+ * Identify a run that cannot progress: nothing dispatchable, nothing in
411
+ * flight, work remaining.
412
+ *
413
+ * @param {{ nodes: object[], doneIds: Set<number>, ready: number[], inFlight: number }} args
414
+ * @returns {{ reason: string, stories: Array<{ id: number, unmetBlockers: number[] }> }|null}
415
+ */
416
+ export function detectWedge({ nodes, doneIds, ready, inFlight }) {
417
+ if (ready.length > 0 || inFlight > 0) return null;
418
+ const undone = nodes.filter((n) => !doneIds.has(n.id));
419
+ if (undone.length === 0) return null;
420
+
421
+ const stories = undone
422
+ .map((n) => ({
423
+ id: n.id,
424
+ unmetBlockers: (n.dependsOn ?? []).filter((dep) => !doneIds.has(dep)),
425
+ }))
426
+ .filter((s) => s.unmetBlockers.length > 0);
427
+
428
+ // Undone work with no unmet blockers would have been dispatched; if that
429
+ // is the whole set, the cap or in-flight accounting explains the empty
430
+ // ready set rather than a wedge.
431
+ if (stories.length === 0) return null;
432
+
433
+ const detail = stories
434
+ .map((s) => `#${s.id} ← ${s.unmetBlockers.map((d) => `#${d}`).join(', ')}`)
435
+ .join('; ');
436
+ return {
437
+ reason:
438
+ `No Story can be dispatched: nothing is in flight and ${stories.length} ` +
439
+ `Story(ies) are waiting on blockers that are not done — ${detail}. ` +
440
+ `A blocker outside the delivered set must land first, or be included in --ids.`,
441
+ stories,
442
+ };
369
443
  }
370
444
 
371
445
  /**
@@ -405,6 +479,7 @@ export function runStoriesWaveTick({
405
479
  concurrencyCap,
406
480
  inFlight: inFlightValue,
407
481
  cycleError: null,
482
+ wedged: null,
408
483
  inputError: message,
409
484
  },
410
485
  exitCode: 1,
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Operator planning:
14
14
  * node .agents/scripts/plan-context.js --seed "…" | --seed-file <path> | --tickets <ids>
15
- * node .agents/scripts/plan-persist.js --stories … --risk-verdict …
15
+ * node .agents/scripts/plan-persist.js --stories …
16
16
  */
17
17
 
18
18
  import { readFile } from 'node:fs/promises';
@@ -146,7 +146,7 @@ async function runEmitContext({
146
146
  // corpus digest reads the project's actual docs directory regardless
147
147
  // of the directory this CLI happens to be invoked from — matching the
148
148
  // sibling resolution pattern in
149
- // epic-plan-spec/phases/authoring-context.js.
149
+ // planning/authoring-context.js.
150
150
  const docsRoot = path.resolve(
151
151
  PROJECT_ROOT,
152
152
  config?.project?.paths?.docsRoot ?? 'docs',
@@ -157,7 +157,7 @@ async function runEmitContext({
157
157
  loadBodyTemplate(projectRoot),
158
158
  fetchOpenStories(provider),
159
159
  readTechStackSummary(projectRoot),
160
- buildCorpusContext({ seed, provider, docsContextFiles, docsRoot }),
160
+ buildCorpusContext({ docsContextFiles, docsRoot }),
161
161
  ]);
162
162
 
163
163
  const duplicateCandidates = rankDuplicateCandidates({
@@ -8,15 +8,7 @@
8
8
  import { parseArgs } from 'node:util';
9
9
  import { resolveConfig } from './lib/config-resolver.js';
10
10
  import { Logger } from './lib/Logger.js';
11
- import {
12
- enqueueLabel,
13
- outboxPathFor,
14
- } from './lib/orchestration/bookkeeping-outbox.js';
15
- import {
16
- cascadeCompletion,
17
- STATE_LABELS,
18
- transitionTicketState,
19
- } from './lib/orchestration/ticketing.js';
11
+ import { transitionTicketState } from './lib/orchestration/ticketing.js';
20
12
  import { createProvider } from './lib/provider-factory.js';
21
13
 
22
14
  // ── CLI Main Block ────────────────────────────────────────────────────────
@@ -31,8 +23,6 @@ if (
31
23
  ticket: { type: 'string' },
32
24
  state: { type: 'string' },
33
25
  'remove-label': { type: 'string' },
34
- buffer: { type: 'boolean' },
35
- epic: { type: 'string' },
36
26
  },
37
27
  strict: false,
38
28
  });
@@ -66,50 +56,18 @@ if (
66
56
  return;
67
57
  }
68
58
 
69
- // Headless buffering (Epic #4476 M5): buffer an intermediate state flip to
70
- // the per-Epic outbox instead of a live GitHub round-trip; finalize's
71
- // bookkeeping-reconcile.js drains it once. `agent::blocked` (the HITL gate,
72
- // §1.J) and `agent::done` (its cascade must run live) ALWAYS surface
73
- // immediately and are never buffered. Requires --epic to locate the outbox.
74
- if (
75
- values.buffer &&
76
- state !== STATE_LABELS.BLOCKED &&
77
- state !== STATE_LABELS.DONE
78
- ) {
79
- const epicId = Number.parseInt(values.epic ?? '', 10);
80
- if (!Number.isInteger(epicId) || epicId <= 0) {
81
- throw new Error('--buffer requires --epic <id>');
82
- }
83
- enqueueLabel({
84
- outboxPath: outboxPathFor(epicId, config),
85
- ticketId,
86
- state,
87
- });
88
- Logger.info(
89
- `[State-Sync] Buffered #${ticketId} → ${state} (headless; drained at finalize)`,
90
- );
91
- return;
92
- }
93
-
94
59
  Logger.info(
95
60
  `[State-Sync] Transitioning ticket #${ticketId} to ${state}...`,
96
61
  );
62
+ // Story #4545 — no second cascade here. `transitionTicketState` already
63
+ // fires the upward cascade on every transition (bulk.js registers
64
+ // `cascadeParentState` as transition.js's cascade runner, and the DONE
65
+ // branch delegates to `cascadeCompletion`), so the explicit call this
66
+ // used to make re-walked the same parents and re-spent the same API
67
+ // calls a second time on every done transition — under a Story-only
68
+ // model where no orchestration parent exists to find.
97
69
  await transitionTicketState(provider, ticketId, state);
98
70
 
99
- if (state === STATE_LABELS.DONE) {
100
- Logger.info(`[State-Sync] Cascading completion from #${ticketId}...`);
101
- const cascade = await cascadeCompletion(provider, ticketId);
102
- // Hoisted out of the `for...of` initializer because typhonjs-escomplex
103
- // mis-parses optional chaining there (it would zero out this file's
104
- // maintainability score).
105
- const cascadeFailures = cascade?.failed ?? [];
106
- for (const { parentId, error } of cascadeFailures) {
107
- Logger.warn(
108
- `[State-Sync] ⚠️ Cascade partial-failure on parent #${parentId}: ${error}`,
109
- );
110
- }
111
- }
112
-
113
71
  // Optional secondary label removal alongside the state transition
114
72
  // (e.g. clear `status::blocked` when transitioning back to ready).
115
73
  if (removeLabel) {