mandrel 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. package/.agents/README.md +59 -28
  2. package/.agents/agents/acceptance-critic.md +20 -9
  3. package/.agents/agents/story-worker.md +45 -48
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +60 -46
  6. package/.agents/docs/agentrc-reference.json +8 -13
  7. package/.agents/docs/configuration.md +33 -57
  8. package/.agents/docs/execution-reference.md +39 -10
  9. package/.agents/docs/quality-gates.md +17 -19
  10. package/.agents/docs/workflows.md +6 -6
  11. package/.agents/instructions.md +64 -79
  12. package/.agents/rules/ci-remediation.md +3 -3
  13. package/.agents/rules/gherkin-standards.md +10 -0
  14. package/.agents/rules/git-conventions-reference.md +42 -51
  15. package/.agents/schemas/acceptance-eval-verdict.schema.json +2 -2
  16. package/.agents/schemas/agentrc.schema.json +35 -46
  17. package/.agents/schemas/audit-rules.json +59 -1
  18. package/.agents/schemas/audit-rules.schema.json +33 -1
  19. package/.agents/schemas/lifecycle/README.md +1 -2
  20. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  21. package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
  22. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
  23. package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
  24. package/.agents/schemas/signal-event.schema.json +3 -3
  25. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  26. package/.agents/schemas/validation-evidence.schema.json +1 -1
  27. package/.agents/scripts/acceptance-eval.js +24 -68
  28. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  29. package/.agents/scripts/bootstrap.js +3 -3
  30. package/.agents/scripts/check-dead-exports.js +43 -104
  31. package/.agents/scripts/check-doc-links.js +2 -2
  32. package/.agents/scripts/check-lifecycle-lint.js +1 -1
  33. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  34. package/.agents/scripts/deliver-recover.js +122 -0
  35. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  36. package/.agents/scripts/evidence-gate.js +20 -50
  37. package/.agents/scripts/generate-skills-index.js +17 -1
  38. package/.agents/scripts/generate-workflows-doc.js +4 -4
  39. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  40. package/.agents/scripts/lib/audit-suite/selector.js +323 -23
  41. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
  42. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  43. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  44. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  45. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  46. package/.agents/scripts/lib/checks/core-bare-clean.js +4 -1
  47. package/.agents/scripts/lib/checks/index.js +1 -1
  48. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  49. package/.agents/scripts/lib/checks/state.js +17 -248
  50. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +3 -3
  51. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  52. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  53. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  54. package/.agents/scripts/lib/cli-args.js +23 -2
  55. package/.agents/scripts/lib/close-validation/gates.js +13 -13
  56. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  57. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  58. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  59. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  60. package/.agents/scripts/lib/config/acceptance-eval.js +2 -2
  61. package/.agents/scripts/lib/config/delivery-routing.js +7 -6
  62. package/.agents/scripts/lib/config/explain.js +10 -16
  63. package/.agents/scripts/lib/config/github.js +7 -5
  64. package/.agents/scripts/lib/config/limits.js +15 -25
  65. package/.agents/scripts/lib/config/quality.js +11 -14
  66. package/.agents/scripts/lib/config/runners.js +8 -21
  67. package/.agents/scripts/lib/config/temp-paths.js +18 -56
  68. package/.agents/scripts/lib/config-settings-schema-delivery.js +34 -16
  69. package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
  70. package/.agents/scripts/lib/config-settings-schema.js +48 -22
  71. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  72. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  73. package/.agents/scripts/lib/duplicate-search.js +38 -7
  74. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  75. package/.agents/scripts/lib/format-generated-json.js +97 -0
  76. package/.agents/scripts/lib/framework-version.js +19 -189
  77. package/.agents/scripts/lib/gh-exec.js +8 -0
  78. package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
  79. package/.agents/scripts/lib/git-utils.js +0 -14
  80. package/.agents/scripts/lib/json-utils.js +1 -2
  81. package/.agents/scripts/lib/label-constants.js +0 -15
  82. package/.agents/scripts/lib/label-taxonomy.js +1 -12
  83. package/.agents/scripts/lib/observability/active-story-env.js +42 -163
  84. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  85. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  86. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  87. package/.agents/scripts/lib/observability/source-classifier.js +2 -2
  88. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  89. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  90. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  91. package/.agents/scripts/lib/orchestration/ceremony-routing.js +43 -45
  92. package/.agents/scripts/lib/orchestration/change-set.js +103 -0
  93. package/.agents/scripts/lib/orchestration/code-review.js +70 -191
  94. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
  95. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  96. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  97. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  98. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  99. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  100. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +9 -11
  101. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  102. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  103. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +7 -3
  104. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  105. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  106. package/.agents/scripts/lib/orchestration/merge-block-class.js +76 -20
  107. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  108. package/.agents/scripts/lib/orchestration/plan-context.js +116 -33
  109. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +26 -36
  110. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +31 -22
  111. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  112. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
  113. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
  114. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  115. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -100
  116. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
  117. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
  118. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  119. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +230 -0
  120. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
  121. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -2
  122. package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
  123. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  124. package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
  125. package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
  126. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  127. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  128. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  129. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  130. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  131. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  132. package/.agents/scripts/lib/orchestration/run-epilogue.js +374 -16
  133. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
  134. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  135. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  136. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +4 -13
  137. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  138. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +72 -30
  139. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  140. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  141. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +12 -8
  142. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  143. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  144. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +264 -43
  145. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  146. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  147. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +104 -279
  148. package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +191 -0
  149. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +120 -0
  150. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  151. package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
  152. package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
  153. package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
  154. package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
  155. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  156. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
  157. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
  158. package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
  159. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
  160. package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
  161. package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
  162. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  163. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  164. package/.agents/scripts/lib/planning-corpus.js +12 -286
  165. package/.agents/scripts/lib/preflight-runner.js +2 -2
  166. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  167. package/.agents/scripts/lib/signals/index.js +4 -17
  168. package/.agents/scripts/lib/signals/read.js +35 -35
  169. package/.agents/scripts/lib/signals/schema.js +8 -11
  170. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  171. package/.agents/scripts/lib/signals/write.js +0 -1
  172. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  173. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  174. package/.agents/scripts/lib/story-adjacency.js +8 -7
  175. package/.agents/scripts/lib/story-body/story-body.js +81 -13
  176. package/.agents/scripts/lib/templates/decomposer-prompts.js +15 -16
  177. package/.agents/scripts/lib/test-env.js +14 -1
  178. package/.agents/scripts/lib/test-tiers.js +0 -3
  179. package/.agents/scripts/lib/ticket-body-sections.js +0 -14
  180. package/.agents/scripts/lib/validation-evidence.js +31 -59
  181. package/.agents/scripts/lib/wave-runner/live-probe.js +315 -0
  182. package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
  183. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  184. package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
  185. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  186. package/.agents/scripts/plan-context.js +38 -7
  187. package/.agents/scripts/plan-critics.js +203 -0
  188. package/.agents/scripts/plan-persist.js +145 -35
  189. package/.agents/scripts/plan-run-epilogue.js +83 -38
  190. package/.agents/scripts/post-structured-comment.js +0 -38
  191. package/.agents/scripts/pr-watch-with-update.js +43 -22
  192. package/.agents/scripts/providers/github/compose.js +0 -1
  193. package/.agents/scripts/providers/github/errors.js +0 -19
  194. package/.agents/scripts/providers/github/issues.js +1 -11
  195. package/.agents/scripts/providers/github/mappers.js +5 -0
  196. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  197. package/.agents/scripts/providers/github/tickets.js +33 -153
  198. package/.agents/scripts/providers/github.js +17 -6
  199. package/.agents/scripts/quality-preview.js +13 -6
  200. package/.agents/scripts/resolve-stories.js +236 -0
  201. package/.agents/scripts/run-coverage.js +4 -1
  202. package/.agents/scripts/run-lint.js +2 -2
  203. package/.agents/scripts/run-verify.js +31 -2
  204. package/.agents/scripts/signals-view.js +9 -10
  205. package/.agents/scripts/single-story-close.js +173 -18
  206. package/.agents/scripts/single-story-confirm-merge.js +288 -15
  207. package/.agents/scripts/single-story-init.js +6 -10
  208. package/.agents/scripts/stories-wave-tick.js +380 -53
  209. package/.agents/scripts/story-plan.js +3 -3
  210. package/.agents/scripts/update-ticket-state.js +8 -50
  211. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  212. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  213. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  214. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  215. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  216. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  217. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  218. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  219. package/.agents/skills/core/idea-refinement/SKILL.md +3 -3
  220. package/.agents/skills/core/scope-triage/SKILL.md +3 -0
  221. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  222. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  223. package/.agents/skills/skills.index.json +2 -12
  224. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  225. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  226. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  227. package/.agents/workflows/audit-architecture.md +3 -4
  228. package/.agents/workflows/audit-clean-code.md +4 -4
  229. package/.agents/workflows/audit-documentation.md +4 -5
  230. package/.agents/workflows/audit-lighthouse.md +8 -0
  231. package/.agents/workflows/audit-navigability.md +10 -0
  232. package/.agents/workflows/audit-performance.md +2 -3
  233. package/.agents/workflows/audit-quality.md +8 -9
  234. package/.agents/workflows/audit-security.md +1 -2
  235. package/.agents/workflows/audit-seo.md +10 -0
  236. package/.agents/workflows/audit-ux-ui.md +7 -0
  237. package/.agents/workflows/deliver.md +133 -45
  238. package/.agents/workflows/git-cleanup.md +2 -2
  239. package/.agents/workflows/git-deliver.md +1 -1
  240. package/.agents/workflows/helpers/acceptance-self-eval.md +34 -17
  241. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -12
  242. package/.agents/workflows/helpers/code-review.md +14 -12
  243. package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
  244. package/.agents/workflows/helpers/deliver-story.md +209 -118
  245. package/.agents/workflows/helpers/parallel-tooling.md +2 -2
  246. package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
  247. package/.agents/workflows/plan.md +239 -19
  248. package/.agents/workflows/qa-assist.md +6 -6
  249. package/.agents/workflows/qa-explore.md +3 -3
  250. package/.agents/workflows/qa-run.md +1 -5
  251. package/bin/mandrel.js +12 -1
  252. package/docs/CHANGELOG.md +62 -0
  253. package/lib/cli/registry.js +262 -19
  254. package/lib/cli/sync-agents.js +157 -0
  255. package/lib/cli/sync-commands.js +115 -6
  256. package/lib/cli/sync.js +168 -6
  257. package/lib/cli/update.js +105 -8
  258. package/lib/cli/version-helpers.js +131 -0
  259. package/lib/migrations/README.md +7 -5
  260. package/lib/migrations/index.js +17 -9
  261. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  262. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  263. package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +154 -0
  264. package/package.json +2 -2
  265. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  266. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  267. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  268. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  269. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  270. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  271. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  272. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  273. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  274. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  275. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  276. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  277. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  278. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  279. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  280. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  281. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  282. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  283. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  284. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  285. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  286. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  287. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  288. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  289. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  290. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  291. package/.agents/schemas/risk-verdict.schema.json +0 -53
  292. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  293. package/.agents/scripts/analyze-execution.js +0 -444
  294. package/.agents/scripts/check-prepush-recovery.js +0 -90
  295. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  296. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  297. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -187
  298. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  299. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  300. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  301. package/.agents/scripts/lib/orchestration/audit-lens-routing.js +0 -128
  302. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
  303. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  304. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  305. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  306. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  307. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  308. package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
  309. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  310. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  311. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  312. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -21
  313. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  314. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  315. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  316. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -397
  317. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  318. package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
  319. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  320. package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
  321. package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
  322. package/.agents/scripts/resolve-plan-run.js +0 -117
  323. package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
