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
@@ -3,7 +3,10 @@ import { detectCycle } from '../Graph.js';
3
3
  import { gitSpawn } from '../git-utils.js';
4
4
 
5
5
  import { Logger } from '../Logger.js';
6
- import { parse as parseStoryBody } from '../story-body/story-body.js';
6
+ import {
7
+ parse as parseStoryBody,
8
+ StoryBodyParseError,
9
+ } from '../story-body/story-body.js';
7
10
  import { validateStoryFileAssumptions } from './file-assumptions.js';
8
11
  import {
9
12
  computeConflictFindings,
@@ -43,6 +46,95 @@ function collectPathsFromText(text, paths) {
43
46
  }
44
47
  }
45
48
 
49
+ /**
50
+ * Parse a Story's serialized markdown body, translating a
51
+ * `StoryBodyParseError` into a `ValidationError` that names the offending
52
+ * **section** and **entry** (Story #4541).
53
+ *
54
+ * `StoryBodyParseError` already carries `field` (the section the parser was
55
+ * reading) and `raw` (the entry text that failed); this lifts both into an
56
+ * operator-legible message and a structured `violation` payload so an
57
+ * authoring loop can point at the exact bullet instead of re-deriving it
58
+ * from a downstream freshness miss.
59
+ *
60
+ * @param {object} story Story whose `body` is a non-empty markdown string.
61
+ * @returns {object} The structured body.
62
+ * @throws {ValidationError} `code: 'story-body-unparseable'`.
63
+ */
64
+ function parseStoryBodyOrThrow(story) {
65
+ try {
66
+ return parseStoryBody(story.body).body;
67
+ } catch (err) {
68
+ if (!(err instanceof StoryBodyParseError)) throw err;
69
+ const slug = story.slug ?? '<unknown>';
70
+ const section = err.field ?? 'body';
71
+ const entry = err.raw ?? null;
72
+ const entryLine = entry === null ? '' : `\n entry: ${entry}`;
73
+ const violation = { slug, section, entry, reason: err.message };
74
+ const error = new ValidationError(
75
+ `Cross-Validation Failed: Story "${slug}" has an unparseable body — ` +
76
+ `the ## ${section} section could not be read: ${err.message}` +
77
+ `${entryLine}\n\nFix the offending entry; this is a malformed body, ` +
78
+ 'not a stale path reference.',
79
+ { violations: [violation] },
80
+ );
81
+ error.code = 'story-body-unparseable';
82
+ error.violations = [violation];
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Refuse the plan when any Story's serialized body cannot be parsed, before
89
+ * either git-probe gate runs (Story #4541). Ordering matters: the freshness
90
+ * gate consults `body.changes` for its net-new whitelist, so an unparseable
91
+ * body used to reach the operator as a freshness miss naming declared paths.
92
+ *
93
+ * @param {{ tickets: object[] }} opts
94
+ * @throws {ValidationError} `code: 'story-body-unparseable'` on the first
95
+ * offending Story.
96
+ */
97
+ function assertStoryBodiesParse({ tickets }) {
98
+ for (const story of (tickets ?? []).filter((t) => t.type === 'story')) {
99
+ if (typeof story.body !== 'string' || story.body.trim().length === 0) {
100
+ continue;
101
+ }
102
+ parseStoryBodyOrThrow(story);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Resolve every acceptance line a Story declares, across both authoring
108
+ * shapes (Story #4541).
109
+ *
110
+ * The canonical shape is a **serialized string body** with the criteria at
111
+ * the ticket's **top level** — the machine contract persist syncs into the
112
+ * body. `validateAcceptanceSubjectPrefix` used to read `body.acceptance` on
113
+ * an object body only, so on every real plan it scanned nothing and the gate
114
+ * silently passed. Union both sources (deduplicated) so the gate fires on
115
+ * whichever surface the author used.
116
+ *
117
+ * @param {object} story
118
+ * @returns {string[]}
119
+ */
120
+ function resolveAcceptanceLines(story) {
121
+ const lines = new Set();
122
+ if (Array.isArray(story?.acceptance)) {
123
+ for (const item of story.acceptance) lines.add(String(item ?? ''));
124
+ }
125
+ const body = story?.body;
126
+ let bodyAcceptance = null;
127
+ if (typeof body === 'string' && body.trim().length > 0) {
128
+ bodyAcceptance = parseStoryBodyOrThrow(story).acceptance;
129
+ } else if (body !== null && typeof body === 'object') {
130
+ bodyAcceptance = body.acceptance;
131
+ }
132
+ if (Array.isArray(bodyAcceptance)) {
133
+ for (const item of bodyAcceptance) lines.add(String(item ?? ''));
134
+ }
135
+ return [...lines];
136
+ }
137
+
46
138
  function collectTaskPathReferences(task) {
47
139
  const paths = new Set();
48
140
  const body = task.body;
@@ -82,7 +174,7 @@ function collectTaskPathReferences(task) {
82
174
  * path out of the prose.
83
175
  * 3. **Object form** — `{ path: "<path>", assumption: "creates" | ... }`,
84
176
  * introduced by Story #2636 as the canonical declaration shape and
85
- * documented in `epic-plan-decompose-author/SKILL.md`. The path is
177
+ * documented in `lib/templates/decomposer-prompts.js`. The path is
86
178
  * trusted verbatim.
87
179
  *
88
180
  * Only `body.changes` (and `body.references`) is consulted —
@@ -100,15 +192,16 @@ function collectTaskChangesPaths(task) {
100
192
  // arrays before scanning. Without this, a string body causes the
101
193
  // object-form branch below to fall through on every item, leaving the
102
194
  // freshness gate blind to declared paths.
195
+ //
196
+ // Story #4541: a parse failure is NOT swallowed here. Swallowing it
197
+ // returned an empty whitelist, so a single malformed `## Changes` entry
198
+ // surfaced downstream as "files do not exist at main" naming the very
199
+ // paths the Story *had* declared — a misdiagnosis that cost two authoring
200
+ // round-trips. `assertStoryBodiesParse` runs before the freshness gate and
201
+ // owns that failure with a named error; the throw here is the same error
202
+ // for any caller that drives `validateAcFreshness` directly.
103
203
  if (typeof body === 'string' && body.trim().length > 0) {
104
- let parsed;
105
- try {
106
- parsed = parseStoryBody(body).body;
107
- } catch {
108
- // Unparseable body — no paths to whitelist; the freshness gate will
109
- // catch any real references in the text scan below.
110
- return paths;
111
- }
204
+ const parsed = parseStoryBodyOrThrow(task);
112
205
  for (const arrName of ['changes', 'references']) {
113
206
  const arr = parsed[arrName];
114
207
  if (!Array.isArray(arr)) continue;
@@ -307,11 +400,16 @@ const SUBJECT_PREFIX_RE = /Commit subject begins with ['"`]([^'"`]+):['"`]/g;
307
400
  * the form `baseline-refresh` is rejected because no Conventional-Commits
308
401
  * type starts with that token.
309
402
  *
310
- * Only `body.acceptance[]` is scanned; `body.goal` / `body.verify` /
403
+ * Only acceptance criteria are scanned; `body.goal` / `body.verify` /
311
404
  * `body.changes` are not commit-subject prescriptions by convention and
312
405
  * scanning them would surface false positives from prose that happens to
313
406
  * quote a forbidden prefix while explaining why it's forbidden.
314
407
  *
408
+ * Both authoring shapes are covered (Story #4541): the canonical top-level
409
+ * `acceptance[]` on a serialized string body, and the pre-serialize
410
+ * `body.acceptance[]` object shape. Scanning only the latter made the gate
411
+ * inert on every real plan.
412
+ *
315
413
  * @param {object} opts
316
414
  * @param {object[]} opts.tickets - Validated ticket hierarchy.
317
415
  * @throws {ValidationError} when one or more Story acceptance items
@@ -324,11 +422,7 @@ export function validateAcceptanceSubjectPrefix({ tickets }) {
324
422
  const violations = [];
325
423
  const stories = (tickets ?? []).filter((t) => t.type === 'story');
326
424
  for (const story of stories) {
327
- const body = story.body;
328
- if (body === null || typeof body !== 'object') continue;
329
- if (!Array.isArray(body.acceptance)) continue;
330
- for (const item of body.acceptance) {
331
- const line = String(item ?? '');
425
+ for (const line of resolveAcceptanceLines(story)) {
332
426
  // Reset the global regex between iterations.
333
427
  SUBJECT_PREFIX_RE.lastIndex = 0;
334
428
  let match = SUBJECT_PREFIX_RE.exec(line);
@@ -381,8 +475,8 @@ function renderMissLine({ slug, path }) {
381
475
  * The returned tickets array carries two extra non-array properties:
382
476
  * - `findings` — structured sizing findings (hard + soft) keyed by the
383
477
  * three-layer sizing model. The bounded re-decomposition loop in
384
- * `epic-plan-decompose` reads `findings.filter(f => f.severity === 'hard')`
385
- * to decide whether to re-prompt.
478
+ * `/plan` reads `findings.filter(f => f.severity === 'hard')` to decide
479
+ * whether to re-prompt.
386
480
  * - `errors` — human-readable strings, one per hard finding. Non-empty
387
481
  * `errors[]` is the AC-visible "block normalization" signal; the legacy
388
482
  * hierarchy/cycle/freshness checks continue to throw, so callers that
@@ -540,6 +634,14 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
540
634
 
541
635
  assertAcyclic(slugAdjacency);
542
636
 
637
+ // Story #4541 — refuse an unparseable Story body up front, with a named
638
+ // error pointing at the offending section + entry. Must precede both the
639
+ // subject-prefix scan and the freshness gate: each parses the body, and
640
+ // the freshness gate's net-new whitelist comes from `body.changes`, so a
641
+ // malformed body used to surface as a stale-path miss naming the paths the
642
+ // Story had legitimately declared.
643
+ assertStoryBodiesParse({ tickets });
644
+
543
645
  // Reject any Task acceptance item that prescribes a non-Conventional-Commits
544
646
  // subject prefix (e.g. legacy "Commit subject begins with 'baseline-refresh:'"
545
647
  // from pre-Epic-#2501 planner output). Runs before the freshness gate so
@@ -648,6 +750,7 @@ export function validateAndNormalizeTickets(tickets, opts = {}) {
648
750
 
649
751
  // Internal helpers exposed for unit tests; not part of the public surface.
650
752
  export const _internal = {
753
+ assertStoryBodiesParse,
651
754
  indexTicketsBySlug,
652
755
  assertAllTicketsAreStories,
653
756
  assertEveryStoryHasInlineContract,
@@ -357,7 +357,7 @@ async function processCascadeParentLocked(
357
357
  * unset so the module-level {@link Logger} is used.
358
358
  * @returns {Promise<{ cascadedTo: number[], failed: Array<{ parentId: number, error: string }> }>}
359
359
  */
360
- export async function cascadeCompletion(provider, ticketId, opts = {}) {
360
+ async function cascadeCompletion(provider, ticketId, opts = {}) {
361
361
  const ticket = await provider.getTicket(ticketId);
362
362
 
363
363
  // Determine if this ticket is agent::done
@@ -365,43 +365,14 @@ export async function cascadeCompletion(provider, ticketId, opts = {}) {
365
365
  return { cascadedTo: [], failed: [] };
366
366
  }
367
367
 
368
+ // Story #4545 — one strategy, not three. The `parent: #N` body footer and
369
+ // the native sub-issue link were both written by `createTicket`, the
370
+ // Epic-hierarchy write surface deleted in the same Story; with no writer,
371
+ // the body regex could only ever miss and the native lookup could only
372
+ // ever spend an API call to learn the same. The operator-settable `blocks`
373
+ // annotation is the one parent edge that can still exist.
368
374
  const { blocks: parentIds } = await provider.getTicketDependencies(ticketId);
369
-
370
- // Fallback: parse `parent: #NNN` from the body when `blocks` syntax isn't used (C-5).
371
- let parsedParents = parentIds;
372
- if (!parsedParents || parsedParents.length === 0) {
373
- const parentMatch = ticket.body
374
- ? [...ticket.body.matchAll(/parent:\s*#(\d+)/gi)]
375
- : [];
376
- parsedParents = parentMatch.map((m) => Number.parseInt(m[1], 10));
377
- }
378
-
379
- // Story #2982 — third fallback: GitHub's native Sub-Issues API. The
380
- // resume reconciler can strip the `parent: #N` orchestrator footer
381
- // from a Story body (see Issue 2 in #2982); without the body marker
382
- // the cascade silently returned `{ cascadedTo: [], failed: [] }` and
383
- // left intermediate parent tickets stranded OPEN. The native link is
384
- // independent of body text, so consult it when the first two
385
- // strategies came back empty.
386
- if (
387
- parsedParents.length === 0 &&
388
- typeof provider._getNativeParent === 'function' &&
389
- ticket.nodeId
390
- ) {
391
- try {
392
- const nativeParent = await provider._getNativeParent(
393
- ticket.nodeId,
394
- ticketId,
395
- );
396
- if (typeof nativeParent === 'number') {
397
- parsedParents = [nativeParent];
398
- }
399
- } catch (err) {
400
- Logger.warn(
401
- `[cascadeCompletion] native parent lookup failed for #${ticketId}: ${err.message}`,
402
- );
403
- }
404
- }
375
+ const parsedParents = Array.isArray(parentIds) ? parentIds : [];
405
376
 
406
377
  if (parsedParents.length === 0) {
407
378
  return { cascadedTo: [], failed: [] };
@@ -537,22 +508,18 @@ export async function cascadeParentState(provider, ticketId, opts = {}) {
537
508
  }
538
509
 
539
510
  /**
540
- * Resolve the parent issue ids for a ticket: native `blocks:` dependency
541
- * annotations first, then `parent: #NNN` body references as a fallback.
542
- * Mirrors the resolution path used by {@link cascadeCompletion}.
511
+ * Resolve the parent issue ids for a ticket from its `blocks:` dependency
512
+ * annotations. Mirrors the resolution path used by {@link cascadeCompletion}
513
+ * see there for why the `parent: #NNN` body fallback is gone (Story #4545).
543
514
  *
544
515
  * @param {import('../../ITicketingProvider.js').ITicketingProvider} provider
545
- * @param {object} ticket
516
+ * @param {object} _ticket
546
517
  * @param {number} ticketId
547
518
  * @returns {Promise<number[]>}
548
519
  */
549
- async function resolveParentIds(provider, ticket, ticketId) {
520
+ async function resolveParentIds(provider, _ticket, ticketId) {
550
521
  const { blocks: parentIds } = await provider.getTicketDependencies(ticketId);
551
- if (Array.isArray(parentIds) && parentIds.length > 0) return parentIds;
552
- const parentMatch = ticket?.body
553
- ? [...ticket.body.matchAll(/parent:\s*#(\d+)/gi)]
554
- : [];
555
- return parentMatch.map((m) => Number.parseInt(m[1], 10));
522
+ return Array.isArray(parentIds) ? parentIds : [];
556
523
  }
557
524
 
558
525
  /**
@@ -82,27 +82,13 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
82
82
  'epic-run-progress',
83
83
  'epic-plan-state',
84
84
  'parked-follow-ons',
85
- // Story #566 — per-phase wall-clock summary posted by single-story-close.js
86
- // and consumed by analyze-execution / perf tooling to surface median /
87
- // p95 phase timings across completed stories.
85
+ // Story #566 — per-phase wall-clock summary posted by single-story-close.js.
88
86
  'phase-timings',
89
87
  // Story #831 — story-init upserts a `story-init` comment that
90
88
  // surfaces `dependenciesInstalled` (and the underlying installStatus) so
91
89
  // downstream workflow steps don't have to infer install state from
92
90
  // node_modules presence.
93
91
  'story-init',
94
- // Story #908 — /deliver upserts a `story-run-progress` snapshot
95
- // on each Story per Task transition. The /deliver aggregator and
96
- // `analyze-execution.js` both read this comment to derive
97
- // Story-level state without re-fetching ticket labels.
98
- 'story-run-progress',
99
- // Story #1123 — analyze-execution.js upserts perf summaries at close
100
- // time. Story-mode posts `story-perf-summary` on each Story; Epic-mode
101
- // posts `epic-perf-report` on the Epic. Both replace the legacy
102
- // per-Task `friction` fan-out and the standalone `phase-timings`
103
- // surface (Epic #1030).
104
- 'story-perf-summary',
105
- 'epic-perf-report',
106
92
  // Story #2128 — Phase 6 Epic Clarity Gate (CLI retired). Historical
107
93
  // `clarity-gate-update` comments may still exist on older tickets.
108
94
  'clarity-gate-update',
@@ -151,19 +137,13 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
151
137
  // the documented remediation actually executable
152
138
  // (assertValidStructuredCommentType would otherwise throw).
153
139
  'wave-stall',
154
- // Story #3873 (Epic #3865) — `plan-persist.js` upserts a `risk-verdict`
155
- // comment on the Epic at persist time, recording the planner-authored,
156
- // schema-validated risk verdict and the planningRisk envelope derived
157
- // from it (`deriveRiskEnvelope`). One entry per Epic; re-plans upsert in
158
- // place. Schema: `.agents/schemas/risk-verdict.schema.json`.
159
- 'risk-verdict',
160
- // Story #4019 `epic-plan-lease-guard.js` upserts a `plan-lease`
161
- // comment on the Epic at lease-acquire time, recording the claiming
162
- // operator and the claim timestamp. `/plan` emits no
163
- // `story.heartbeat`, so this claim-time is the liveness signal that
164
- // makes the documented `--steal` contract decidable: a foreign claim
165
- // older than the lease TTL is reclaimed automatically; a fresh one
166
- // refuses with the claim age. One entry per Epic; re-acquires upsert
140
+ // Story #4019 the plan-tier lease guard upserts a `plan-lease`
141
+ // comment at lease-acquire time, recording the claiming operator and the
142
+ // claim timestamp. Nothing emits a per-run liveness beat, so this
143
+ // claim-time is the only age signal a reader has when reasoning about the
144
+ // documented `--steal` contract. The guard itself fails closed regardless
145
+ // (it anchors liveness to `now`), so a stranded claim is cleared with
146
+ // `--steal`, not by TTL expiry. One entry per ticket; re-acquires upsert
167
147
  // in place.
168
148
  'plan-lease',
169
149
  // Story #4415 (Epic #4406) — the feedback-loop graduators
@@ -183,6 +163,13 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
183
163
  // v2 Stage 3 — flat Story persist checkpoint on every created Story
184
164
  // (replaces epic-plan-state for new plans). plan-summary stays primary-only.
185
165
  'story-plan-state',
166
+ // Story #4535 — `plan-persist.js` upserts a `superseded-by` comment on
167
+ // each `/plan --tickets` source issue at persist time, naming the single
168
+ // Story that claims it (plus any per-supersede note the plan authored),
169
+ // immediately before closing the issue as `not_planned`. Keying off this
170
+ // marker rather than a bare `postComment` is what makes a re-run
171
+ // non-double-commenting. One entry per source issue.
172
+ 'superseded-by',
186
173
  ]);
187
174
 
188
175
  export const WAVE_TYPE_PATTERN = WAVE_MARKER_RE;
@@ -268,8 +255,8 @@ export function structuredCommentMarker(type, attrs = null) {
268
255
  * recent `findStructuredComment` / `upsertStructuredComment` call.
269
256
  * Story #1795 — every Epic run owns its process for the duration of
270
257
  * the run, so a single seed-then-reuse window saves one
271
- * `getTicketComments` per repeat upsert on the hot path (wave-level
272
- * `story-run-progress`, `wave-N-end`, etc).
258
+ * `getTicketComments` per repeat upsert on the hot path (`story-init`,
259
+ * `verification-results`, etc).
273
260
  *
274
261
  * Lifecycle:
275
262
  * - First call to `findStructuredComment(provider, t, type, attrs)`
@@ -355,8 +342,8 @@ export function structuredCommentCacheKey(ticketId, type, attrs) {
355
342
  * array returned by the most recent `provider.getTicketComments(ticketId)`
356
343
  * call. Story #2465 — `findStructuredComment` is invoked back-to-back for
357
344
  * different `type` discriminators against the same ticket (e.g.
358
- * `story-run-progress` + `epic-run-progress` + a `friction` probe during
359
- * an Epic-close wave). Without this cache each lookup pays a full
345
+ * `story-init` + `verification-results` + a `friction` probe during a
346
+ * single close). Without this cache each lookup pays a full
360
347
  * pagination round-trip even when the prior call already fetched the
361
348
  * same comments. The structured-comment-id cache short-circuits *repeat*
362
349
  * lookups for the same `(type, attrs)` tuple but does not help across
@@ -37,6 +37,10 @@ import {
37
37
  eventSeverity,
38
38
  renderTransitionMessage,
39
39
  } from '../../notifications/notifier.js';
40
+ import {
41
+ emitRuntimeFriction,
42
+ RUNTIME_FRICTION_CATEGORIES,
43
+ } from '../../observability/runtime-friction.js';
40
44
  import { ColumnSync } from '../column-sync.js';
41
45
  import {
42
46
  ALL_STATES,
@@ -279,6 +283,46 @@ function dispatchTransitionNotification(args) {
279
283
  });
280
284
  }
281
285
 
286
+ /**
287
+ * Emit a runtime-derived `friction` signal when a ticket parks at
288
+ * `agent::blocked` (Story #4578). No-op for every other target state.
289
+ *
290
+ * Why here: `agent::blocked` is the single runtime HITL pause point
291
+ * (`.agents/instructions.md` § 1.J), and this is its canonical mutator — so
292
+ * one hook catches every block regardless of which path drove it (the
293
+ * `merge.unlanded` and `merge.flip-failed` paths in
294
+ * `single-story-close/phases/confirm-merge.js`, the review-block path, an
295
+ * operator's `update-ticket-state.js`, a Story worker giving up). Before
296
+ * this, a block was only ever a *label* — it left no trace in the friction
297
+ * stream the retro reads, so a run could park a worker and still produce a
298
+ * zero-signal roll-up.
299
+ *
300
+ * This is also why the terminal-envelope hook deliberately skips `blocked`
301
+ * (see `frictionForTerminal`): the two would otherwise count one incident
302
+ * twice.
303
+ *
304
+ * Best-effort and awaited: `emitRuntimeFriction` swallows its own failures
305
+ * and resolves `false`, so this can neither throw nor block the transition.
306
+ * It is awaited rather than fire-and-forget because CLI entry points exit
307
+ * via `process.exit` as soon as `main` resolves (`cli-utils.runAsCli` with
308
+ * `propagateExitCode`), which would discard a still-pending append.
309
+ *
310
+ * @param {number} ticketId
311
+ * @param {string} newState
312
+ * @param {{ config?: object }} opts
313
+ * @returns {Promise<void>}
314
+ */
315
+ async function emitBlockedFriction(ticketId, newState, opts) {
316
+ if (newState !== STATE_LABELS.BLOCKED) return;
317
+ await emitRuntimeFriction({
318
+ storyId: ticketId,
319
+ category: RUNTIME_FRICTION_CATEGORIES.STORY_BLOCKED,
320
+ tool: 'transitionTicketState',
321
+ details: { toState: newState },
322
+ config: opts?.config,
323
+ });
324
+ }
325
+
282
326
  /**
283
327
  * Transitions a ticket's label to the new state.
284
328
  * Removes other agent:: state labels.
@@ -361,6 +405,10 @@ export async function transitionTicketState(
361
405
  _ticketSnapshot: ticketSnapshot,
362
406
  });
363
407
 
408
+ // Story #4578 — derive a friction signal from the block, at the point the
409
+ // runtime already knows. Best-effort; never blocks the transition.
410
+ await emitBlockedFriction(ticketId, newState, opts);
411
+
364
412
  // Story #2548 — mirror the new state onto the Projects v2 Status
365
413
  // column. Best-effort; never blocks the transition.
366
414
  // Story #3645 — thread the DIP seam so callers can inject a stub.
@@ -460,14 +508,25 @@ export async function toggleTasklistCheckbox(
460
508
  /**
461
509
  * Post a structured comment to a ticket.
462
510
  *
511
+ * Returns whatever the provider's `postComment` resolved to (Story #4543).
512
+ * Previously the result was swallowed, which made it impossible for a caller
513
+ * to reference the comment it had just written — the terminal envelope's
514
+ * `blocked.frictionCommentId` pointer needs exactly that, so an operator can
515
+ * be sent straight to the remediation instead of told to go find it. Callers
516
+ * that don't need the id simply ignore the return, so this is additive.
517
+ *
518
+ * The shape is provider-defined and may be `undefined` for providers that do
519
+ * not surface one; callers must treat the id as best-effort.
520
+ *
463
521
  * @param {import('../../ITicketingProvider.js').ITicketingProvider} provider
464
522
  * @param {number} ticketId
465
523
  * @param {'progress'|'friction'|'notification'} type
466
524
  * @param {string} payload
525
+ * @returns {Promise<unknown>} The provider's `postComment` result.
467
526
  */
468
527
  export async function postStructuredComment(provider, ticketId, type, payload) {
469
528
  assertValidStructuredCommentType(type);
470
- await provider.postComment(ticketId, {
529
+ const posted = await provider.postComment(ticketId, {
471
530
  type,
472
531
  body: payload,
473
532
  });
@@ -475,4 +534,5 @@ export async function postStructuredComment(provider, ticketId, type, payload) {
475
534
  // `findStructuredComment` against this ticket re-fetches and sees the
476
535
  // freshly-posted comment.
477
536
  invalidateRawCommentsCache(provider, ticketId);
537
+ return posted;
478
538
  }
@@ -26,7 +26,6 @@
26
26
  export {
27
27
  __resetParentCascadeLocks,
28
28
  __setCascadeRetryDelays,
29
- cascadeCompletion,
30
29
  logCascadePartialFailures,
31
30
  } from './ticketing/bulk.js';
32
31
  // Re-export the read surface.
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * plan-phase-cleanup.js — Post-phase temp-file cleanup for `/plan`.
3
3
  *
4
- * The spec and decompose phases write several Epic-scoped temp files under
5
- * the per-Epic tree (`temp/epic-<id>/planner-context.json`,
6
- * `temp/epic-<id>/techspec.md`, etc. — see `lib/config/temp-paths.js`). The
4
+ * The spec and decompose phases write several run-scoped temp files under
5
+ * the per-run tree (`temp/run-<id>/planner-context.json`,
6
+ * `temp/run-<id>/techspec.md`, etc. — see `lib/config/temp-paths.js`). The
7
7
  * workflow .md previously told the operator to `Remove-Item` those files by
8
8
  * name at the end of each phase, which rots: adding a new temp file in the
9
9
  * script required a synchronized markdown edit, and missed edits left
@@ -19,20 +19,20 @@
19
19
  * never sinks a successful phase.
20
20
  *
21
21
  * Migration note (Epic #1030 Story #1040): the legacy flat layout
22
- * (`temp/planner-context-epic-<id>.json` etc.) has been retired. The
23
- * resolver now delegates to `epicArtifactPath` from
24
- * `lib/config/temp-paths.js`, which yields `temp/epic-<id>/<basename>`
22
+ * (`temp/planner-context-for-<id>.json` etc.) has been retired. The
23
+ * resolver now delegates to `runArtifactPath` from
24
+ * `lib/config/temp-paths.js`, which yields `temp/run-<id>/<basename>`
25
25
  * under the configured `tempRoot`.
26
26
  */
27
27
 
28
28
  import fs from 'node:fs/promises';
29
29
  import path from 'node:path';
30
- import { epicArtifactPath } from './config/temp-paths.js';
30
+ import { runArtifactPath } from './config/temp-paths.js';
31
31
  import { PROJECT_ROOT, resolveConfig } from './config-resolver.js';
32
32
 
33
33
  /**
34
- * Map of phase → artifact basenames (no path components). The per-Epic
35
- * directory prefix is supplied by `epicArtifactPath` at resolution time.
34
+ * Map of phase → artifact basenames (no path components). The per-run
35
+ * directory prefix is supplied by `runArtifactPath` at resolution time.
36
36
  */
37
37
  export const PHASE_TEMP_BASENAMES = Object.freeze({
38
38
  spec: Object.freeze([
@@ -45,15 +45,13 @@ export const PHASE_TEMP_BASENAMES = Object.freeze({
45
45
  // plan-phase artifact and deletes them ONLY at terminal success (after
46
46
  // the `agent::ready` flip), fixing the mid-pipeline deletion defect where
47
47
  // per-phase cleanup removed artifacts a `--force`/`--resume` re-persist
48
- // was still entitled to reuse. `risk-verdict.json` joins the set here:
49
- // the split-phase cleanup never owned it, which orphaned it in temp/.
48
+ // was still entitled to reuse.
50
49
  // `plan-metrics.json` stays deliberately excluded (PR1) — the ledger
51
50
  // must survive cleanup so the whole plan run is visible in one stream.
52
51
  persist: Object.freeze([
53
52
  'planner-context.json',
54
53
  'techspec.md',
55
54
  'acceptance-spec.md',
56
- 'risk-verdict.json',
57
55
  'decomposer-context.json',
58
56
  'tickets.json',
59
57
  ]),
@@ -84,8 +82,8 @@ export function resolvePhaseTempPaths(phase, epicId, repoRoot = PROJECT_ROOT) {
84
82
  config = undefined;
85
83
  }
86
84
  return basenames.map((basename) => {
87
- const rel = epicArtifactPath(epicId, basename, config);
88
- // `epicArtifactPath` yields a path under `tempRoot` (relative when
85
+ const rel = runArtifactPath(epicId, basename, config);
86
+ // `runArtifactPath` yields a path under `tempRoot` (relative when
89
87
  // tempRoot is itself relative — the framework default). Rebase
90
88
  // relative paths against `repoRoot` so callers always receive an
91
89
  // absolute path — matches the pre-migration contract that