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
@@ -2,8 +2,13 @@
2
2
  * summary.js — plan-persist terminal summary (v2 Stage 3).
3
3
  *
4
4
  * Upserts a single `plan-summary` structured comment on the primary Story
5
- * at terminal success. Carries risk / routing receipts and the dry-run
6
- * `depends_on` ordering table for the rare N>1 plan.
5
+ * at terminal success. Carries the persist receipts including whether the
6
+ * operator forced a review stop — and the dry-run `depends_on` ordering table
7
+ * for the rare N>1 plan.
8
+ *
9
+ * Story #4542 removed the risk / review-routing line: no risk level, gate
10
+ * decision, or acceptance disposition is computed at plan time any more, so
11
+ * reporting one here would document a mechanism that does not run.
7
12
  *
8
13
  * @module lib/orchestration/plan-persist/summary
9
14
  */
@@ -69,15 +74,13 @@ function renderWaveTableLines(waveTable) {
69
74
  export function buildPlanSummaryCommentBody({
70
75
  epicId,
71
76
  ticketCount,
72
- planningRisk,
73
- reviewRouting,
77
+ forceReview = false,
74
78
  freshness,
75
79
  healthcheck,
76
80
  waveTable,
77
81
  mode = 'stories',
78
82
  planMetricsLine = null,
79
83
  stories = null,
80
- planRunLabel = null,
81
84
  // legacy unused knobs kept so older test call sites don't crash mid-migration
82
85
  single = null,
83
86
  amend = null,
@@ -101,23 +104,25 @@ export function buildPlanSummaryCommentBody({
101
104
  ? stories.map((s) => `#${s.id} (\`${s.slug}\`)`).join(', ')
102
105
  : `${ticketCount} Story(ies)`;
103
106
 
104
- const waiverLines = planningRisk?.acceptanceWaivedReason
105
- ? [
106
- `- ⚠️ Acceptance disposition auto-waived to \`not-applicable\` — ${planningRisk.acceptanceWaivedReason}`,
107
- ]
107
+ // The only planning-time review gate left (Story #4542): an explicit
108
+ // operator flag, never a value derived from a self-authored risk verdict.
109
+ const reviewLines = forceReview
110
+ ? ['- ⚠️ Review: operator-forced via `--force-review`.']
108
111
  : [];
109
112
 
110
- const runLine = planRunLabel
111
- ? `- Plan-run label: \`${planRunLabel}\` (N>1 deliver with \`/deliver --run <planRunId>\`).`
112
- : '- Plan-run: single Story (default).';
113
+ // The exact command to run — Story #4540. This comment is posted to
114
+ // GitHub on every plan, so it is the operator's primary instruction: it
115
+ // must name real ids, not a batch token that no longer exists.
116
+ const deliverCommand =
117
+ Array.isArray(stories) && stories.length > 0
118
+ ? `/deliver ${stories.map((s) => s.id).join(' ')}`
119
+ : '/deliver <storyId> [<storyId> ...]';
113
120
 
114
121
  return [
115
122
  `### 📋 Plan Summary — Story #${epicId} is \`agent::ready\``,
116
123
  '',
117
124
  `- ${ticketCount} Story ticket(s) persisted: ${storyList}.`,
118
- runLine,
119
- `- Risk: ${planningRisk?.overallLevel ?? 'unknown'} · ${planningRisk?.gateDecision ?? 'unknown'} (review routing: ${reviewRouting?.decision ?? 'unknown'}).`,
120
- ...waiverLines,
125
+ ...reviewLines,
121
126
  freshnessLine,
122
127
  healthcheckLine,
123
128
  ...(typeof planMetricsLine === 'string' && planMetricsLine.length > 0
@@ -128,6 +133,6 @@ export function buildPlanSummaryCommentBody({
128
133
  '',
129
134
  ...renderWaveTableLines(waveTable),
130
135
  '',
131
- '_Deliver with `/deliver <storyId>` (N=1) or `/deliver --run <planRunId>` (N>1)._',
136
+ `_Deliver with \`${deliverCommand}\` — \`/deliver\` resolves the dependency graph from live state, so edges may point at Stories from earlier plan runs._`,
132
137
  ].join('\n');
133
138
  }
@@ -0,0 +1,509 @@
1
+ /**
2
+ * supersede-ops.js — close the `/plan --tickets` source issues that the
3
+ * authored Stories supersede (Story #4535).
4
+ *
5
+ * `plan-context.js` fetches the source issues, emits `sourceTickets[]` on the
6
+ * `/plan` envelope, and (with `--out`) writes that envelope to disk.
7
+ * `resolveSourceTicketIds` below reads the id set back off it, so the normal
8
+ * `/plan --tickets` path needs no flag; an explicit `--source-tickets` still
9
+ * wins when passed, as the override for hand-driven runs (Story #4554).
10
+ * Before that thread existed the ids reached
11
+ * this module *solely* via the hand-passed flag, so a forgotten flag left
12
+ * `sourceTicketIds` empty — the partition below then passed **vacuously** (an
13
+ * empty set trivially partitions), the close phase short-circuited, and the
14
+ * run reported success while every source issue stayed open.
15
+ *
16
+ * Two halves, deliberately separated by the `createIssue` boundary:
17
+ *
18
+ * 1. **`assertSupersedePartition`** — a plan-time, fail-closed check that
19
+ * runs *before* any GitHub write. Mirrors `assertAcceptancePartition`:
20
+ * every id passed to `--tickets` must be claimed by exactly one Story,
21
+ * and no Story may claim an id that was not a source ticket. A partial
22
+ * supersede map is a planning error, not something to paper over at
23
+ * write time.
24
+ * 2. **`closeSupersededTickets`** — the bookkeeping pass that runs *after*
25
+ * the Stories exist. It **never throws**: a throw here would leave the
26
+ * run half-done with Stories already live. An already-closed, deleted,
27
+ * or inaccessible source ticket is a clean skip-and-report, and a
28
+ * partial failure reports which tickets were and were not closed so the
29
+ * operator can finish by hand.
30
+ *
31
+ * Idempotency is keyed off the `superseded-by` structured-comment marker
32
+ * (`upsertStructuredComment`), not a bare `postComment`, so a re-run cannot
33
+ * double-comment.
34
+ *
35
+ * @module lib/orchestration/plan-persist/supersede-ops
36
+ */
37
+
38
+ import { Logger } from '../../Logger.js';
39
+ import { upsertStructuredComment } from '../ticketing.js';
40
+
41
+ /** Structured-comment type marking a source issue as superseded. */
42
+ const SUPERSEDED_BY_COMMENT_TYPE = 'superseded-by';
43
+
44
+ /**
45
+ * GitHub `state_reason` used when closing a superseded source issue.
46
+ *
47
+ * At persist time nothing has shipped — the issue will not be actioned in
48
+ * its own right, so `not_planned` (not `completed`) is the honest reason.
49
+ * The repo history was inconsistent here (#4211 used `completed`, #2870
50
+ * `not_planned`); this constant settles it.
51
+ */
52
+ export const SUPERSEDE_CLOSE_REASON = 'not_planned';
53
+
54
+ /**
55
+ * Coerce one `supersedes[]` entry into `{ id, note }`.
56
+ *
57
+ * Accepts a bare issue number (`4525`), a numeric string (`"4525"`, `"#4525"`),
58
+ * or an object carrying an optional per-supersede note
59
+ * (`{ id: 4525, note: "…" }`). The note is what lets a Story record a
60
+ * *correction* to the source issue's analysis rather than template-only prose.
61
+ *
62
+ * @param {unknown} entry
63
+ * @param {string} slug Story slug, for error reporting.
64
+ * @returns {{ id: number, note: string|null }}
65
+ */
66
+ function normalizeSupersedeEntry(entry, slug) {
67
+ const raw =
68
+ entry !== null && typeof entry === 'object'
69
+ ? (entry.id ?? entry.ticket)
70
+ : entry;
71
+ const id =
72
+ typeof raw === 'string' ? Number(raw.trim().replace(/^#/, '')) : raw;
73
+ if (!Number.isInteger(id) || id <= 0) {
74
+ throw new Error(
75
+ `[plan-persist] Story "${slug}" has an invalid supersedes entry: ` +
76
+ `${JSON.stringify(entry)} — expected a positive issue number or ` +
77
+ '{ id, note }.',
78
+ );
79
+ }
80
+ const note =
81
+ entry !== null &&
82
+ typeof entry === 'object' &&
83
+ typeof entry.note === 'string' &&
84
+ entry.note.trim() !== ''
85
+ ? entry.note.trim()
86
+ : null;
87
+ return { id, note };
88
+ }
89
+
90
+ /**
91
+ * Normalize a plan Story ticket's `supersedes[]` into `{ id, note }[]`.
92
+ * Absent / empty is a no-op returning `[]`.
93
+ *
94
+ * @param {object} ticket
95
+ * @param {string} slug
96
+ * @returns {Array<{ id: number, note: string|null }>}
97
+ */
98
+ export function normalizeSupersedes(ticket, slug) {
99
+ const raw = ticket?.supersedes;
100
+ if (raw === undefined || raw === null) return [];
101
+ if (!Array.isArray(raw)) {
102
+ throw new Error(
103
+ `[plan-persist] Story "${slug}" has a non-array supersedes field — ` +
104
+ 'expected number[] (or { id, note }[]).',
105
+ );
106
+ }
107
+ const normalized = raw.map((entry) => normalizeSupersedeEntry(entry, slug));
108
+ const seen = new Set();
109
+ for (const { id } of normalized) {
110
+ if (seen.has(id)) {
111
+ throw new Error(
112
+ `[plan-persist] Story "${slug}" claims #${id} twice in supersedes[].`,
113
+ );
114
+ }
115
+ seen.add(id);
116
+ }
117
+ return normalized;
118
+ }
119
+
120
+ /**
121
+ * Normalize a source-ticket id list into deduped positive integers.
122
+ *
123
+ * @param {unknown} ids
124
+ * @param {string} [label] Channel name used in the error message, so an
125
+ * envelope-derived failure does not blame the `--source-tickets` flag.
126
+ * @returns {number[]}
127
+ */
128
+ export function normalizeSourceTicketIds(ids, label = '--source-tickets') {
129
+ if (ids === undefined || ids === null) return [];
130
+ const list = Array.isArray(ids)
131
+ ? ids
132
+ : String(ids)
133
+ .split(',')
134
+ .map((token) => token.trim())
135
+ .filter((token) => token !== '');
136
+ const out = [];
137
+ for (const entry of list) {
138
+ const id =
139
+ typeof entry === 'string' ? Number(entry.replace(/^#/, '')) : entry;
140
+ if (!Number.isInteger(id) || id <= 0) {
141
+ throw new Error(
142
+ `[plan-persist] ${label} expects positive issue ids; got ${JSON.stringify(entry)}.`,
143
+ );
144
+ }
145
+ if (!out.includes(id)) out.push(id);
146
+ }
147
+ return out;
148
+ }
149
+
150
+ /**
151
+ * Pull the `--tickets` id set out of a `plan-context.js` envelope.
152
+ *
153
+ * The envelope's `sourceTickets[]` carries whole ticket records
154
+ * (`{ id, title, body, … }`); only the ids matter here. A non-`tickets`-mode
155
+ * envelope (seed / seed-file) has no source tickets and yields `[]`.
156
+ *
157
+ * @param {object|null} envelope
158
+ * @returns {number[]}
159
+ */
160
+ export function extractEnvelopeSourceTicketIds(envelope) {
161
+ const raw = envelope?.sourceTickets;
162
+ if (!Array.isArray(raw) || raw.length === 0) return [];
163
+ return normalizeSourceTicketIds(
164
+ raw.map((ticket) =>
165
+ ticket !== null && typeof ticket === 'object' ? ticket.id : ticket,
166
+ ),
167
+ 'plan-context envelope sourceTickets[]',
168
+ );
169
+ }
170
+
171
+ function sameIdSet(a, b) {
172
+ return a.length === b.length && a.every((id) => b.includes(id));
173
+ }
174
+
175
+ /**
176
+ * Resolve which source-ticket ids reach the partition and the close phase.
177
+ *
178
+ * Precedence — an explicit flag wins, the envelope is the default channel
179
+ * (Story #4554):
180
+ *
181
+ * 1. `--source-tickets` when supplied — the explicit override for
182
+ * hand-driven runs. A disagreement with the envelope is warned about
183
+ * loudly (the operator is overriding what the run actually fetched)
184
+ * but honoured.
185
+ * 2. otherwise the envelope's `sourceTickets[]` — so the common
186
+ * `/plan --tickets` path needs no flag at all.
187
+ * 3. otherwise empty, reported as `origin: 'none'`.
188
+ *
189
+ * `origin` is surfaced on the persist envelope so a run that superseded
190
+ * nothing says *why* rather than looking like a clean no-op.
191
+ *
192
+ * @param {object} [args]
193
+ * @param {unknown} [args.explicitIds] Raw `--source-tickets` value.
194
+ * @param {object|null} [args.envelope] Parsed `plan-context.js` envelope.
195
+ * @returns {{ ids: number[], origin: 'flag'|'envelope'|'none' }}
196
+ */
197
+ export function resolveSourceTicketIds({
198
+ explicitIds = null,
199
+ envelope = null,
200
+ } = {}) {
201
+ const explicit = normalizeSourceTicketIds(explicitIds);
202
+ const derived = extractEnvelopeSourceTicketIds(envelope);
203
+
204
+ if (explicit.length > 0) {
205
+ if (derived.length > 0 && !sameIdSet(explicit, derived)) {
206
+ Logger.warn(
207
+ '[plan-persist] --source-tickets ' +
208
+ `(${explicit.map((id) => `#${id}`).join(', ')}) disagrees with the ` +
209
+ 'plan-context envelope ' +
210
+ `(${derived.map((id) => `#${id}`).join(', ')}) — honouring the ` +
211
+ 'explicit flag. Drop --source-tickets to use the envelope.',
212
+ );
213
+ }
214
+ return { ids: explicit, origin: 'flag' };
215
+ }
216
+
217
+ if (derived.length > 0) {
218
+ Logger.info(
219
+ `[plan-persist] derived ${derived.length} source ticket(s) from the ` +
220
+ `plan-context envelope: ${derived.map((id) => `#${id}`).join(', ')}.`,
221
+ );
222
+ return { ids: derived, origin: 'envelope' };
223
+ }
224
+
225
+ return { ids: [], origin: 'none' };
226
+ }
227
+
228
+ function describeStoryIds(entries) {
229
+ return entries.map((e) => `"${e}"`).join(', ');
230
+ }
231
+
232
+ /**
233
+ * Fail closed on a partial supersede map.
234
+ *
235
+ * Runs **before** `createIssue` so a mis-authored map never leaves Stories
236
+ * live against an inconsistent tracker.
237
+ *
238
+ * @param {Array<{ slug: string, supersedes: Array<{ id: number }> }>} stories
239
+ * @param {number[]} sourceTicketIds Ids passed to `/plan --tickets`.
240
+ */
241
+ export function assertSupersedePartition(stories, sourceTicketIds = []) {
242
+ const list = Array.isArray(stories) ? stories : [];
243
+ const sources = new Set(sourceTicketIds);
244
+
245
+ /** @type {Map<number, string[]>} */
246
+ const claims = new Map();
247
+ for (const story of list) {
248
+ for (const { id } of story.supersedes ?? []) {
249
+ const owners = claims.get(id) ?? [];
250
+ owners.push(story.slug);
251
+ claims.set(id, owners);
252
+ }
253
+ }
254
+
255
+ const errors = [];
256
+
257
+ for (const [id, owners] of claims) {
258
+ if (owners.length > 1) {
259
+ errors.push(
260
+ `source ticket #${id} is claimed by ${owners.length} Stories ` +
261
+ `(${describeStoryIds(owners)}) — exactly one Story must own it.`,
262
+ );
263
+ }
264
+ if (!sources.has(id)) {
265
+ errors.push(
266
+ `Story "${owners[0]}" supersedes #${id}, which was not passed to ` +
267
+ '--tickets. Only source tickets may be superseded.',
268
+ );
269
+ }
270
+ }
271
+
272
+ for (const id of sources) {
273
+ if (!claims.has(id)) {
274
+ errors.push(
275
+ `source ticket #${id} is not claimed by any Story's supersedes[] — ` +
276
+ 'a partial supersede map is a planning error. Claim it, or drop it ' +
277
+ 'from --tickets.',
278
+ );
279
+ }
280
+ }
281
+
282
+ if (errors.length > 0) {
283
+ throw new Error(
284
+ `[plan-persist] supersede partition failed with ${errors.length} ` +
285
+ `error(s):\n${errors.map((e) => ` - ${e}`).join('\n')}`,
286
+ );
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Render the supersede comment posted on a source issue.
292
+ *
293
+ * @param {object} args
294
+ * @param {{ id: number, title: string }} args.story The claiming Story.
295
+ * @param {string|null} [args.note] Optional per-supersede note authored on
296
+ * the Story — carries a correction to this issue's analysis.
297
+ * @param {number[]} args.sourceTicketIds Full `--tickets` argument.
298
+ * @returns {string}
299
+ */
300
+ export function buildSupersedeCommentBody({
301
+ story,
302
+ note = null,
303
+ sourceTicketIds,
304
+ }) {
305
+ // Story #4540 retired the plan-run label, which used to be listed here
306
+ // alongside the type/state labels.
307
+ const labels = ['`type::story`', '`agent::ready`'];
308
+
309
+ const lines = [
310
+ `**Superseded by #${story.id}** — *${story.title}* (${labels.join(', ')}).`,
311
+ '',
312
+ `Planned via \`/plan --tickets ${sourceTicketIds.join(',')}\`.`,
313
+ ];
314
+ if (note) {
315
+ lines.push('', note);
316
+ }
317
+ lines.push(
318
+ '',
319
+ 'The analysis in this issue is preserved as the historical record; ' +
320
+ `#${story.id} carries the delivery contract.`,
321
+ );
322
+ return lines.join('\n');
323
+ }
324
+
325
+ /**
326
+ * Resolve the live state of a source ticket.
327
+ *
328
+ * @returns {Promise<{ ok: true, state: string } | { ok: false, reason: string }>}
329
+ */
330
+ async function probeSourceTicket(provider, id) {
331
+ try {
332
+ const ticket = await provider.getTicket(id, { fresh: true });
333
+ if (!ticket) return { ok: false, reason: 'not-found' };
334
+ return { ok: true, state: String(ticket.state ?? 'open').toLowerCase() };
335
+ } catch (err) {
336
+ return { ok: false, reason: `inaccessible: ${err.message}` };
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Comment on and close one source ticket. Never throws.
342
+ *
343
+ * @returns {Promise<{ outcome: 'closed'|'skipped'|'failed', reason?: string }>}
344
+ */
345
+ async function closeOneSupersededTicket({
346
+ provider,
347
+ id,
348
+ note,
349
+ story,
350
+ sourceTicketIds,
351
+ }) {
352
+ const probe = await probeSourceTicket(provider, id);
353
+ if (!probe.ok) return { outcome: 'skipped', reason: probe.reason };
354
+ if (probe.state === 'closed') {
355
+ return { outcome: 'skipped', reason: 'already-closed' };
356
+ }
357
+
358
+ try {
359
+ await upsertStructuredComment(
360
+ provider,
361
+ id,
362
+ SUPERSEDED_BY_COMMENT_TYPE,
363
+ buildSupersedeCommentBody({
364
+ story,
365
+ note,
366
+ sourceTicketIds,
367
+ }),
368
+ );
369
+ await provider.updateTicket(id, {
370
+ state: 'closed',
371
+ state_reason: SUPERSEDE_CLOSE_REASON,
372
+ });
373
+ return { outcome: 'closed' };
374
+ } catch (err) {
375
+ return { outcome: 'failed', reason: err.message };
376
+ }
377
+ }
378
+
379
+ /**
380
+ * @typedef {object} SupersedeReport
381
+ * @property {boolean} enabled
382
+ * @property {boolean} dryRun
383
+ * @property {string|null} reason Why the phase was skipped wholesale.
384
+ * @property {'flag'|'envelope'|'none'} [sourceTicketOrigin] Which channel the
385
+ * source ids came from. Stamped by `runPlanPersist`, not this module.
386
+ * @property {number[]} closed
387
+ * @property {Array<{ ticket: number, storySlug: string }>} planned Dry-run
388
+ * only. Keyed by slug, not id: under `--dry-run` no issue was created, so
389
+ * `createStoryIssues` hands back a synthetic negative placeholder id. The
390
+ * slug is the only identifier that means anything before the writes land.
391
+ * @property {Array<{ ticket: number, reason: string }>} skipped
392
+ * @property {Array<{ ticket: number, reason: string }>} failed
393
+ */
394
+
395
+ function emptyReport(overrides) {
396
+ return {
397
+ enabled: false,
398
+ dryRun: false,
399
+ reason: null,
400
+ closed: [],
401
+ planned: [],
402
+ skipped: [],
403
+ failed: [],
404
+ ...overrides,
405
+ };
406
+ }
407
+
408
+ /**
409
+ * Comment on and close every superseded source ticket.
410
+ *
411
+ * **Never throws** and never fails the run — Stories are already live by the
412
+ * time this executes, so bookkeeping failures degrade to a report the
413
+ * operator can act on.
414
+ *
415
+ * @param {object} args
416
+ * @param {object} args.provider
417
+ * @param {Array<{ slug: string, supersedes: Array<{ id: number, note: string|null }> }>} args.stories
418
+ * @param {Array<{ slug: string, id: number, title: string }>} args.created
419
+ * @param {number[]} args.sourceTicketIds
420
+ * @param {boolean} [args.dryRun=false]
421
+ * @param {boolean} [args.closeSuperseded=true]
422
+ * @returns {Promise<SupersedeReport>}
423
+ */
424
+ export async function closeSupersededTickets({
425
+ provider,
426
+ stories,
427
+ created,
428
+ sourceTicketIds,
429
+ dryRun = false,
430
+ closeSuperseded = true,
431
+ }) {
432
+ const sources = Array.isArray(sourceTicketIds) ? sourceTicketIds : [];
433
+ if (sources.length === 0) {
434
+ return emptyReport({ reason: 'no-source-tickets' });
435
+ }
436
+ if (!closeSuperseded) {
437
+ return emptyReport({ reason: 'disabled-by-flag' });
438
+ }
439
+
440
+ const createdBySlug = new Map(
441
+ (created ?? []).map((story) => [story.slug, story]),
442
+ );
443
+
444
+ const report = emptyReport({ enabled: true, dryRun });
445
+
446
+ for (const story of stories ?? []) {
447
+ const createdStory = createdBySlug.get(story.slug);
448
+ for (const { id, note } of story.supersedes ?? []) {
449
+ if (!createdStory) {
450
+ report.skipped.push({ ticket: id, reason: 'story-not-created' });
451
+ continue;
452
+ }
453
+ if (dryRun) {
454
+ report.planned.push({ ticket: id, storySlug: createdStory.slug });
455
+ continue;
456
+ }
457
+ const result = await closeOneSupersededTicket({
458
+ provider,
459
+ id,
460
+ note,
461
+ story: createdStory,
462
+ sourceTicketIds: sources,
463
+ });
464
+ if (result.outcome === 'closed') {
465
+ report.closed.push(id);
466
+ } else if (result.outcome === 'skipped') {
467
+ report.skipped.push({ ticket: id, reason: result.reason });
468
+ } else {
469
+ report.failed.push({ ticket: id, reason: result.reason });
470
+ }
471
+ }
472
+ }
473
+
474
+ logSupersedeReport(report);
475
+ return report;
476
+ }
477
+
478
+ /**
479
+ * Surface the supersede outcome on the console so a partial failure is
480
+ * visible without reading the JSON envelope.
481
+ *
482
+ * @param {SupersedeReport} report
483
+ */
484
+ function logSupersedeReport(report) {
485
+ if (report.dryRun) {
486
+ for (const { ticket, storySlug } of report.planned) {
487
+ Logger.info(
488
+ `[plan-persist] dry-run: would comment on and close #${ticket} ` +
489
+ `as superseded by Story "${storySlug}" (${SUPERSEDE_CLOSE_REASON}).`,
490
+ );
491
+ }
492
+ return;
493
+ }
494
+ if (report.closed.length > 0) {
495
+ Logger.info(
496
+ `[plan-persist] closed ${report.closed.length} superseded source ` +
497
+ `ticket(s): ${report.closed.map((id) => `#${id}`).join(', ')}.`,
498
+ );
499
+ }
500
+ for (const { ticket, reason } of report.skipped) {
501
+ Logger.info(`[plan-persist] skipped source ticket #${ticket}: ${reason}.`);
502
+ }
503
+ for (const { ticket, reason } of report.failed) {
504
+ Logger.warn(
505
+ `[plan-persist] could NOT close source ticket #${ticket}: ${reason} — ` +
506
+ 'close it by hand.',
507
+ );
508
+ }
509
+ }