@@ -0,0 +1,120 @@
1
+ /**
2
+ * phases/review-core.js — the shared Story-scope review spine.
3
+ *
4
+ * Extracted from `phases/code-review.js` (Story #4603). `runStoryReviewCore` is
5
+ * the one implementation both close paths call `runCodeReview` through — the
6
+ * epic-attached phase (`code-review.js#runStoryCodeReview`) and the standalone
7
+ * v2 path (`single-story-close/phases/code-review.js#runStoryScopeReview`) —
8
+ * so it belongs to neither and lives here rather than inside one path's phase
9
+ * file (Story #3653 established the shared-spine contract).
10
+ */
11
+
12
+ import { gitSpawn } from '../../../git-utils.js';
13
+ import { computeChangeSet } from '../../change-set.js';
14
+ import { runCodeReview } from '../../code-review.js';
15
+ import { runLocalLensReview } from './local-lens-review.js';
16
+
17
+ /**
18
+ * Invoke `runCodeReviewFn` with the canonical Story-scope envelope and return
19
+ * the raw result.
20
+ *
21
+ * The caller is responsible for error handling and result interpretation —
22
+ * this function propagates throws rather than swallowing them, because the
23
+ * two callers have different advisory postures:
24
+ *
25
+ * - Epic-attached close: swallows throws (non-blocking advisory, same as
26
+ * `refresh.js`).
27
+ * - Standalone close: propagates throws (a review failure stops the close).
28
+ *
29
+ * Review depth is not passed in: `runCodeReview` derives it from the changed
30
+ * files — their sensitive-path intersection plus their count (Story #4542, which
31
+ * retired the planner-authored risk envelope this spine used to forward). Depth
32
+ * remains an **input-only** signal: it tells the provider how thorough to be and
33
+ * never alters the review's output envelope or the posted structured-comment
34
+ * body.
35
+ *
36
+ * Story #4593 — this spine is the **single injection point** for the change set.
37
+ * It enumerates `baseRef...headRef` exactly once via {@link computeChangeSet}
38
+ * and threads the resulting list into both the local-lens pass and
39
+ * `runCodeReview`, which otherwise each enumerated the diff for themselves. Both
40
+ * consumers ultimately route through `deriveChangeLevel`, so feeding them one
41
+ * list is what makes the lens roster and the review depth provably agree about
42
+ * what changed — even when a commit lands between the two calls.
43
+ *
44
+ * Story #4603 — the invariant now holds on the failure path too: an unenumerable
45
+ * diff injects an explicit `null`, and both consumers distinguish that from
46
+ * `undefined` ("nobody enumerated") rather than re-spawning git.
47
+ *
48
+ * @param {{
49
+ * storyId: number|string,
50
+ * baseRef: string,
51
+ * headRef: string,
52
+ * commentTargetId?: number|null,
53
+ * provider: object,
54
+ * progress: (tag: string, msg: string) => void,
55
+ * progressTag?: string,
56
+ * gitSpawnFn?: import('../../change-set.js').GitSpawnFn,
57
+ * computeChangeSetFn?: typeof computeChangeSet,
58
+ * runCodeReviewFn?: typeof runCodeReview,
59
+ * runLocalLensReviewFn?: typeof runLocalLensReview,
60
+ * }} args
61
+ * @returns {Promise<object>} Raw result envelope from `runCodeReview`, augmented
62
+ * with a `localLensReview` field carrying the Story-scope local-lens pass
63
+ * outcome (Epic #4405, Story #4409) and the `changeSet` this run computed
64
+ * (Story #4593).
65
+ */
66
+ export async function runStoryReviewCore({
67
+ storyId,
68
+ baseRef,
69
+ headRef,
70
+ commentTargetId = null,
71
+ provider,
72
+ progress,
73
+ progressTag = 'CODE-REVIEW',
74
+ gitSpawnFn = gitSpawn,
75
+ computeChangeSetFn = computeChangeSet,
76
+ runCodeReviewFn = runCodeReview,
77
+ runLocalLensReviewFn = runLocalLensReview,
78
+ }) {
79
+ const storyIdNum = Number(storyId);
80
+
81
+ // The one enumeration per close run. Every consumer below is injected from
82
+ // this list; none of them re-derives the diff. `files` is `null` when the
83
+ // diff is unenumerable — an explicit "already tried" signal both consumers
84
+ // honour without retrying (Story #4603).
85
+ const changeSet = computeChangeSetFn({ baseRef, headRef, gitSpawnFn });
86
+
87
+ const opts = {
88
+ scope: 'story',
89
+ ticketId: storyIdNum,
90
+ baseRef,
91
+ headRef,
92
+ provider,
93
+ changedFiles: changeSet.files,
94
+ gitSpawnFn,
95
+ logger: {
96
+ info: (m) => progress(progressTag, m),
97
+ warn: (m) => progress(progressTag, `⚠️ ${m}`),
98
+ },
99
+ };
100
+ if (commentTargetId != null) {
101
+ opts.commentTargetId = commentTargetId;
102
+ }
103
+
104
+ // Shift-left local-lens pass (Epic #4405). Runs matched local lenses at
105
+ // `light` depth against the actual Story diff, inside this close-subprocess
106
+ // spine so the maker never grades its own work. Advisory — it never blocks
107
+ // the close and its outcome rides on the returned envelope for downstream
108
+ // consumers.
109
+ const localLensReview = await runLocalLensReviewFn({
110
+ baseRef,
111
+ headRef,
112
+ changedFiles: changeSet.files,
113
+ progress,
114
+ progressTag,
115
+ gitSpawnFn,
116
+ });
117
+
118
+ const result = await runCodeReviewFn(opts);
119
+ return { ...result, localLensReview, changeSet };
120
+ }
@@ -0,0 +1,360 @@
1
+ /**
2
+ * story-deliver-terminal.js — the one terminal envelope every Story
3
+ * close-and-land invocation emits, and the shared next-command vocabulary
4
+ * (Story #4543).
5
+ *
6
+ * Before this module the delivery tail had **two** divergent return
7
+ * contracts, both prose and neither validated:
8
+ * `.agents/workflows/helpers/deliver-story.md` defined one shape and
9
+ * `.agents/agents/story-worker.md` a different one, so a caller could not
10
+ * distinguish a landed Story from a parked one without re-probing GitHub.
11
+ * `story-deliver-terminal.schema.json` is now the SSOT both docs reference
12
+ * rather than restate, and this module is its only writer.
13
+ *
14
+ * Two exports carry the contract:
15
+ *
16
+ * - {@link buildTerminalEnvelope} assembles and **validates** the envelope.
17
+ * It throws on a schema violation rather than emitting a malformed
18
+ * terminal: a silently-wrong terminal is exactly the failure this Story
19
+ * exists to eliminate, so failing loudly at the writer is the point.
20
+ * - {@link NEXT_COMMANDS} is the next-command vocabulary shared with
21
+ * `deliver-recover.js`, so a `pending` envelope and a recovery probe
22
+ * name the same command for the same state instead of inventing two
23
+ * dialects for one condition.
24
+ *
25
+ * `TERMINAL_EXIT_CODES` maps status → process exit code. `pending` gets its
26
+ * **own** code (3) precisely so a caller can tell "slow CI, resume me" from
27
+ * "hard block, come look" without parsing stdout — the distinction the
28
+ * pre-#4543 pipeline collapsed by treating budget exhaustion as a block.
29
+ */
30
+
31
+ import { readFileSync } from 'node:fs';
32
+ import path from 'node:path';
33
+ import { fileURLToPath } from 'node:url';
34
+
35
+ import Ajv2020 from 'ajv/dist/2020.js';
36
+ import addFormats from 'ajv-formats';
37
+
38
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
39
+ const SCHEMA_PATH = path.resolve(
40
+ __dirname,
41
+ '..',
42
+ '..',
43
+ '..',
44
+ 'schemas',
45
+ 'story-deliver-terminal.schema.json',
46
+ );
47
+
48
+ export const TERMINAL_ENVELOPE_KIND = 'story-deliver-terminal';
49
+
50
+ /**
51
+ * status → process exit code.
52
+ *
53
+ * `pending` is **3**, not 0 and not 1. Zero would tell a headless caller the
54
+ * Story landed when it did not; one would conflate a resumable slow-CI wait
55
+ * with a hard block and send the operator to diagnose branch protection that
56
+ * is working fine. A distinct code is what lets `/deliver` resume without
57
+ * classifying.
58
+ */
59
+ export const TERMINAL_EXIT_CODES = Object.freeze({
60
+ landed: 0,
61
+ pending: 3,
62
+ blocked: 1,
63
+ failed: 1,
64
+ });
65
+
66
+ export const TERMINAL_STATUSES = Object.freeze([
67
+ 'landed',
68
+ 'pending',
69
+ 'blocked',
70
+ 'failed',
71
+ ]);
72
+
73
+ /**
74
+ * The shared next-command vocabulary. Every producer of a "what now?"
75
+ * answer — the `pending` terminal envelope and `deliver-recover.js` —
76
+ * builds its command from here, so the two surfaces never drift into
77
+ * naming different commands for the same observed state.
78
+ */
79
+ export const NEXT_COMMANDS = Object.freeze({
80
+ /**
81
+ * Resume a bounded merge wait that expired with the PR still in flight.
82
+ *
83
+ * `--wait` is load-bearing: without it the confirm CLI probes once and
84
+ * answers `pending` again, so the cumulative-budget give-up never fires and
85
+ * a wedged PR is never escalated to anyone.
86
+ */
87
+ resumeLand: (storyId) =>
88
+ `node .agents/scripts/single-story-confirm-merge.js --story ${storyId} --wait`,
89
+ /** Confirm a merged-but-mislabelled Story (the idempotent flip + tail). */
90
+ confirmMerge: (storyId) =>
91
+ `node .agents/scripts/single-story-confirm-merge.js --story ${storyId}`,
92
+ /** Enter the red-CI fix loop against the failing PR. */
93
+ watchCi: (storyId, prNumber) =>
94
+ `node .agents/scripts/pr-watch-with-update.js --pr ${prNumber} --story ${storyId}`,
95
+ /** Re-run close for a Story whose PR was never opened. */
96
+ close: (storyId) =>
97
+ `node .agents/scripts/single-story-close.js --story ${storyId}`,
98
+ /** Resume implementation in the Story worktree. */
99
+ implement: (storyId) =>
100
+ `node .agents/scripts/single-story-init.js --story ${storyId}`,
101
+ /** Re-assert a drifted Projects v2 Status column. */
102
+ resync: (storyId) =>
103
+ `node .agents/scripts/resync-status-column.js --story ${storyId}`,
104
+ /** Probe a stranded Story and print its single next command. */
105
+ recover: (storyId) =>
106
+ `node .agents/scripts/deliver-recover.js --story ${storyId}`,
107
+ });
108
+
109
+ /** @type {Function|null} */
110
+ let _validator = null;
111
+
112
+ /**
113
+ * Compile (once) and return the terminal-envelope validator.
114
+ *
115
+ * @returns {Function}
116
+ */
117
+ function getValidator() {
118
+ if (_validator) return _validator;
119
+ const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
120
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
121
+ addFormats(ajv);
122
+ _validator = ajv.compile(schema);
123
+ return _validator;
124
+ }
125
+
126
+ /**
127
+ * Validate a candidate envelope against the shipped schema.
128
+ *
129
+ * @param {object} envelope
130
+ * @returns {{ valid: boolean, errors: string[] }}
131
+ */
132
+ export function validateTerminalEnvelope(envelope) {
133
+ const validate = getValidator();
134
+ const valid = validate(envelope);
135
+ if (valid) return { valid: true, errors: [] };
136
+ const errors = (validate.errors ?? []).map(
137
+ (e) => `${e.instancePath || '/'} ${e.message}`,
138
+ );
139
+ return { valid: false, errors };
140
+ }
141
+
142
+ /**
143
+ * Drop `undefined`-valued keys so the schema's `additionalProperties: false`
144
+ * and its nullable unions both stay satisfiable from one optional-argument
145
+ * builder signature.
146
+ *
147
+ * @param {object} obj
148
+ * @returns {object}
149
+ */
150
+ function compact(obj) {
151
+ const out = {};
152
+ for (const [k, v] of Object.entries(obj)) {
153
+ if (v !== undefined) out[k] = v;
154
+ }
155
+ return out;
156
+ }
157
+
158
+ /**
159
+ * Assemble the terminal envelope and validate it before returning.
160
+ *
161
+ * Throws a `TypeError` naming the schema violations when the assembled
162
+ * object does not validate. That is deliberate: the whole point of the
163
+ * envelope is that a caller can trust its status without re-probing
164
+ * GitHub, so emitting an unvalidated one would reintroduce the ambiguity
165
+ * this replaces.
166
+ *
167
+ * @param {object} args
168
+ * @param {number} args.storyId
169
+ * @param {'landed'|'pending'|'blocked'|'failed'} args.status
170
+ * @param {string} args.phase
171
+ * @param {string} [args.storyBranch]
172
+ * @param {string} [args.baseBranch]
173
+ * @param {object|null} [args.pr]
174
+ * @param {object} [args.gates]
175
+ * @param {object|null} [args.tail]
176
+ * @param {object|null} [args.blocked]
177
+ * @param {object|null} [args.failure]
178
+ * @param {string|null} [args.nextCommand]
179
+ * @param {number} args.elapsedSeconds
180
+ * @param {object|null} [args.waitBudget]
181
+ * @param {string} [args.timestamp]
182
+ * @returns {object} The validated envelope.
183
+ */
184
+ export function buildTerminalEnvelope({
185
+ storyId,
186
+ status,
187
+ phase,
188
+ storyBranch,
189
+ baseBranch,
190
+ pr,
191
+ gates,
192
+ tail,
193
+ blocked,
194
+ failure,
195
+ nextCommand,
196
+ elapsedSeconds = 0,
197
+ waitBudget,
198
+ timestamp = new Date().toISOString(),
199
+ }) {
200
+ const envelope = compact({
201
+ kind: TERMINAL_ENVELOPE_KIND,
202
+ storyId: Number(storyId),
203
+ status,
204
+ phase,
205
+ storyBranch: storyBranch ?? null,
206
+ baseBranch: baseBranch ?? null,
207
+ pr: pr ?? null,
208
+ gates,
209
+ tail: tail ?? null,
210
+ blocked: blocked ?? null,
211
+ failure: failure ?? null,
212
+ nextCommand: nextCommand ?? null,
213
+ elapsedSeconds: Math.max(0, Number(elapsedSeconds) || 0),
214
+ waitBudget: waitBudget ?? null,
215
+ timestamp,
216
+ });
217
+
218
+ const { valid, errors } = validateTerminalEnvelope(envelope);
219
+ if (!valid) {
220
+ throw new TypeError(
221
+ `buildTerminalEnvelope: assembled envelope violates story-deliver-terminal.schema.json:\n` +
222
+ errors.map((e) => ` - ${e}`).join('\n'),
223
+ );
224
+ }
225
+ return envelope;
226
+ }
227
+
228
+ /**
229
+ * Resolve the process exit code for a terminal envelope.
230
+ *
231
+ * @param {object} envelope
232
+ * @returns {number}
233
+ */
234
+ export function exitCodeForTerminal(envelope) {
235
+ return TERMINAL_EXIT_CODES[envelope?.status] ?? 1;
236
+ }
237
+
238
+ /** The markers a caller scans stdout for to recover the envelope. */
239
+ export const TERMINAL_BEGIN_MARKER = '--- STORY DELIVER TERMINAL ---';
240
+ export const TERMINAL_END_MARKER = '--- END TERMINAL ---';
241
+
242
+ /**
243
+ * Write a terminal envelope to stdout, between its markers.
244
+ *
245
+ * **Deliberately not `Logger.info`.** The envelope is this CLI's
246
+ * machine-readable contract — every invocation emits exactly ONE, and a
247
+ * headless caller parses it out of stdout to decide what happened.
248
+ * `Logger.info` is level-gated, so under the documented
249
+ * `AGENT_LOG_LEVEL=silent` (§ 1.H) the envelope silently vanished and the
250
+ * caller got a bare exit code: precisely the "no envelope at all" outcome
251
+ * Story #4543 exists to remove. A contract payload must not be suppressible
252
+ * by a verbosity knob.
253
+ *
254
+ * Single home for the marker format so the four emit sites (the runner's
255
+ * terminal, the close CLI's failed-terminal catch, and both confirm-CLI
256
+ * paths) cannot drift apart.
257
+ *
258
+ * @param {object} envelope
259
+ * @param {{ write?: (s: string) => void }} [opts] `write` is a test seam.
260
+ * @returns {void}
261
+ */
262
+ export function emitTerminalEnvelope(
263
+ envelope,
264
+ { write = (s) => process.stdout.write(s) } = {},
265
+ ) {
266
+ write(
267
+ `\n${TERMINAL_BEGIN_MARKER}\n${JSON.stringify(envelope, null, 2)}\n${TERMINAL_END_MARKER}\n`,
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Map a `runConfirmMergePhase` outcome onto the schema-validated terminal
273
+ * envelope (Story #4543). One writer, one shape — the two prose contracts
274
+ * this replaces disagreed with each other precisely because each surface
275
+ * assembled its own.
276
+ *
277
+ * @returns {object} A validated `story-deliver-terminal` envelope.
278
+ */
279
+ export function terminalFromWaitOutcome({
280
+ waitOutcome,
281
+ storyId,
282
+ storyBranch,
283
+ baseBranch,
284
+ prNumber,
285
+ prUrl,
286
+ autoMergeEnabled,
287
+ gates,
288
+ elapsedSeconds,
289
+ }) {
290
+ const prBase = {
291
+ number: prNumber,
292
+ url: prUrl ?? null,
293
+ autoMergeEnabled: Boolean(autoMergeEnabled),
294
+ };
295
+ const common = {
296
+ storyId,
297
+ storyBranch,
298
+ baseBranch,
299
+ gates,
300
+ elapsedSeconds,
301
+ };
302
+
303
+ if (waitOutcome.terminal === 'landed') {
304
+ return buildTerminalEnvelope({
305
+ ...common,
306
+ status: 'landed',
307
+ phase: 'post-land',
308
+ pr: {
309
+ ...prBase,
310
+ state: 'MERGED',
311
+ // The observed rollup, not an assumed 'success' — a merge can land by
312
+ // admin override or with non-required checks red.
313
+ checksStatus: waitOutcome.prProbe?.checksStatus ?? null,
314
+ },
315
+ tail: waitOutcome.tail,
316
+ nextCommand: null,
317
+ });
318
+ }
319
+
320
+ if (waitOutcome.terminal === 'pending') {
321
+ return buildTerminalEnvelope({
322
+ ...common,
323
+ status: 'pending',
324
+ phase: 'confirm-merge',
325
+ pr: {
326
+ ...prBase,
327
+ state: waitOutcome.prProbe?.state ?? 'OPEN',
328
+ checksStatus: waitOutcome.prProbe?.checksStatus ?? null,
329
+ },
330
+ waitBudget: waitOutcome.waitBudget,
331
+ nextCommand: NEXT_COMMANDS.resumeLand(storyId),
332
+ });
333
+ }
334
+
335
+ // blocked — the classifier already named the class and the friction
336
+ // comment already carries the class-specific remediation, so the next
337
+ // command mirrors it rather than inventing a second opinion.
338
+ const nextCommand =
339
+ waitOutcome.blockClass === 'checks-failed'
340
+ ? NEXT_COMMANDS.watchCi(storyId, prNumber)
341
+ : waitOutcome.blockClass === 'merged-flip-failed'
342
+ ? NEXT_COMMANDS.confirmMerge(storyId)
343
+ : NEXT_COMMANDS.recover(storyId);
344
+ return buildTerminalEnvelope({
345
+ ...common,
346
+ status: 'blocked',
347
+ phase: 'confirm-merge',
348
+ pr: {
349
+ ...prBase,
350
+ state: waitOutcome.prProbe?.state ?? null,
351
+ checksStatus: waitOutcome.prProbe?.checksStatus ?? null,
352
+ },
353
+ blocked: {
354
+ blockClass: waitOutcome.blockClass,
355
+ reason: waitOutcome.reason,
356
+ frictionCommentId: waitOutcome.frictionCommentId ?? null,
357
+ },
358
+ nextCommand,
359
+ });
360
+ }
@@ -72,15 +72,65 @@ export async function gatherStoryFrictionSignals(storyId, config) {
72
72
  return signals;
73
73
  }
74
74
 
75
+ /**
76
+ * Render the empty-roll-up line.
77
+ *
78
+ * Story #4578 — an empty roll-up over a multi-Story run must NOT read as
79
+ * success. The pre-#4578 text ("No friction signals — nothing to follow up")
80
+ * was *truthful* about the stream and *false* about the run: a 7-Story
81
+ * delivery containing a mid-run git outage, a parked worker, and a
82
+ * four-round acceptance critic rendered byte-identically to a genuinely
83
+ * clean run. An operator cannot tell "nothing went wrong" from "the
84
+ * telemetry never fired", and the second is likeliest exactly when the run
85
+ * went worst.
86
+ *
87
+ * So the line is a function of `storyCount`:
88
+ * - `storyCount <= 1` → the honest, quiet reading is retained. A single
89
+ * Story that emitted nothing plausibly *was* clean, and crying wolf on
90
+ * every clean Story is how a warning channel gets tuned out.
91
+ * - `storyCount > 1` → zero signals across N Stories is a **claim**, and
92
+ * the surrounding text says so and names the two readings, rather than
93
+ * asserting the flattering one.
94
+ *
95
+ * This mirrors the sibling precedent in `run-epilogue.js`'s
96
+ * `renderDiffLines`, which refuses to let an unresolvable base diff render
97
+ * as "0 changed files".
98
+ *
99
+ * @param {number} storyCount
100
+ * @returns {string[]}
101
+ */
102
+ function renderEmptyRollupLines(storyCount) {
103
+ if (storyCount <= 1) {
104
+ return ['_No friction signals — nothing to follow up._'];
105
+ }
106
+ return [
107
+ `> ⚠️ **0 friction signals across ${storyCount} Stories — this is a claim, not a clean bill of health.**`,
108
+ '> Either the run was genuinely friction-free, or telemetry never fired.',
109
+ '> An empty stream is indistinguishable from a clean run, and it is least',
110
+ '> likely to fill exactly when a run is going badly and the agent is busy.',
111
+ '> The runtime emits friction from its own observables (`agent::blocked`',
112
+ '> transitions, failed closes, exhausted merge waits) — so zero here also',
113
+ '> means none of those fired. If the run had friction you can name, that',
114
+ '> gap is itself the follow-up worth filing.',
115
+ ];
116
+ }
117
+
75
118
  /**
76
119
  * @param {{
77
120
  * storyId: number,
78
121
  * proposals: object,
79
122
  * graduated: object,
80
- * }} args
123
+ * storyCount?: number,
124
+ * }} args - `storyCount` (default 1) is how many Stories the roll-up spans;
125
+ * it decides whether an empty result reads as quiet or as a flagged claim.
81
126
  * @returns {string}
82
127
  */
83
- export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
128
+ export function buildFollowUpsCommentBody({
129
+ storyId,
130
+ proposals,
131
+ graduated,
132
+ storyCount = 1,
133
+ }) {
84
134
  const filed = Array.isArray(graduated?.filed) ? graduated.filed : [];
85
135
  const framework = proposals?.framework ?? [];
86
136
  const consumer = proposals?.consumer ?? [];
@@ -124,7 +174,7 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
124
174
  consumer.length === 0 &&
125
175
  discarded.length === 0
126
176
  ) {
127
- lines.push('_No friction signals — nothing to follow up._');
177
+ lines.push(...renderEmptyRollupLines(storyCount));
128
178
  lines.push('');
129
179
  }
130
180
  lines.push('```json');
@@ -132,6 +182,7 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
132
182
  JSON.stringify(
133
183
  {
134
184
  storyId,
185
+ storyCount,
135
186
  framework: framework.map((i) => i.category),
136
187
  consumer: consumer.map((i) => i.category),
137
188
  discarded: discarded.map((i) => i.category),
@@ -139,6 +190,15 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
139
190
  category: i.category,
140
191
  url: i.url ?? null,
141
192
  })),
193
+ // Story #4578 — an empty roll-up over N>1 Stories is a claim worth
194
+ // flagging, not a success. Machine-readable twin of the warning
195
+ // prose so a caller need not regex the body.
196
+ emptyRollupSuspect:
197
+ storyCount > 1 &&
198
+ filed.length === 0 &&
199
+ framework.length === 0 &&
200
+ consumer.length === 0 &&
201
+ discarded.length === 0,
142
202
  },
143
203
  null,
144
204
  2,
@@ -149,8 +209,18 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
149
209
  }
