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
@@ -1,9 +1,22 @@
1
+ /**
2
+ * story-plan-state.js — read the v2 Story planning checkpoint.
3
+ *
4
+ * `plan-persist.js` upserts one `story-plan-state` structured comment per
5
+ * created Story carrying the persist receipt (when the plan completed, how many
6
+ * Stories it created, and their ids). Story #4542 removed the risk fields it
7
+ * used to carry: the planner-authored verdict, the envelope derived from it,
8
+ * and the review routing computed from that envelope. Nothing read any of them
9
+ * back — review depth is now derived from the diff at close time
10
+ * (`review-depth.js#deriveChangeLevel`), so no checkpoint read sits on the
11
+ * delivery path at all.
12
+ */
13
+
1
14
  import { parseFencedJsonComment } from './structured-comment-parser.js';
2
15
  import { findStructuredComment } from './ticketing.js';
3
16
 
4
17
  /**
5
18
  * Read the v2 Story planning checkpoint. Missing/malformed comments degrade
6
- * to null so unplanned Stories still receive the neutral review posture.
19
+ * to null an unplanned Story simply has no persist receipt.
7
20
  */
8
21
  export async function readStoryPlanState({
9
22
  provider,
@@ -18,31 +31,3 @@ export async function readStoryPlanState({
18
31
  const state = parseFencedJsonComment(comment);
19
32
  return state && typeof state === 'object' ? state : null;
20
33
  }
21
-
22
- export async function readStoryPlanningRisk(args) {
23
- const state = await readStoryPlanState(args);
24
- return state?.planningRisk ?? null;
25
- }
26
-
27
- export async function readStoryPlanningRiskSafe(args) {
28
- try {
29
- return await readStoryPlanningRisk(args);
30
- } catch {
31
- return null;
32
- }
33
- }
34
-
35
- /**
36
- * Prefer an explicit `planningRisk` override; otherwise load the Story
37
- * checkpoint. Callers that omit the field (or pass `undefined`) get the
38
- * persisted plan risk; callers that pass `null` keep the neutral posture.
39
- */
40
- export async function resolveStoryPlanningRisk({
41
- provider,
42
- storyId,
43
- planningRisk,
44
- findCommentFn,
45
- }) {
46
- if (planningRisk !== undefined) return planningRisk;
47
- return readStoryPlanningRiskSafe({ provider, storyId, findCommentFn });
48
- }
@@ -24,6 +24,15 @@
24
24
  * name at least one path-shaped token so vague verbs ("clean up",
25
25
  * "refactor") can't slip through.
26
26
  *
27
+ * `acceptance` / `verify` are the **top-level machine contract** (Story
28
+ * #4541). The decomposer prompt tells authors to write those lists once at
29
+ * the ticket's top level and omit the matching body sections; persist syncs
30
+ * them into the body at assemble time. Validation runs *before* that sync,
31
+ * so this validator resolves each contract field from the parsed body and
32
+ * falls back to the ticket's top-level array when the body section is
33
+ * absent. Without that fallback the validator rejected the very shape its
34
+ * own prompt prescribes.
35
+ *
27
36
  * `body.changes` items must be object-form `{ path: string, assumption: enum }`
28
37
  * entries (Story #2636 shape). Plain string bullets are rejected at parse
29
38
  * time and by this validator.
@@ -51,8 +60,8 @@ import { FILE_ASSUMPTION_VALUES } from './file-assumption-enum.js';
51
60
 
52
61
  /**
53
62
  * Canonical testing-tier labels that a `verify[]` entry must name (in
54
- * parentheses) to pass plan-time validation. Mirrors the skill contract in
55
- * `core/epic-plan-decompose-author/SKILL.md § verify rules`.
63
+ * parentheses) to pass plan-time validation. Mirrors the verify-rules contract
64
+ * in `.agents/scripts/lib/templates/decomposer-prompts.js`.
56
65
  *
57
66
  * Entries that do not end with `(<tier>)` and are not `manual:<reason>` are
58
67
  * rejected by `collectVerifyErrors`.
@@ -164,6 +173,39 @@ function resolveStructuredBody(ticket) {
164
173
  }
165
174
  }
166
175
 
176
+ /**
177
+ * The two contract fields that live at the ticket's top level and are
178
+ * synced into the body by `plan-persist` at assemble time.
179
+ */
180
+ const CONTRACT_FIELDS = Object.freeze(['acceptance', 'verify']);
181
+
182
+ /**
183
+ * Resolve the body's contract fields against the ticket's top-level arrays
184
+ * (Story #4541). The decomposer prompt prescribes authoring `acceptance[]`
185
+ * / `verify[]` **once** at top level and omitting the matching body
186
+ * sections; `assemblePlanStories#syncContractFieldFromTopLevel` performs
187
+ * the sync, but it runs *after* validation. So an absent body section is
188
+ * not a violation when the ticket carries the list at top level — it is the
189
+ * preferred shape. A body section that is present and disagrees with the
190
+ * top level is left alone here: the sync itself fails closed on that
191
+ * mismatch, and duplicating the check would report it twice.
192
+ *
193
+ * @param {object} ticket
194
+ * @param {object} bodyObject Parsed / structured body.
195
+ * @returns {object} A copy of `bodyObject` with the contract fields resolved.
196
+ */
197
+ function resolveContractFieldsFromTopLevel(ticket, bodyObject) {
198
+ const resolved = { ...bodyObject };
199
+ for (const field of CONTRACT_FIELDS) {
200
+ const bodyValue = Array.isArray(resolved[field]) ? resolved[field] : [];
201
+ if (bodyValue.length > 0) continue;
202
+ const topLevel = Array.isArray(ticket?.[field]) ? ticket[field] : [];
203
+ if (topLevel.length === 0) continue;
204
+ resolved[field] = topLevel.map(String);
205
+ }
206
+ return resolved;
207
+ }
208
+
167
209
  /**
168
210
  * Validate one Story body and return every violation it exhibits. Empty
169
211
  * array means clean. Splits the per-ticket cascade out of
@@ -182,13 +224,14 @@ function resolveStructuredBody(ticket) {
182
224
  */
183
225
  export function validateTaskBodyShape(ticket) {
184
226
  const prefix = `Story "${ticket.title}" (${ticket.slug})`;
185
- const { body, error } = resolveStructuredBody(ticket);
227
+ const { body: parsed, error } = resolveStructuredBody(ticket);
186
228
  if (error !== null) {
187
229
  return [error];
188
230
  }
189
- if (body === null || typeof body !== 'object') {
190
- return [`${prefix}: body must be an object, got ${typeof body}.`];
231
+ if (parsed === null || typeof parsed !== 'object') {
232
+ return [`${prefix}: body must be an object, got ${typeof parsed}.`];
191
233
  }
234
+ const body = resolveContractFieldsFromTopLevel(ticket, parsed);
192
235
  const errors = [];
193
236
  if (typeof body.goal !== 'string' || body.goal.trim() === '') {
194
237
  errors.push(`${prefix}: body.goal must be a non-empty string.`);
@@ -300,7 +343,9 @@ function collectReferencesErrors(prefix, rawReferences) {
300
343
  function collectAcceptanceErrors(prefix, rawAcceptance) {
301
344
  const acceptance = Array.isArray(rawAcceptance) ? rawAcceptance : [];
302
345
  if (acceptance.length === 0) {
303
- return [`${prefix}: body.acceptance must list at least one criterion.`];
346
+ return [
347
+ `${prefix}: acceptance must list at least one criterion — author it at the ticket's top level (preferred) or in the body's ## Acceptance section.`,
348
+ ];
304
349
  }
305
350
  return [];
306
351
  }
@@ -323,7 +368,7 @@ function collectVerifyErrors(prefix, rawVerify) {
323
368
  const verify = Array.isArray(rawVerify) ? rawVerify : [];
324
369
  if (verify.length === 0) {
325
370
  return [
326
- `${prefix}: body.verify must list at least one entry. Use "manual:<reason>" only when truly unverifiable in isolation.`,
371
+ `${prefix}: verify must list at least one entry — author it at the ticket's top level (preferred) or in the body's ## Verify section. Use "manual:<reason>" only when truly unverifiable in isolation.`,
327
372
  ];
328
373
  }
329
374
  const errors = [];
@@ -5,9 +5,19 @@
5
5
  * an exclusive, time-bounded claim on a ticket so two concurrent runs do not
6
6
  * both drive the same Story. Rather than invent a new state column, the lease
7
7
  * rides the ticket's existing **assignees** surface: the single assignee *is*
8
- * the lease owner. Liveness is decided by the owner's most-recent
9
- * `story.heartbeat` timestamp (the `operator` field added in this Story links
10
- * a heartbeat back to the claimant) compared against a configured TTL.
8
+ * the lease owner. Liveness is decided by the owner's last-heartbeat epoch-ms
9
+ * compared against a configured TTL.
10
+ *
11
+ * **There is no live heartbeat source.** The `story.heartbeat` emitter this
12
+ * module was designed against was structurally inert (it demanded an
13
+ * `epicId >= 1` that v2, which has no Epics, never supplies) and has been
14
+ * deleted. Every caller now reaches `acquireLease` through
15
+ * `lease-guard-shared.acquireLeaseFailClosed` with `anchorHeartbeatToNow`,
16
+ * which pins `heartbeatAt` to `now`: the lease **fails closed**, so any
17
+ * foreign claim reads as live and refuses. A stranded claim is cleared with
18
+ * `--steal`, never by TTL expiry. The TTL and the stale-reclaim branch below
19
+ * are therefore reachable only via an explicit caller-supplied `heartbeatAt`
20
+ * — the seam is kept, the automatic expiry is not real.
11
21
  *
12
22
  * The three exported operations are deliberately thin and provider-agnostic:
13
23
  *
@@ -31,19 +41,16 @@
31
41
  * Liveness seam: callers supply the owner's last-heartbeat epoch-ms via the
32
42
  * `heartbeatAt` option (a number, or `null`/`undefined` when no heartbeat has
33
43
  * ever been recorded for the current owner). Threading the timestamp in keeps
34
- * this module pure and trivially unit-testable — it does not read the
35
- * lifecycle ledger itself. A claim with no heartbeat is treated as stale
36
- * (reclaimable) so an abandoned assignment never wedges the ticket.
44
+ * this module pure and trivially unit-testable — it does not read any ledger
45
+ * itself. A claim with no heartbeat is treated as stale (reclaimable) by
46
+ * `isClaimLive`; note the live guards never take that branch, per the
47
+ * fail-closed anchoring described above.
37
48
  *
38
49
  * `now` is injectable (epoch ms) for deterministic tests; it defaults to
39
50
  * `Date.now()`.
40
51
  */
41
52
 
42
- import { readFileSync } from 'node:fs';
43
-
44
53
  import { resolveLeaseTtlMs } from '../config/limits.js';
45
- import { epicLedgerPath } from '../config/temp-paths.js';
46
- import { parseLedger } from './lifecycle/trace-logger.js';
47
54
 
48
55
  /**
49
56
  * The shipped, non-personal operator-identity placeholder (and its bare,
@@ -87,76 +94,22 @@ export function normalizeOperatorHandle(raw) {
87
94
  return trimmed;
88
95
  }
89
96
 
90
- /**
91
- * Read the most-recent `story.heartbeat` epoch-ms recorded for a given lease
92
- * owner from an Epic lifecycle ledger. Returns `null` when the ledger is
93
- * absent, unreadable, or carries no heartbeat for that owner — which the lease
94
- * primitive treats as a stale (reclaimable) claim.
95
- *
96
- * The ledger is NDJSON; each `story.heartbeat` record carries
97
- * `payload.operator` (Story #3480) and `payload.timestamp` (ISO-8601). A
98
- * malformed ledger downgrades to `null` rather than throwing so a corrupt
99
- * observability artifact never wedges the lease preflight.
100
- *
101
- * This is the shared liveness source the lease guards thread into
102
- * `acquireLease` via `heartbeatAt`; `/plan` and `/deliver` both
103
- * reuse it so a live foreign claim actually refuses.
104
- *
105
- * @param {object} args
106
- * @param {number} args.epicId
107
- * @param {string} args.owner Lease owner whose heartbeat to find.
108
- * @param {object} [args.config] Resolved config (for ledger path).
109
- * @param {string} [args.ledgerPath] Explicit path override (tests).
110
- * @param {(eid: number, config?: object) => string} [args.ledgerPathResolver]
111
- * Injectable resolver (tests). Defaults to `epicLedgerPath`.
112
- * @param {(p: string) => string} [args.readFile] Injectable reader (tests).
113
- * @returns {number|null}
114
- */
115
- export function latestHeartbeatForOwner({
116
- epicId,
117
- owner,
118
- config,
119
- ledgerPath,
120
- ledgerPathResolver = epicLedgerPath,
121
- readFile = (p) => readFileSync(p, 'utf8'),
122
- }) {
123
- if (typeof owner !== 'string' || owner.length === 0) return null;
124
- const resolvedPath = ledgerPath ?? ledgerPathResolver(epicId, config);
125
-
126
- let text;
127
- try {
128
- text = readFile(resolvedPath);
129
- } catch (_err) {
130
- // No ledger yet (fresh Epic) → no heartbeat → reclaimable.
131
- return null;
132
- }
133
-
134
- let records;
135
- try {
136
- records = parseLedger(text);
137
- } catch (_err) {
138
- // Corrupt ledger is an observability problem, not a coordination blocker.
139
- return null;
140
- }
141
-
142
- let latest = null;
143
- for (const record of records) {
144
- const payload = record?.payload;
145
- if (!payload || payload.event !== 'story.heartbeat') continue;
146
- if (payload.operator !== owner) continue;
147
- const ts = Date.parse(payload.timestamp ?? '');
148
- if (!Number.isFinite(ts)) continue;
149
- if (latest === null || ts > latest) latest = ts;
150
- }
151
- return latest;
152
- }
153
-
154
97
  /**
155
98
  * Decide whether a foreign claim is still "live" given the owner's last
156
99
  * heartbeat and the configured TTL. A claim is live when a heartbeat exists
157
100
  * and is no older than `ttlMs`. A missing heartbeat (`null`/`undefined`) or a
158
101
  * heartbeat older than the TTL is stale and therefore reclaimable.
159
102
  *
103
+ * The `heartbeatAt` seam is retained, but there is no longer any in-repo
104
+ * heartbeat *source*: the `story.heartbeat` emitter was structurally inert
105
+ * (it required an `epicId >= 1` that v2 never sets) and was deleted along
106
+ * with the ledger reader that scanned for it. Every live caller reaches this
107
+ * through `lease-guard-shared.acquireLeaseFailClosed` with
108
+ * `anchorHeartbeatToNow`, which pins `heartbeatAt` to `now` so ANY foreign
109
+ * claim reads live and the guard fails closed — a stranded claim is cleared
110
+ * with `--steal`, not by TTL expiry. The parameter stays because that
111
+ * anchoring is expressed through it.
112
+ *
160
113
  * @param {object} args
161
114
  * @param {number|null|undefined} args.heartbeatAt Owner's last heartbeat (epoch ms).
162
115
  * @param {number} args.ttlMs Lease TTL in milliseconds.
@@ -589,13 +589,53 @@ function registryRegistry(producers, assumptionEntries, patterns, cache) {
589
589
  }
590
590
 
591
591
  /**
592
- * Compute `fan-out-warning` findings (Story #2962).
592
+ * Normalize a fan-out probe result into `{ count, files, probe }`.
593
593
  *
594
- * For each `body.changes` entry whose `assumption` is `"deletes"` (or
595
- * `"refactors-existing"` when the planner declared a symbol replacement),
596
- * count the number of distinct files in the base branch that reference
597
- * the deleted module via its basename. When the count exceeds the
598
- * configured `largeFanOutThreshold`, emit a finding.
594
+ * The production probe reports its referencing files and the exact command
595
+ * that found them so an operator can reproduce the figure (Story #4547).
596
+ * A bare number stays valid injected test counters and any consumer
597
+ * counter written against the Story #2962 contract keep working, they just
598
+ * carry no audit trail.
599
+ */
600
+ function normalizeFanOutProbe(result) {
601
+ if (typeof result === 'number') {
602
+ return { count: result, files: [], probe: null };
603
+ }
604
+ if (result === null || typeof result !== 'object') {
605
+ return { count: 0, files: [], probe: null };
606
+ }
607
+ const files = Array.isArray(result.files) ? result.files : [];
608
+ const count = Number.isFinite(result.count) ? result.count : files.length;
609
+ return { count, files, probe: result.probe ?? null };
610
+ }
611
+
612
+ /**
613
+ * Index the basenames this spec *creates*, so a deletion that is really one
614
+ * half of a move can be told apart from a genuine wide-coupling removal.
615
+ */
616
+ function indexCreatedBasenames(assumptionEntries) {
617
+ const byBasename = new Map();
618
+ for (const entry of assumptionEntries) {
619
+ if (entry.assumption !== 'creates') continue;
620
+ const base = entry.path.slice(entry.path.lastIndexOf('/') + 1);
621
+ if (!byBasename.has(base)) byBasename.set(base, entry.path);
622
+ }
623
+ return byBasename;
624
+ }
625
+
626
+ /**
627
+ * Compute `fan-out-warning` findings (Story #2962, reworked in #4547).
628
+ *
629
+ * For each `body.changes` entry whose `assumption` is `"deletes"`, probe the
630
+ * files at the base branch that genuinely import or require the deleted
631
+ * module. When that count exceeds the configured `largeFanOutThreshold`,
632
+ * emit a finding carrying the referencing files and the probe that produced
633
+ * them.
634
+ *
635
+ * The finding also records whether the deletion is **rename-shaped** — the
636
+ * same spec creates a file with the deleted module's basename elsewhere —
637
+ * because the remedy diverges: a move wants its importers repointed in one
638
+ * Story, not a subsystem-by-subsystem migration split across several.
599
639
  *
600
640
  * The default severity is always `'soft'` — the persist gate enforces a
601
641
  * hard refusal via the `--allow-large-fan-out` operator flag, since the
@@ -613,21 +653,28 @@ function computeFanOutFindings({
613
653
  if (!Number.isFinite(threshold) || threshold < 0) return [];
614
654
  const findings = [];
615
655
  const cache = new Map();
656
+ const createdBasenames = indexCreatedBasenames(assumptionEntries);
616
657
  for (const entry of assumptionEntries) {
617
658
  if (entry.assumption !== 'deletes') continue;
618
- let count = cache.get(entry.path);
619
- if (count === undefined) {
620
- count = counter({ path: entry.path }) ?? 0;
621
- cache.set(entry.path, count);
659
+ let probed = cache.get(entry.path);
660
+ if (probed === undefined) {
661
+ probed = normalizeFanOutProbe(counter({ path: entry.path }));
662
+ cache.set(entry.path, probed);
622
663
  }
623
- if (count <= threshold) continue;
664
+ if (probed.count <= threshold) continue;
665
+ const base = entry.path.slice(entry.path.lastIndexOf('/') + 1);
666
+ const renameTarget = createdBasenames.get(base);
624
667
  findings.push({
625
668
  kind: 'fan-out-warning',
626
669
  severity,
627
670
  taskSlug: entry.taskSlug,
628
671
  storySlug: entry.storySlug,
629
672
  path: entry.path,
630
- callSiteCount: count,
673
+ callSiteCount: probed.count,
674
+ callSites: probed.files,
675
+ probe: probed.probe,
676
+ renameShaped: renameTarget !== undefined && renameTarget !== entry.path,
677
+ renameTarget: renameTarget === entry.path ? null : (renameTarget ?? null),
631
678
  threshold,
632
679
  });
633
680
  }
@@ -701,6 +748,60 @@ export function computeConflictFindings({ stories, policy } = {}) {
701
748
  ];
702
749
  }
703
750
 
751
+ /**
752
+ * Render the audit trail behind a fan-out finding's number, so an operator
753
+ * can check the figure rather than trust it (Story #4547).
754
+ *
755
+ * Every importer is named — the list is deliberately **not** truncated. A
756
+ * gate that fires at 100 importers is precisely when the operator needs the
757
+ * list, and a `…and 109 more` tail would leave the figure uncheckable in
758
+ * exactly the case the gate exists for. This message is a fail-closed stop,
759
+ * not a log line; its length is the point.
760
+ *
761
+ * The probe is reported as what it is — the *candidate* net, which each hit
762
+ * is then re-resolved against. It will report at least as many lines as the
763
+ * gate counts files, so labelling it as the thing that produced the number
764
+ * would send an operator chasing a discrepancy that is by design.
765
+ *
766
+ * Returns `''` for a bare-number counter, which carries no audit trail.
767
+ */
768
+ export function renderFanOutEvidence(finding) {
769
+ const files = Array.isArray(finding.callSites) ? finding.callSites : [];
770
+ const parts = [];
771
+ if (files.length > 0) {
772
+ parts.push(` Importers (${files.length}):`);
773
+ for (const file of files) parts.push(` ${file}`);
774
+ }
775
+ if (finding.probe) {
776
+ parts.push(
777
+ ` Candidate probe (each hit re-resolved against its importer's directory):`,
778
+ ` ${finding.probe}`,
779
+ );
780
+ }
781
+ return parts.length > 0 ? `\n${parts.join('\n')}` : '';
782
+ }
783
+
784
+ /**
785
+ * Render the remedy that actually fits the finding. A rename-shaped
786
+ * deletion has nowhere to split to — the importers just need repointing at
787
+ * the path the same plan creates — so telling the operator to split it
788
+ * across Stories leaves the override as the only exit, which is exactly the
789
+ * habit that defeats the gate (Story #4547).
790
+ */
791
+ export function renderFanOutRemedy(finding) {
792
+ if (finding.renameShaped && finding.renameTarget) {
793
+ return (
794
+ `This deletion is rename-shaped: the same plan creates "${finding.renameTarget}" under the same basename. ` +
795
+ `Repoint the importer(s) at the new path inside this Story — a move has no subsystems to split across — ` +
796
+ `then rerun --allow-large-fan-out.`
797
+ );
798
+ }
799
+ return (
800
+ `Split the deletion into a subsystem-by-subsystem migration across multiple Stories, ` +
801
+ `or rerun --allow-large-fan-out after confirming the deletion is intentional.`
802
+ );
803
+ }
804
+
704
805
  /**
705
806
  * Render a `'hard'`-severity conflict finding as a human-readable error
706
807
  * message. Used by the validator when policy flags upgrade a finding to
@@ -712,14 +813,18 @@ export function renderHardConflictError(finding) {
712
813
  return `Shared-editor conflict: "${finding.path}" is written by ${finding.storySlugs.length} concurrent Stories (${stories}). Add depends_on chains between them or split the edits into a dedicated late-wave wiring Story.`;
713
814
  }
714
815
  if (finding.kind === 'implicit-cross-story-dep') {
715
- return `Implicit cross-Story dependency: Task "${finding.consumer.taskSlug}" in Story "${finding.consumer.storySlug}" references "${finding.path}" (produced by Task "${finding.producer.taskSlug}" in Story "${finding.producer.storySlug}") via body.${finding.consumer.sourceField}, but Story "${finding.consumer.storySlug}" has no depends_on link to Story "${finding.producer.storySlug}". Add depends_on: ["${finding.producer.storySlug}"] to the consumer Story or remove the reference.`;
816
+ return `Implicit cross-Story dependency: Story "${finding.consumer.storySlug}" references "${finding.path}" (produced by Story "${finding.producer.storySlug}") via body.${finding.consumer.sourceField}, but Story "${finding.consumer.storySlug}" has no depends_on link to Story "${finding.producer.storySlug}". Add depends_on: ["${finding.producer.storySlug}"] to the consumer Story or remove the reference.`;
716
817
  }
717
818
  if (finding.kind === 'cross-cutting-registries') {
718
819
  const stories = finding.storySlugs.map((s) => `"${s}"`).join(', ');
719
820
  return `Cross-cutting registry conflict: ${finding.storySlugs.length} concurrent Stories (${stories}) edit or register into "${finding.registryPath}". Add depends_on chains between them so the registry updates serialize, or split the registration into a dedicated late-wave wiring Story.`;
720
821
  }
721
822
  if (finding.kind === 'fan-out-warning') {
722
- return `Large fan-out: Task "${finding.taskSlug}" in Story "${finding.storySlug}" deletes "${finding.path}" with ${finding.callSiteCount} call site(s) on the base branch (threshold ${finding.threshold}). Split into a subsystem-by-subsystem migration across multiple Stories, or rerun --allow-large-fan-out after confirming the deletion is intentional.`;
823
+ return (
824
+ `Large fan-out: Story "${finding.storySlug}" deletes "${finding.path}" ` +
825
+ `with ${finding.callSiteCount} importer(s) on the base branch (threshold ${finding.threshold}). ` +
826
+ `${renderFanOutRemedy(finding)}${renderFanOutEvidence(finding)}`
827
+ );
723
828
  }
724
829
  if (finding.kind === 'missing-bdd-scaffold') {
725
830
  return `Missing BDD scaffold: Story "${finding.consumer.storySlug}" verifies against "${finding.path}" (created by Story "${finding.producer.storySlug}") via body.${finding.consumer.sourceField}, but "${finding.consumer.storySlug}" has no depends_on path to "${finding.producer.storySlug}" — the .feature file is scaffolded in the same wave (or later), so verification runs before the file exists. Add depends_on: ["${finding.producer.storySlug}"] to the consumer Story so the scaffold lands in an earlier wave.`;
@@ -14,10 +14,9 @@
14
14
  *
15
15
  * `DEFAULT_MODEL_CAPACITY` is the **single source of truth** for the capacity
16
16
  * thresholds. The decomposer prompt template
17
- * (`.agents/scripts/lib/templates/decomposer-prompts.js`) and the authoring
18
- * SKILL (`.agents/skills/core/epic-plan-decompose-author/SKILL.md`) reference
19
- * these numbers rather than restating divergent ones — the prompt generates
20
- * its threshold sentence from this constant so the two surfaces cannot drift.
17
+ * (`.agents/scripts/lib/templates/decomposer-prompts.js`) generates its
18
+ * threshold sentence from this constant rather than restating divergent
19
+ * numbers, so the two surfaces cannot drift.
21
20
  *
22
21
  * Capacity model:
23
22
  * - Plan-time **session mass** = authored tokens only (`estimateTokens` of