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
@@ -4,8 +4,8 @@
4
4
  * acceptance-eval.js — bounded per-Story acceptance self-eval gate (Story #3819).
5
5
  *
6
6
  * The Story-implementation phase runs an independent (fresh-context)
7
- * critic pass that scores the working diff against each inline
8
- * `acceptance[]` item, emitting one verdict file per round
7
+ * critic pass that scores the caller-injected change set against each
8
+ * inline `acceptance[]` item, emitting one verdict file per round
9
9
  * (`.agents/schemas/acceptance-eval-verdict.schema.json`). This CLI is the
10
10
  * deterministic substrate that turns that verdict into the loop's next
11
11
  * action:
@@ -32,26 +32,20 @@
32
32
  * — this CLI is the decision + signal boundary, mirroring how the existing
33
33
  * gates separate decision from ticket mutation.
34
34
  *
35
- * Two invocation shapes (Epic #4475, M4-B):
36
- * - Per-Story (fan-out): the diff is one Story's; round scoping is per
37
- * Story off the Story's `signals.ndjson`.
38
- * - Per-AC-cluster (single delivery): `--epic <id> --cluster <clusterId>`
39
- * with NO `--story`. The verdict scores one AC cluster of the Epic's
40
- * `## Acceptance Table` against the cumulative `main..epic/<id>` diff;
41
- * round scoping is per cluster off the Epic's `signals.ndjson`. This is
42
- * the acceptance-dilution guard — `ceil(totalACs / clusterCeiling)`
43
- * independent maker-blind critic passes, one per cluster.
35
+ * One invocation shape: per-Story. The diff is one Story's; round scoping
36
+ * is per Story off the Story's `signals.ndjson`. (v2.0.0 removed the Epic
37
+ * tier along with the per-AC-cluster `--epic <id> --cluster <id>` mode that
38
+ * scored an Epic `## Acceptance Table` against a `main..epic/<id>` diff.)
44
39
  *
45
40
  * CLI:
46
- * --story <id> Story ID (required unless --cluster is given).
47
- * --epic <id> Parent Epic ID (required with --cluster).
48
- * --cluster <id> AC-cluster id for the single-delivery critic.
41
+ * --story <id> Story ID (required).
49
42
  * --verdict <path> Path to the round's verdict JSON (required).
50
43
  * --no-signal Suppress the signal emit (tests).
51
44
  *
52
45
  * Stdout: a single JSON envelope
53
46
  * { storyId, epicId, decision, round, cap, capReached, totalCriteria,
54
47
  * metCount, unmetCriteria[], signalEmitted }
48
+ * (`epicId` is retained as a always-null field for envelope stability.)
55
49
  *
56
50
  * @see .agents/scripts/lib/orchestration/acceptance-eval-decision.js
57
51
  * @see .agents/schemas/acceptance-eval-verdict.schema.json
@@ -68,10 +62,7 @@ import addFormats from 'ajv-formats';
68
62
  import { runAsCli } from './lib/cli-utils.js';
69
63
  import { getAcceptanceEval, resolveConfig } from './lib/config-resolver.js';
70
64
  import { Logger } from './lib/Logger.js';
71
- import {
72
- appendEpicSignal,
73
- appendSignal,
74
- } from './lib/observability/signals-writer.js';
65
+ import { appendSignal } from './lib/observability/signals-writer.js';
75
66
  import {
76
67
  buildAcceptanceEvalSignal,
77
68
  decideAcceptanceEval,
@@ -139,22 +130,14 @@ function parseCliArgs(argv) {
139
130
  args: argv,
140
131
  options: {
141
132
  story: { type: 'string' },
142
- epic: { type: 'string' },
143
- cluster: { type: 'string' },
144
133
  verdict: { type: 'string' },
145
134
  'no-signal': { type: 'boolean', default: false },
146
135
  },
147
136
  strict: false,
148
137
  });
149
138
  const storyId = Number.parseInt(values.story ?? '', 10);
150
- const epicRaw = Number.parseInt(values.epic ?? '', 10);
151
139
  return {
152
140
  storyId: Number.isInteger(storyId) && storyId > 0 ? storyId : null,
153
- epicId: Number.isInteger(epicRaw) && epicRaw > 0 ? epicRaw : null,
154
- clusterId:
155
- typeof values.cluster === 'string' && values.cluster.length > 0
156
- ? values.cluster
157
- : null,
158
141
  verdictPath: values.verdict ?? null,
159
142
  emitSignal: values['no-signal'] !== true,
160
143
  };
@@ -168,7 +151,6 @@ function parseCliArgs(argv) {
168
151
  *
169
152
  * @param {object} args
170
153
  * @param {number} args.storyId
171
- * @param {number | null} args.epicId
172
154
  * @param {object} args.verdict — validated verdict object.
173
155
  * @param {object} args.config — resolved `.agentrc.json`.
174
156
  * @param {boolean} args.emitSignal
@@ -182,23 +164,18 @@ function parseCliArgs(argv) {
182
164
  * @returns {Promise<{ envelope: object, exitCode: number }>}
183
165
  */
184
166
  export async function runAcceptanceEval(
185
- { storyId, epicId, clusterId = null, verdict, config, emitSignal, round },
167
+ { storyId, verdict, config, emitSignal, round },
186
168
  deps = {},
187
169
  ) {
188
170
  const {
189
171
  appendSignalFn = appendSignal,
190
- appendEpicSignalFn = appendEpicSignal,
191
172
  deriveRoundFn = deriveAcceptanceEvalRound,
192
173
  } = deps;
193
- const clusterMode =
194
- typeof clusterId === 'string' &&
195
- clusterId.length > 0 &&
196
- Number.isInteger(epicId);
197
174
  const { maxRounds } = getAcceptanceEval(config);
198
175
  const resolvedRound =
199
176
  Number.isInteger(round) && round >= 1
200
177
  ? round
201
- : deriveRoundFn({ epicId: epicId ?? null, storyId, clusterId, config });
178
+ : deriveRoundFn({ epicId: null, storyId, config });
202
179
  const outcome = decideAcceptanceEval({
203
180
  verdict,
204
181
  maxRounds,
@@ -208,21 +185,16 @@ export async function runAcceptanceEval(
208
185
  let signalEmitted = false;
209
186
  if (emitSignal) {
210
187
  const signal = {
211
- ...buildAcceptanceEvalSignal({ storyId, epicId, outcome, clusterId }),
188
+ ...buildAcceptanceEvalSignal({ storyId, epicId: null, outcome }),
212
189
  ts: new Date().toISOString(),
213
190
  };
214
191
  try {
215
- // Cluster mode (single delivery) writes to the epic-level signals
216
- // stream so per-cluster round counts survive resume; the per-Story
217
- // path writes to the Story stream (unchanged).
218
- signalEmitted = clusterMode
219
- ? await appendEpicSignalFn({ epicId, signal, config })
220
- : await appendSignalFn({
221
- epicId,
222
- storyId,
223
- signal,
224
- config,
225
- });
192
+ signalEmitted = await appendSignalFn({
193
+ epicId: null,
194
+ storyId,
195
+ signal,
196
+ config,
197
+ });
226
198
  } catch (err) {
227
199
  // Observability is best-effort — a failed signal write must never
228
200
  // take down the gate. The decision still stands.
@@ -236,8 +208,7 @@ export async function runAcceptanceEval(
236
208
 
237
209
  const envelope = {
238
210
  storyId: storyId ?? null,
239
- epicId: epicId ?? null,
240
- ...(clusterMode ? { clusterId } : {}),
211
+ epicId: null,
241
212
  decision: outcome.decision,
242
213
  round: outcome.round,
243
214
  cap: outcome.cap,
@@ -261,19 +232,11 @@ export async function runAcceptanceEval(
261
232
  }
262
233
 
263
234
  export async function main(argv = process.argv.slice(2)) {
264
- const { storyId, epicId, clusterId, verdictPath, emitSignal } =
265
- parseCliArgs(argv);
235
+ const { storyId, verdictPath, emitSignal } = parseCliArgs(argv);
266
236
 
267
- // Epic #4475 (M4-B): two invocation shapes.
268
- // - Per-Story (fan-out): --story <id> [--epic <id>].
269
- // - Per-AC-cluster (single delivery): --epic <id> --cluster <id> (no
270
- // --story); the verdict scores one AC cluster of the Epic's Acceptance
271
- // Table against the cumulative main..epic/<id> diff.
272
- const clusterMode = Boolean(clusterId) && Number.isInteger(epicId);
273
- if (!storyId && !clusterMode) {
237
+ if (!storyId) {
274
238
  throw new Error(
275
- 'Usage: node acceptance-eval.js --story <id> [--epic <id>] --verdict <path> [--no-signal]\n' +
276
- ' or: node acceptance-eval.js --epic <id> --cluster <clusterId> --verdict <path> [--no-signal]',
239
+ 'Usage: node acceptance-eval.js --story <id> --verdict <path> [--no-signal]',
277
240
  );
278
241
  }
279
242
  if (!verdictPath) {
@@ -305,13 +268,8 @@ export async function main(argv = process.argv.slice(2)) {
305
268
  const verdict = validateVerdict(parsed);
306
269
 
307
270
  // A verdict whose embedded storyId disagrees with the CLI flag is a
308
- // wiring error worth failing on, not a silent mismatch. Skipped in cluster
309
- // mode (no --story; the verdict scores an AC cluster, not a Story).
310
- if (
311
- !clusterMode &&
312
- Number.isInteger(verdict.storyId) &&
313
- verdict.storyId !== storyId
314
- ) {
271
+ // wiring error worth failing on, not a silent mismatch.
272
+ if (Number.isInteger(verdict.storyId) && verdict.storyId !== storyId) {
315
273
  throw new Error(
316
274
  `acceptance-eval: verdict storyId (${verdict.storyId}) does not match --story ${storyId}.`,
317
275
  );
@@ -320,8 +278,6 @@ export async function main(argv = process.argv.slice(2)) {
320
278
  const config = resolveConfig();
321
279
  const { envelope, exitCode } = await runAcceptanceEval({
322
280
  storyId,
323
- epicId,
324
- clusterId,
325
281
  verdict,
326
282
  config,
327
283
  emitSignal,
@@ -3,7 +3,7 @@
3
3
  * agents-bootstrap-github — Idempotent Label & Field Setup
4
4
  *
5
5
  * Creates the required label taxonomy and project board custom fields
6
- * for the v5 Epic-centric flow on a target GitHub repo. Idempotent —
6
+ * for the v5 Story-centric flow on a target GitHub repo. Idempotent —
7
7
  * skips resources that already exist.
8
8
  *
9
9
  * Usage:
@@ -45,8 +45,8 @@
45
45
  * and custom fields. Off by default — the project
46
46
  * board object is still created when a project
47
47
  * name is supplied, but decoration is skipped.
48
- * --with-issue-forms Opt-in: generate .github/ISSUE_TEMPLATE/story.yml
49
- * and epic.yml. Off by default.
48
+ * --with-issue-forms Opt-in: generate .github/ISSUE_TEMPLATE/story.yml.
49
+ * Off by default.
50
50
  * --reap-conflicting-workflows Delete Projects V2 built-in workflows that
51
51
  * race against the orchestrator (destructive)
52
52
  * --help Print this help
@@ -112,7 +112,7 @@ Flags:
112
112
  --with-project-board Opt-in: provision the Projects V2 Status field
113
113
  and custom fields (default: off — prompted y/N).
114
114
  --with-issue-forms Opt-in: generate .github/ISSUE_TEMPLATE/story.yml
115
- and epic.yml (default: off — prompted y/N).
115
+ (default: off — prompted y/N).
116
116
  --reap-conflicting-workflows Delete Projects V2 built-in workflows that
117
117
  race against the orchestrator (destructive)
118
118
  --help Print this help
@@ -9,12 +9,18 @@
9
9
  * misconfigured knip installation cannot block CI when we have no current
10
10
  * snapshot to compare against.
11
11
  *
12
+ * Story #4575 adds the **production pass** (`--production`), the test-only-
13
+ * importer discount: default mode counts a test as an importer, so
14
+ * production-dead code hides behind its own tests. See
15
+ * `lib/dead-exports-mode.js` for why the two passes carry separate baselines.
16
+ *
12
17
  * Contract:
13
- * - Reads the committed baseline at `baselines/dead-exports.json`
14
- * (override with `--baseline <path>`). Envelope shape:
18
+ * - Reads the committed baseline at `baselines/dead-exports.json` — or
19
+ * `baselines/dead-exports-production.json` under `--production`
20
+ * (override either with `--baseline <path>`). Envelope shape:
15
21
  * { $schema, kernelVersion, generatedAt, rows: [{ file, symbol }] }
16
- * - Spawns `npx knip --reporter json --no-progress`, parses stdout,
17
- * extracts `{ file, symbol }` rows from `issues[].exports[]`.
22
+ * - Spawns `npx knip --reporter json --no-progress` (plus `--production`),
23
+ * parses stdout, extracts `{ file, symbol }` rows from `issues[].exports[]`.
18
24
  * - Diffs current vs. baseline by `(file, symbol)` identity.
19
25
  * - Prints `+ <file>: <symbol>` for each added dead export and
20
26
  * `- <file>: <symbol>` for each removed one, then a summary line.
@@ -25,24 +31,30 @@
25
31
  * Knip spawn/parse failure exits 0 (advisory) with a stderr warning.
26
32
  */
27
33
 
28
- import { spawnSync } from 'node:child_process';
29
34
  import fs from 'node:fs';
30
35
  import path from 'node:path';
31
36
  import process from 'node:process';
32
37
  import { runAsCli } from './lib/cli-utils.js';
38
+ import {
39
+ extractRowsFromKnip,
40
+ readKnipOutput,
41
+ runKnip,
42
+ } from './lib/dead-exports-knip.js';
43
+ import { resolveDeadExportsMode } from './lib/dead-exports-mode.js';
33
44
 
34
45
  /**
35
- * Parse argv for `--baseline <path>`, `--json`, and `--knip-output <path>`.
36
- * `--knip-output` is a test seam: pass a pre-captured knip JSON file instead
37
- * of spawning knip. Exported so unit tests can pin the parser.
46
+ * Parse argv for `--baseline <path>`, `--json`, `--knip-output <path>`, and
47
+ * `--production`. `--knip-output` is a test seam: pass a pre-captured knip JSON
48
+ * file instead of spawning knip. Exported so unit tests can pin the parser.
38
49
  *
39
50
  * @param {string[]} argv
40
- * @returns {{ baselinePath: string | null, json: boolean, knipOutputPath: string | null }}
51
+ * @returns {{ baselinePath: string | null, json: boolean, knipOutputPath: string | null, production: boolean }}
41
52
  */
42
53
  export function parseArgv(argv = []) {
43
54
  let baselinePath = null;
44
55
  let json = false;
45
56
  let knipOutputPath = null;
57
+ let production = false;
46
58
  for (let i = 0; i < argv.length; i += 1) {
47
59
  const a = argv[i];
48
60
  if (a === '--baseline') {
@@ -53,6 +65,8 @@ export function parseArgv(argv = []) {
53
65
  }
54
66
  } else if (a === '--json') {
55
67
  json = true;
68
+ } else if (a === '--production') {
69
+ production = true;
56
70
  } else if (a === '--knip-output') {
57
71
  const next = argv[i + 1];
58
72
  if (next && !next.startsWith('--')) {
@@ -61,7 +75,7 @@ export function parseArgv(argv = []) {
61
75
  }
62
76
  }
63
77
  }
64
- return { baselinePath, json, knipOutputPath };
78
+ return { baselinePath, json, knipOutputPath, production };
65
79
  }
66
80
 
67
81
  /**
@@ -84,36 +98,6 @@ export function loadBaseline(baselinePath) {
84
98
  }
85
99
  }
86
100
 
87
- /**
88
- * Pure helper: normalize knip's `--reporter json` output into a flat array of
89
- * `{ file, symbol }` rows. Knip emits `{ issues: [{ file, exports: [{ name, ... }], ... }, ...] }`.
90
- * Only `exports` rows are mapped — the dead-export ratchet ignores file-level,
91
- * dependency-level, and duplicate-level issues (knip surfaces those via
92
- * separate `rules` keys).
93
- *
94
- * @param {unknown} knipEnvelope The parsed knip JSON report.
95
- * @returns {Array<{ file: string, symbol: string }>}
96
- */
97
- export function extractRowsFromKnip(knipEnvelope) {
98
- const rows = [];
99
- if (!knipEnvelope || typeof knipEnvelope !== 'object') return rows;
100
- const issues = Array.isArray(knipEnvelope.issues) ? knipEnvelope.issues : [];
101
- for (const issue of issues) {
102
- const file = issue?.file;
103
- if (typeof file !== 'string' || file.length === 0) continue;
104
- const exports_ = Array.isArray(issue.exports) ? issue.exports : [];
105
- for (const e of exports_) {
106
- const symbol =
107
- (e && typeof e.name === 'string' && e.name) ||
108
- (e && typeof e.symbol === 'string' && e.symbol) ||
109
- null;
110
- if (!symbol) continue;
111
- rows.push({ file, symbol });
112
- }
113
- }
114
- return rows;
115
- }
116
-
117
101
  /**
118
102
  * Pure helper: diff two `{ file, symbol }` row sets. Returns `added` (in
119
103
  * current but not baseline) and `removed` (in baseline but not current).
@@ -151,75 +135,24 @@ export function diffRows(baselineRows, currentRows) {
151
135
  * "no drift" signal. When added rows are present the summary includes a
152
136
  * "(gate fail)" marker so the ratchet violation is visible in CI output.
153
137
  *
138
+ * `label` distinguishes the two passes in CI logs, which run back to back and
139
+ * would otherwise emit two identical-looking summaries.
140
+ *
154
141
  * @param {{ added: Array, removed: Array }} diff
142
+ * @param {string} [label='dead-exports']
155
143
  * @returns {string}
156
144
  */
157
- export function renderDiff(diff) {
145
+ export function renderDiff(diff, label = 'dead-exports') {
158
146
  const lines = [];
159
147
  for (const r of diff.added) lines.push(`+ ${r.file}: ${r.symbol}`);
160
148
  for (const r of diff.removed) lines.push(`- ${r.file}: ${r.symbol}`);
161
149
  const tag = diff.added.length > 0 ? '(gate fail)' : '(ok)';
162
150
  lines.push(
163
- `[dead-exports] added=${diff.added.length} removed=${diff.removed.length} ${tag}`,
151
+ `[${label}] added=${diff.added.length} removed=${diff.removed.length} ${tag}`,
164
152
  );
165
153
  return lines.join('\n');
166
154
  }
167
155
 
168
- /**
169
- * Spawn `npx knip --reporter json --no-progress` and return the parsed
170
- * envelope. Returns `null` on spawn / parse failure — the caller logs the
171
- * underlying error and falls back to treating current rows as empty (which
172
- * surfaces every baseline row as "removed", a loud-but-safe signal).
173
- *
174
- * Exported as a hook so tests can stub the spawn without setting up a
175
- * functioning knip workspace.
176
- *
177
- * @param {{ cwd?: string, spawn?: typeof spawnSync }} [opts]
178
- * @returns {{ ok: true, envelope: unknown } | { ok: false, error: string }}
179
- */
180
- export function runKnip({ cwd = process.cwd(), spawn = spawnSync } = {}) {
181
- const result = spawn(
182
- process.platform === 'win32' ? 'npx.cmd' : 'npx',
183
- ['knip', '--reporter', 'json', '--no-progress'],
184
- {
185
- cwd,
186
- stdio: ['ignore', 'pipe', 'pipe'],
187
- encoding: 'utf-8',
188
- shell: process.platform === 'win32',
189
- },
190
- );
191
- if (result.error) {
192
- return { ok: false, error: `spawn failed: ${result.error.message}` };
193
- }
194
- const stdout = typeof result.stdout === 'string' ? result.stdout : '';
195
- if (stdout.trim().length === 0) {
196
- return { ok: false, error: 'knip produced empty stdout' };
197
- }
198
- try {
199
- return { ok: true, envelope: JSON.parse(stdout) };
200
- } catch (err) {
201
- return {
202
- ok: false,
203
- error: `knip JSON parse failed: ${err?.message ?? err}`,
204
- };
205
- }
206
- }
207
-
208
- /**
209
- * Read a pre-captured knip JSON envelope from disk (for the `--knip-output`
210
- * test seam). Returns the parsed envelope or `null` on failure.
211
- *
212
- * @param {string} filePath
213
- * @returns {unknown}
214
- */
215
- function readKnipOutput(filePath) {
216
- try {
217
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
218
- } catch {
219
- return null;
220
- }
221
- }
222
-
223
156
  /**
224
157
  * Top-level CLI entry. Exported so tests can drive the full pipeline through
225
158
  * injected hooks without spawning knip.
@@ -242,10 +175,15 @@ export async function runCli({
242
175
  runKnipImpl = runKnip,
243
176
  loadBaselineImpl = loadBaseline,
244
177
  } = {}) {
245
- const { baselinePath, json, knipOutputPath } = parseArgv(argv);
178
+ const { baselinePath, json, knipOutputPath, production } = parseArgv(argv);
179
+ const {
180
+ mode,
181
+ label,
182
+ baseline: defaultBaseline,
183
+ } = resolveDeadExportsMode(production);
246
184
  const resolvedBaselinePath = path.resolve(
247
185
  cwd,
248
- baselinePath ?? path.join('baselines', 'dead-exports.json'),
186
+ baselinePath ?? defaultBaseline,
249
187
  );
250
188
  const baseline = loadBaselineImpl(resolvedBaselinePath);
251
189
  const baselineRows = Array.isArray(baseline?.rows) ? baseline.rows : [];
@@ -256,7 +194,7 @@ export async function runCli({
256
194
  knipEnvelope = readKnipOutput(path.resolve(cwd, knipOutputPath));
257
195
  if (!knipEnvelope) knipError = `failed to read ${knipOutputPath}`;
258
196
  } else {
259
- const result = runKnipImpl({ cwd });
197
+ const result = runKnipImpl({ cwd, production });
260
198
  if (result.ok) {
261
199
  knipEnvelope = result.envelope;
262
200
  } else {
@@ -275,6 +213,7 @@ export async function runCli({
275
213
  if (json) {
276
214
  const envelope = {
277
215
  kind: 'dead-exports-report',
216
+ mode,
278
217
  baselinePath: resolvedBaselinePath,
279
218
  baselineRows,
280
219
  currentRows,
@@ -287,14 +226,14 @@ export async function runCli({
287
226
  } else {
288
227
  if (!baseline) {
289
228
  stderr.write(
290
- `[dead-exports] ⚠ baseline not found at ${resolvedBaselinePath} — treating as empty\n`,
229
+ `[${label}] ⚠ baseline not found at ${resolvedBaselinePath} — treating as empty\n`,
291
230
  );
292
231
  }
293
232
  if (knipError) {
294
- stderr.write(`[dead-exports] ⚠ knip run failed: ${knipError}\n`);
233
+ stderr.write(`[${label}] ⚠ knip run failed: ${knipError}\n`);
295
234
  }
296
- stdout.write(`\n--- dead-exports preview ---\n`);
297
- stdout.write(`${renderDiff(diff)}\n`);
235
+ stdout.write(`\n--- ${label} preview ---\n`);
236
+ stdout.write(`${renderDiff(diff, label)}\n`);
298
237
  }
299
238
 
300
239
  return exitCode;
@@ -257,8 +257,8 @@ function stripAnchorAndQuery(target) {
257
257
  // A slash token is `/<name>` where:
258
258
  // - the preceding char is NOT a word char, `/`, `:`, `.`, `>`, `]`, `)`,
259
259
  // so we don't match URL path segments or fragments embedded in paths
260
- // like `temp/epic-<id>/lifecycle.ndjson` (preceded by `>`) or
261
- // `temp/epic-[ID]/tickets.json` (preceded by `]`).
260
+ // like `temp/run-<id>/lifecycle.ndjson` (preceded by `>`) or
261
+ // `temp/run-[ID]/tickets.json` (preceded by `]`).
262
262
  // - the following char is NOT a word char, `-`, or `.`, so file
263
263
  // extensions like `/tickets.json` and identifier suffixes don't match.
264
264
  // The optional `(?::[a-z][a-z0-9-]*)?` tail captures the namespaced
@@ -17,7 +17,7 @@
17
17
  * Any module under `.agents/scripts/lib/orchestration/lifecycle/listeners/**`
18
18
  * that calls `bus.on('*', …)` MUST NOT import a side-effecting module.
19
19
  * The static blocklist is small (the modules that mutate GitHub state,
20
- * the worktree, or write outside `temp/epic-<id>/`); we match by module
20
+ * the worktree, or write outside `temp/run-<id>/`); we match by module
21
21
  * specifier suffix to keep the rule simple and stable.
22
22
  *
23
23
  * Rule 3 — "Auto-merge lockout" (Story #2253 / Task #2255, Epic #2172
@@ -61,6 +61,19 @@ const BACKTICK_CALL_RE = /`[A-Za-z_$][\w$]*\s*\(/;
61
61
  const EXPORTED_FROM_RE = /\bexported from\b/;
62
62
  const LIB_IMPORT_RE = /\b(?:import|require)\(\s*['"`][^'"`]*scripts\/lib\//;
63
63
 
64
+ /** A markdown table row (leading pipe, optionally indented). */
65
+ const TABLE_ROW_RE = /^\s*\|/;
66
+ /** A markdown heading of any level — the lookback boundary for Rule 4. */
67
+ const HEADING_RE = /^\s*#{1,6}\s/;
68
+ /** A backticked `--flag` occupying a table row's first cell. */
69
+ const FLAG_CELL_RE = /^\s*\|\s*`?--[\w-]+/;
70
+ /** A `.js` script filename — bare or path-qualified. */
71
+ const SCRIPT_NAME_RE = /([\w./-]*[\w-]+\.js)\b/;
72
+ /** A markdown table's delimiter row (`| --- | --- |`). */
73
+ const TABLE_DELIMITER_RE = /^\s*\|[\s|:-]+\|?\s*$/;
74
+ /** Minimum `--flag` rows before a table counts as a flag *table*. */
75
+ export const MIN_FLAG_ROWS = 2;
76
+
64
77
  /**
65
78
  * Strip fenced code blocks (``` / ~~~), replacing their lines with empty
66
79
  * strings so line numbers stay stable. Complete runnable commands live in
@@ -106,6 +119,82 @@ export function toParagraphs(lines) {
106
119
  return paragraphs;
107
120
  }
108
121
 
122
+ /**
123
+ * Rule 4 — CLI flag table. Find markdown tables that enumerate the `--flags`
124
+ * of a script the repo already owns, and flag them as duplicated surface.
125
+ *
126
+ * The script's own argument parser is the source of truth for its flags, and
127
+ * it prints them itself; a copy in prose is drift waiting to happen (the very
128
+ * drift Story #4546 removed). Prose must point at the command instead.
129
+ *
130
+ * The signal is deliberately narrow, to stay at zero false positives:
131
+ *
132
+ * 1. The table has at least `MIN_FLAG_ROWS` data rows whose **first cell**
133
+ * is a `--flag`. One flag row among prose rows is a contract/behaviour
134
+ * table (e.g. "Default | …", "`--dry-run` | …"), not a flag enumeration.
135
+ * 2. A `.js` script is named between the table and the nearest preceding
136
+ * heading (inclusive) — i.e. the section is *about* that script, so the
137
+ * table is restating its surface.
138
+ *
139
+ * A slash command's own argument table (`/plan`, `/deliver`) has no script
140
+ * behind it — nothing owns those flags but the workflow prose itself — so
141
+ * condition 2 leaves it alone by design.
142
+ *
143
+ * @param {string[]} rawLines unstripped source lines (fenced commands are the
144
+ * usual place a script is named, so this rule reads the original text).
145
+ * @returns {Array<{ rule: string, line: number, hint: string }>}
146
+ */
147
+ export function lintFlagTables(rawLines) {
148
+ const violations = [];
149
+ let inFence = false;
150
+
151
+ for (let i = 0; i < rawLines.length; i++) {
152
+ if (/^\s*(```|~~~)/.test(rawLines[i])) {
153
+ inFence = !inFence;
154
+ continue;
155
+ }
156
+ if (inFence || !TABLE_ROW_RE.test(rawLines[i])) continue;
157
+
158
+ // Collect the contiguous table block starting here.
159
+ const start = i;
160
+ let end = i;
161
+ while (end + 1 < rawLines.length && TABLE_ROW_RE.test(rawLines[end + 1])) {
162
+ end += 1;
163
+ }
164
+ i = end; // continue scanning after this table
165
+
166
+ const flagRows = rawLines
167
+ .slice(start, end + 1)
168
+ .filter((l) => !TABLE_DELIMITER_RE.test(l) && FLAG_CELL_RE.test(l));
169
+ if (flagRows.length < MIN_FLAG_ROWS) continue;
170
+
171
+ // Look back to the nearest heading (inclusive) for a named script.
172
+ let script = null;
173
+ for (let j = start - 1; j >= 0; j--) {
174
+ const m = SCRIPT_NAME_RE.exec(rawLines[j]);
175
+ if (m) {
176
+ script = m[1];
177
+ break;
178
+ }
179
+ if (HEADING_RE.test(rawLines[j])) break;
180
+ }
181
+ if (!script) continue;
182
+
183
+ const base = script.split('/').pop();
184
+ violations.push({
185
+ rule: 'no-cli-flag-table',
186
+ line: start + 1,
187
+ hint:
188
+ `Workflow prose restates the flag surface of \`${base}\` as a table (${flagRows.length} flag rows). ` +
189
+ "The script's argument parser owns those flags, so a prose copy is drift waiting to happen. " +
190
+ `Delete the table and point at the command (\`node .agents/scripts/${base} …\`), keeping only the ` +
191
+ 'judgement a reader cannot get from the command itself. If the script has no help output to point ' +
192
+ 'at, add one — do not re-inline the table.',
193
+ });
194
+ }
195
+ return violations;
196
+ }
197
+
109
198
  /**
110
199
  * Lint one markdown source. Returns violations
111
200
  * `{ rule, line, hint }[]` (empty when clean).
@@ -115,7 +204,9 @@ export function toParagraphs(lines) {
115
204
  */
116
205
  export function lintWorkflowSource(source) {
117
206
  const violations = [];
207
+ const rawLines = source.split('\n');
118
208
  const lines = stripFences(source);
209
+ violations.push(...lintFlagTables(rawLines));
119
210
  for (const para of toParagraphs(lines)) {
120
211
  if (IMPERATIVE_CALL_RE.test(para.text)) {
121
212
  violations.push({