150
210
 
151
211
  /**
152
- * Capture and persist Story follow-ups. Never throws — close must not fail
153
- * because follow-up filing flaked.
212
+ * Capture and persist Story follow-ups. Never throws — the land must not
213
+ * fail because follow-up filing flaked.
214
+ *
215
+ * Story #4543 retired the `captureFollowUpsAfterConfirm` action-gate wrapper
216
+ * (and its `withConfirmFollowUps` sibling) that used to front this function.
217
+ * Re-deriving "did the merge land?" from a confirmation envelope's `action`
218
+ * field was the coupling that made close-and-land — the DEFAULT path — skip
219
+ * capture entirely: the gate only opened on the standalone CLI's `done`, and
220
+ * a belated manual confirm could not backfill because the Story was already
221
+ * `agent::done` (confirm returns `noop`, the gate never opens). The shared
222
+ * land tail (`single-story-close/phases/post-land.js`) now calls this
223
+ * directly, after the merge is already confirmed.
154
224
  *
155
225
  * @param {object} args
156
226
  * @param {number} args.storyId
@@ -160,15 +230,6 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
160
230
  * @param {(tag: string, msg: string) => void} [args.progress]
161
231
  * @returns {Promise<object>}
162
232
  */
163
- /**
164
- * Capture follow-ups only when merge confirm landed (`action === 'done'`).
165
- * One-liner seam for the confirm-merge CLI.
166
- */
167
- export async function captureFollowUpsAfterConfirm(confirmation, ctx) {
168
- if (confirmation?.action !== 'done') return null;
169
- return captureStoryFollowUps(ctx);
170
- }
171
-
172
233
  export async function captureStoryFollowUps({
173
234
  storyId,
174
235
  provider,
@@ -1,6 +1,9 @@
1
1
  import { Logger } from '../Logger.js';
2
- import { AGENT_LABELS } from '../label-constants.js';
3
- import { upsertStructuredComment } from './ticketing.js';
2
+ import {
3
+ STATE_LABELS,
4
+ transitionTicketState,
5
+ upsertStructuredComment,
6
+ } from './ticketing.js';
4
7
 
5
8
  /**
6
9
  * Fail closed when the repository remote cannot be verified. The Story is
@@ -30,13 +33,14 @@ export async function handleRemoteVerificationFailure({
30
33
  `[single-story-init] failed to post remote-verification friction: ${err?.message ?? err}`,
31
34
  );
32
35
  }
36
+ // Story #4539 — the canonical mutator. This is the path where the
37
+ // skipped Projects v2 column sync (Story #2548) visibly drifts: the
38
+ // Story is still agent::ready (To Do) when the remote probe fails, so
39
+ // a direct label write leaves the board reading To Do for a blocked
40
+ // Story. single-story-init.js's own comment explains exactly why this
41
+ // must not bypass the mutator.
33
42
  try {
34
- await provider.updateTicket(storyId, {
35
- labels: {
36
- add: [AGENT_LABELS.BLOCKED],
37
- remove: [AGENT_LABELS.READY, AGENT_LABELS.EXECUTING],
38
- },
39
- });
43
+ await transitionTicketState(provider, storyId, STATE_LABELS.BLOCKED, {});
40
44
  } catch (err) {
41
45
  Logger.warn(
42
46
  `[single-story-init] failed to block Story after remote verification: ${err?.message ?? err}`,