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
@@ -6,16 +6,19 @@
6
6
  * without importing upward from a top-level CLI file.
7
7
  *
8
8
  * Pure (modulo `gitSpawn`) — exposed helpers are:
9
- * - matchesFilePattern — single file × single glob (picomatch with `dot`)
10
- * - matchesAnyFilePattern — file list × pattern list, short-circuiting
11
- * - selectAudits main entry; reads audit-rules.json, runs `git
12
- * diff --name-only`, applies keyword + glob rules.
9
+ * - matchesFilePattern — single file × single glob (picomatch, `dot`)
10
+ * - matchesAnyFilePattern — file list × pattern list, short-circuiting
11
+ * - selectSensitivePathClasses change set × the manifest's `sensitivePaths`
12
+ * block; the review-depth derivation's matcher
13
+ * - selectAudits — main entry; reads audit-rules.json, runs `git
14
+ * diff --name-only`, applies keyword + glob
15
+ * rules.
13
16
  *
14
17
  * All rule-matching lives here; the former `select-audits.js` CLI wrapper
15
18
  * was retired in #4482 (consumers call `selectAudits` via the barrel).
16
19
  */
17
20
 
18
- import { readFileSync } from 'node:fs';
21
+ import { readdirSync, readFileSync } from 'node:fs';
19
22
  import fs from 'node:fs/promises';
20
23
  import path from 'node:path';
21
24
  import picomatch from 'picomatch';
@@ -197,6 +200,50 @@ export function selectLocalLenses({
197
200
  return selected;
198
201
  }
199
202
 
203
+ /**
204
+ * Select the sensitive-path classes a change set touches, from the
205
+ * `sensitivePaths` block of [`audit-rules.json`](../../../schemas/audit-rules.json).
206
+ *
207
+ * This is the matcher behind the close-time review-depth derivation
208
+ * (`review-depth.js#deriveChangeLevel`, Story #4542): a change set intersecting
209
+ * any registered class's globs is *observably* sensitive — auth, a migration, a
210
+ * billing path — and earns a deep pass however narrow the diff is. The classes
211
+ * and their globs live in the manifest next to the lens `triggers.filePatterns`
212
+ * precisely so an operator can extend them without a code change, and matching
213
+ * runs through the same {@link matchesAnyFilePattern} picomatch machinery rather
214
+ * than a second matcher with its own glob semantics.
215
+ *
216
+ * An empty change set, an absent `sensitivePaths` block, or a class whose globs
217
+ * match nothing all contribute no class — the caller then resolves depth from
218
+ * diff width alone.
219
+ *
220
+ * Pure over its injected seam: `injectedRules` skips the manifest disk read.
221
+ * Selection order follows the manifest's declaration order, which is
222
+ * deterministic.
223
+ *
224
+ * @param {{
225
+ * changedFiles?: string[],
226
+ * injectedRules?: { sensitivePaths?: Record<string, { filePatterns?: string[] }> },
227
+ * }} [params]
228
+ * @returns {string[]} The matched class names, in manifest order.
229
+ */
230
+ export function selectSensitivePathClasses({
231
+ changedFiles,
232
+ injectedRules,
233
+ } = {}) {
234
+ const files = Array.isArray(changedFiles) ? changedFiles : [];
235
+ if (files.length === 0) return [];
236
+
237
+ const rules = injectedRules ?? readAuditRulesSync();
238
+ const matched = [];
239
+ for (const [name, entry] of Object.entries(rules.sensitivePaths ?? {})) {
240
+ if (matchesAnyFilePattern(entry?.filePatterns ?? [], files)) {
241
+ matched.push(name);
242
+ }
243
+ }
244
+ return matched;
245
+ }
246
+
200
247
  /**
201
248
  * Resolve the consumer's navigability route globs from the resolved config.
202
249
  * Reads `delivery.quality.navigability.routeGlobs` — the route-tree SSOT the
@@ -221,9 +268,9 @@ export function resolveNavigabilityRouteGlobs(config) {
221
268
  * `false` (the unconfigured no-op), so the existing change-set-scoped lens
222
269
  * selection is unchanged.
223
270
  *
224
- * This is a pure predicate over the SAME inputs the existing risk-routed-lens
225
- * union already consumes; the caller folds its result into `riskRoutedAudits`
226
- * via the existing `unionAudits` no new routing machinery is added.
271
+ * A pure predicate over a change set the same input {@link selectAudits} and
272
+ * {@link selectLocalLenses} already match against. The caller unions its result
273
+ * into the lens roster it is assembling; no new routing machinery is added.
227
274
  *
228
275
  * @param {{ changedFiles?: string[], config?: object|null }} params
229
276
  * @returns {boolean}
@@ -234,6 +281,206 @@ export function routesNavigabilityLens({ changedFiles, config } = {}) {
234
281
  return matchesAnyFilePattern(globs, changedFiles ?? []);
235
282
  }
236
283
 
284
+ /**
285
+ * Package names (or scope/name segments of them) that declare a **web
286
+ * rendering surface**. Matched against the consumer's root `package.json`
287
+ * `dependencies` + `devDependencies` keys, segment-wise, so `@angular/core`
288
+ * matches via `angular` and `@remix-run/react` via `react`.
289
+ */
290
+ const WEB_FRAMEWORK_PACKAGES = Object.freeze([
291
+ 'react',
292
+ 'next',
293
+ 'vue',
294
+ 'svelte',
295
+ 'sveltekit',
296
+ 'astro',
297
+ 'nuxt',
298
+ 'remix',
299
+ 'remix-run',
300
+ 'gatsby',
301
+ 'angular',
302
+ ]);
303
+
304
+ /** File extensions whose presence in the source tree implies a web surface. */
305
+ const WEB_ASSET_EXTENSIONS = Object.freeze(['.html', '.css', '.jsx', '.tsx']);
306
+
307
+ /**
308
+ * Directories the web-surface file scan never descends into. `node_modules` is
309
+ * the load-bearing one (a dependency's bundled `.css` says nothing about the
310
+ * consumer's own surface); the test directories are excluded because a fixture
311
+ * `.html` is a test artifact, not a shipped page.
312
+ */
313
+ const WEB_SCAN_SKIP_DIRS = Object.freeze([
314
+ 'node_modules',
315
+ '.git',
316
+ '.worktrees',
317
+ 'dist',
318
+ 'build',
319
+ 'out',
320
+ 'coverage',
321
+ 'temp',
322
+ 'tmp',
323
+ 'tests',
324
+ 'test',
325
+ '__tests__',
326
+ '__mocks__',
327
+ 'spec',
328
+ 'e2e',
329
+ 'fixtures',
330
+ ]);
331
+
332
+ /** Scan bounds — the probe is a cheap heuristic, not an exhaustive crawl. */
333
+ const WEB_SCAN_MAX_DEPTH = 6;
334
+ const WEB_SCAN_MAX_ENTRIES = 4000;
335
+
336
+ /**
337
+ * Process-lifetime memo for the filesystem half of the web-surface probe,
338
+ * keyed by project root. The config half ({@link resolveNavigabilityRouteGlobs})
339
+ * is a free object read and is deliberately NOT memoized, so a caller passing a
340
+ * different config never reads a stale answer.
341
+ *
342
+ * @type {Map<string, boolean>}
343
+ */
344
+ const _webSurfaceFsCache = new Map();
345
+
346
+ /** Test-only: drop the memo so a fixture root can be re-probed after edits. */
347
+ export function _resetWebSurfaceCache() {
348
+ _webSurfaceFsCache.clear();
349
+ }
350
+
351
+ /**
352
+ * True when the root `package.json` declares a web-framework dependency.
353
+ * Returns `null` — *indeterminate* — when the manifest exists but cannot be
354
+ * read or parsed; the caller fails open on that (see {@link hasWebSurface}).
355
+ * A genuinely absent manifest (`ENOENT`) is determinate: there is no
356
+ * declaration, so there is no signal, and the file scan gets its turn.
357
+ *
358
+ * @param {string} root
359
+ * @returns {boolean|null}
360
+ */
361
+ function declaresWebFramework(root) {
362
+ let raw;
363
+ try {
364
+ raw = readFileSync(path.join(root, 'package.json'), 'utf8');
365
+ } catch (err) {
366
+ if (err?.code === 'ENOENT') return false;
367
+ return null;
368
+ }
369
+ let pkg;
370
+ try {
371
+ pkg = JSON.parse(raw);
372
+ } catch {
373
+ return null;
374
+ }
375
+ const names = [
376
+ ...Object.keys(pkg?.dependencies ?? {}),
377
+ ...Object.keys(pkg?.devDependencies ?? {}),
378
+ ];
379
+ return names.some((name) =>
380
+ name
381
+ .replace(/^@/, '')
382
+ .split('/')
383
+ .some((segment) => WEB_FRAMEWORK_PACKAGES.includes(segment)),
384
+ );
385
+ }
386
+
387
+ /**
388
+ * True when a bounded scan of the source tree finds a web asset file
389
+ * (`.html` / `.css` / `.jsx` / `.tsx`) outside the skipped directories.
390
+ * Returns `null` — *indeterminate* — when the root itself is unreadable, or
391
+ * when the scan exhausted its entry budget without finding one: a truncated
392
+ * scan genuinely did not finish looking, and reporting "no web surface" from
393
+ * a half-walked tree would silently drop lens coverage.
394
+ *
395
+ * Uses `readdirSync(withFileTypes)` rather than `git ls-files` — a filesystem
396
+ * check suffices, and shelling out to git would make the probe cost a process
397
+ * spawn on every selection.
398
+ *
399
+ * @param {string} root
400
+ * @returns {boolean|null}
401
+ */
402
+ function scanForWebAssets(root) {
403
+ let budget = WEB_SCAN_MAX_ENTRIES;
404
+ const queue = [{ dir: root, depth: 0 }];
405
+ let rootRead = false;
406
+
407
+ while (queue.length > 0) {
408
+ const { dir, depth } = queue.shift();
409
+ let entries;
410
+ try {
411
+ entries = readdirSync(dir, { withFileTypes: true });
412
+ } catch {
413
+ // A single unreadable subdirectory is skipped: a permissions oddity is
414
+ // far likelier than an entire web surface hiding behind it. An
415
+ // unreadable ROOT is a different story — see below.
416
+ if (dir === root) return null;
417
+ continue;
418
+ }
419
+ if (dir === root) rootRead = true;
420
+
421
+ for (const entry of entries) {
422
+ if (budget-- <= 0) return null; // truncated ⇒ indeterminate
423
+ if (entry.isDirectory()) {
424
+ if (WEB_SCAN_SKIP_DIRS.includes(entry.name)) continue;
425
+ if (depth + 1 <= WEB_SCAN_MAX_DEPTH) {
426
+ queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
427
+ }
428
+ continue;
429
+ }
430
+ if (WEB_ASSET_EXTENSIONS.includes(path.extname(entry.name))) return true;
431
+ }
432
+ }
433
+ return rootRead ? false : null;
434
+ }
435
+
436
+ /**
437
+ * Decide whether the project has a **web surface** — the applicability
438
+ * predicate behind the `target: "web"` gate in `audit-rules.json` (#4579).
439
+ *
440
+ * Mandrel is materialized into other projects, so this cannot be a property of
441
+ * *this* repository: a consumer with a real web surface MUST still get the web
442
+ * lenses. It is therefore derived from observables in the consumer's own
443
+ * checkout, and deliberately NOT from a new `.agentrc` key — a new key would
444
+ * have to be threaded through the runtime AJV (`lib/config-settings-schema*.js`)
445
+ * *and* the published mirror, and would make consumers hand-configure something
446
+ * already derivable.
447
+ *
448
+ * A project is web-capable when ANY of these hold:
449
+ * 1. `delivery.quality.navigability.routeGlobs` is configured — the existing
450
+ * web signal the navigability lens already routes off.
451
+ * 2. The root `package.json` declares a web framework (react / next / vue /
452
+ * svelte / astro / nuxt / remix / gatsby / angular).
453
+ * 3. A bounded source scan finds a `.html` / `.css` / `.jsx` / `.tsx` file
454
+ * outside test directories.
455
+ *
456
+ * **Fail direction: OPEN.** When a signal is indeterminate — an unparseable
457
+ * `package.json`, an unreadable root, a scan that hit its entry budget — the
458
+ * project is treated as web-capable. A false positive costs one wasted lens
459
+ * run; a false negative silently drops audit coverage on a project that has a
460
+ * real web surface, and nothing downstream would ever report the omission.
461
+ * Wasted spend is recoverable; dropped coverage is not.
462
+ *
463
+ * @param {{ config?: object|null, projectRoot?: string }} [params]
464
+ * @returns {boolean}
465
+ */
466
+ export function hasWebSurface({ config, projectRoot = PROJECT_ROOT } = {}) {
467
+ if (resolveNavigabilityRouteGlobs(config).length > 0) return true;
468
+
469
+ if (_webSurfaceFsCache.has(projectRoot)) {
470
+ return _webSurfaceFsCache.get(projectRoot);
471
+ }
472
+
473
+ const declared = declaresWebFramework(projectRoot);
474
+ // `null` is indeterminate, not false — fail open.
475
+ const result =
476
+ declared === null || declared === true
477
+ ? true
478
+ : scanForWebAssets(projectRoot) !== false;
479
+
480
+ _webSurfaceFsCache.set(projectRoot, result);
481
+ return result;
482
+ }
483
+
237
484
  /**
238
485
  * Test a single filename against a single glob pattern using the project's
239
486
  * configured matcher semantics (`picomatch` with `dot: true`). Exported so
@@ -261,6 +508,16 @@ export function matchesAnyFilePattern(patterns, files) {
261
508
  * @param {number} params.ticketId
262
509
  * @param {string} params.gate
263
510
  * @param {import('../ITicketingProvider.js').ITicketingProvider} params.provider
511
+ * @param {string[]} [params.changedFiles]
512
+ * The change set to select over, when the caller already knows it. Supplying
513
+ * it skips the internal `git diff` entirely and `baseBranch` / `headRef` are
514
+ * then unused. Callers whose change set is NOT the working tree's
515
+ * `baseBranch...headRef` MUST pass this: the plan-run epilogue runs from the
516
+ * main checkout *after* its Stories have merged, where any `main...HEAD`
517
+ * range is empty by construction and selection would silently degrade to
518
+ * keyword-only matching (Story #4571). An empty array is honoured as
519
+ * "the set is known, and it is empty" — distinct from omitting the option,
520
+ * which means "derive it from git".
264
521
  * @param {string} [params.baseBranch]
265
522
  * @param {string} [params.headRef]
266
523
  * Git ref whose diff-against-`baseBranch` defines the change set. Defaults
@@ -284,6 +541,9 @@ export function matchesAnyFilePattern(patterns, files) {
284
541
  * Test-only seam to drive the `--gate-mode` / `MANDREL_GATE_MODE=1`
285
542
  * detection; production callers leave unset and `isGateMode` reads
286
543
  * `process.argv` / `process.env`.
544
+ * @param {typeof hasWebSurface} [params.hasWebSurfaceFn]
545
+ * Test-only seam overriding the web-surface probe behind the
546
+ * `target: "web"` applicability gate. Production callers leave unset.
287
547
  *
288
548
  * Returns either the success envelope (`{ selectedAudits, ticketId, gate, context }`)
289
549
  * OR the degraded envelope (`{ ok: false, degraded: true, reason, detail }`)
@@ -294,11 +554,13 @@ export async function selectAudits({
294
554
  ticketId,
295
555
  gate,
296
556
  provider,
557
+ changedFiles: injectedChangedFiles,
297
558
  baseBranch = 'main',
298
559
  headRef = 'HEAD',
299
560
  injectedGitSpawn,
300
561
  gitTimeoutMsOverride,
301
562
  gateModeOpts,
563
+ hasWebSurfaceFn = hasWebSurface,
302
564
  }) {
303
565
  const config = resolveConfig();
304
566
  const timeoutMs = gitTimeoutMsOverride ?? DEFAULT_GIT_TIMEOUT_MS;
@@ -323,6 +585,14 @@ export async function selectAudits({
323
585
 
324
586
  const runGit = injectedGitSpawn ?? (async (...args) => gitSpawn(...args));
325
587
 
588
+ // A caller that already knows the change set supplies it, and no git ref is
589
+ // consulted at all — `baseBranch` / `headRef` describe a diff that is not
590
+ // being taken. The plan-run epilogue is the motivating caller (Story #4571):
591
+ // it resolves the run's landed diff from the Stories' squash-merges, and any
592
+ // `main...HEAD` range it could name from the main checkout post-land is
593
+ // empty by construction.
594
+ const hasInjectedChangedFiles = Array.isArray(injectedChangedFiles);
595
+
326
596
  // Resolve `headRef` to a commit before diffing. A non-default `headRef`
327
597
  // (Epic-mode callers pass `refs/heads/epic/<id>`) that the repo can't
328
598
  // resolve means the requested Epic's branch is not present in this
@@ -331,7 +601,7 @@ export async function selectAudits({
331
601
  // degraded signal instead of leaking the wrong scope. `HEAD` is always
332
602
  // resolvable in a valid repo, so the default-path callers skip the probe
333
603
  // cost on the common case.
334
- if (headRef !== 'HEAD') {
604
+ if (!hasInjectedChangedFiles && headRef !== 'HEAD') {
335
605
  let resolved;
336
606
  try {
337
607
  resolved = await withTimeout(
@@ -358,18 +628,22 @@ export async function selectAudits({
358
628
  }
359
629
  }
360
630
 
361
- let changedFiles = [];
631
+ let changedFiles = hasInjectedChangedFiles
632
+ ? injectedChangedFiles.map((f) => String(f).trim()).filter(Boolean)
633
+ : [];
362
634
  try {
363
- const diff = await withTimeout(
364
- runGit(
365
- process.cwd(),
366
- 'diff',
367
- '--name-only',
368
- `${baseBranch}...${headRef}`,
369
- ),
370
- timeoutMs,
371
- { label: 'select-audits git diff' },
372
- );
635
+ const diff = hasInjectedChangedFiles
636
+ ? null
637
+ : await withTimeout(
638
+ runGit(
639
+ process.cwd(),
640
+ 'diff',
641
+ '--name-only',
642
+ `${baseBranch}...${headRef}`,
643
+ ),
644
+ timeoutMs,
645
+ { label: 'select-audits git diff' },
646
+ );
373
647
  if (diff?.status === 0) {
374
648
  changedFiles = diff.stdout
375
649
  .split('\n')
@@ -393,16 +667,40 @@ export async function selectAudits({
393
667
 
394
668
  const selectedAudits = [];
395
669
 
670
+ // Resolved at most once per call, and only if a `target: "web"` lens
671
+ // actually clears its gate — a Node-only project must not pay a filesystem
672
+ // scan on a roster with no web lens in it.
673
+ let webSurfaceMemo = null;
674
+ const projectIsWebCapable = () => {
675
+ if (webSurfaceMemo === null) {
676
+ webSurfaceMemo = hasWebSurfaceFn({ config });
677
+ }
678
+ return webSurfaceMemo;
679
+ };
680
+
396
681
  for (const [auditName, ruleOpts] of Object.entries(rulesData.audits || {})) {
397
682
  const triggers = ruleOpts.triggers || {};
398
683
 
399
684
  const gateMatch = triggers.gates?.includes(gate);
400
685
  if (!gateMatch) continue;
401
686
 
687
+ // Target-applicability gate (#4579). A lens declaring `target: "web"`
688
+ // has nothing to read on a project with no web surface, yet still
689
+ // whole-word-matches ordinary prose — `audit-seo` fires on the `meta`
690
+ // inside every Story body's `<!-- meta: {...} -->` machine comment. The
691
+ // roster's own instruction is that the host MUST walk every listed lens,
692
+ // so an inapplicable entry is not just wasted spend: it teaches operators
693
+ // to ignore the MUST. An absent `target` means "always applicable", so no
694
+ // existing lens changes behaviour.
695
+ if (ruleOpts.target === 'web' && !projectIsWebCapable()) continue;
696
+
402
697
  const keywords = triggers.keywords || [];
403
698
  let keywordMatch = false;
404
699
  for (const kw of keywords) {
405
- if (contentToSearch.includes(kw.toLowerCase())) {
700
+ // Whole-word match: a bare substring test selects lenses on accidental
701
+ // fragments ("ui" inside "requires", "auth" inside "author" — #4579).
702
+ const escaped = kw.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
703
+ if (new RegExp(`\\b${escaped}\\b`).test(contentToSearch)) {
406
704
  keywordMatch = true;
407
705
  break;
408
706
  }
@@ -430,8 +728,10 @@ export async function selectAudits({
430
728
  changedFilesCount: changedFiles.length,
431
729
  // The ref the change set was actually diffed against. Epic-mode callers
432
730
  // assert this matches the requested Epic branch (Story #3362) so a
433
- // mis-pinned diff never reaches the audit-lens selector silently.
434
- resolvedRef: headRef,
731
+ // mis-pinned diff never reaches the audit-lens selector silently. `null`
732
+ // on the injected path: no ref was diffed, and naming one would invite
733
+ // exactly that assertion to pass against a diff nobody took.
734
+ resolvedRef: hasInjectedChangedFiles ? null : headRef,
435
735
  ticketTitle: ticket.title,
436
736
  },
437
737
  };
@@ -53,20 +53,9 @@ export const MAINTAINABILITY_EXCLUSIONS = Object.freeze(
53
53
  // escomplex: same "pattern" parse failure as acceptance-spec-reconciler;
54
54
  // both files share the lifecycle-lint regex-driven scan helpers.
55
55
  '.agents/scripts/check-lifecycle-lint.js',
56
- // escomplex: "traveler[node.type] is not a function" — AST node type the
57
- // upstream traveler table is missing (top-level await / private fields).
58
- '.agents/scripts/lib/orchestration/epic-cleanup.js',
59
- // escomplex: same "traveler[node.type] is not a function" — emitted by
60
- // the spec-reconciler's `async` operator-comment IIFE.
61
- '.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js',
62
56
  // escomplex: "this[node.callee.type] is not a function" — the cyclomatic
63
57
  // visitor lacks a handler for one of the quality-watch CLI's call shapes.
64
58
  '.agents/scripts/quality-watch.js',
65
- // escomplex: parse-fails (MI=0) on the spec-freshness phase's optional-
66
- // chained destructuring fallthrough. Same upstream family as the other
67
- // "pattern" / destructuring failures; carve-out so the phase split of
68
- // epic-plan-spec doesn't poison the global rollup.
69
- '.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js',
70
59
  // escomplex: "Cannot read properties of undefined (reading 'pattern')" —
71
60
  // the audit-to-stories parser reuses the same regex-property scan
72
61
  // patterns as acceptance-spec-reconciler.
@@ -13,14 +13,18 @@
13
13
  * framework default at the next bump. The push-to-main job still passes
14
14
  * `--full-scope` so a self-diff doesn't degrade to "no files in diff".
15
15
  *
16
- * 2. **Pass `--epic-ref` where the firing site is Epic-aware.** Inside
17
- * `/deliver` the close-validation chain already threads
18
- * `--epic-ref epic/<id>`; on CI the equivalent surface is the PR's
19
- * base branch. The template wires `--epic-ref ${EPIC_REF}` through an
20
- * env var the workflow computes from `github.head_ref` (story-N
21
- * branches name their Epic in the PR title or via the dispatch
22
- * manifest). When `EPIC_REF` is empty, both gates fall through to the
23
- * `main`-tracked baseline — so the env-var expansion is always safe.
16
+ * 2. **Pin the baseline read to the PR's base branch via `BASELINE_REF`.**
17
+ * Inside `/deliver` the close-validation chain sets the same env var
18
+ * (`close-validation/gates.js` `BASELINE_REF: origin/<baseBranch>`);
19
+ * on CI the equivalent surface is the PR's base branch. This is an
20
+ * ENV VAR, not a CLI flag: `check-baselines.js` resolves it through
21
+ * `resolveDispatchScope`, and the `--epic-ref` flag this template used
22
+ * to pass never existed on it it was left over from the separate
23
+ * `check-crap.js` / `check-maintainability.js` CLIs that were unified
24
+ * away, and it named the ref `epic/<base>`, a branch the Story-only
25
+ * model never creates. When `BASELINE_REF` is empty (push-to-base,
26
+ * where there is no base to compare against) both gates fall through to
27
+ * the working-tree baseline, so the expansion is always safe.
24
28
  *
25
29
  * The renderer is a pure string template so the integration test can assert
26
30
  * on its exact shape without instantiating a YAML parser.
@@ -61,19 +65,15 @@ export function renderCiWorkflow(opts = {}) {
61
65
  # Diff-scoped via the framework default (Story #1394 flipped
62
66
  # --changed-since to '${baseRef}'). On PRs we let the default win;
63
67
  # on push-to-main we pass --full-scope so a self-diff doesn't
64
- # collapse to an empty file set. EPIC_REF is set when the head ref
65
- # points at a story branch under an Epic — the gate then reads the
66
- # per-Epic baseline snapshot via baseline-loader.readBaselineAtRef.
68
+ # collapse to an empty file set. BASELINE_REF makes the gate read
69
+ # the baseline at the base branch tip via
70
+ # baseline-loader.readBaselineAtRef.
67
71
  env:
68
- EPIC_REF: \${{ env.EPIC_REF }}
72
+ BASELINE_REF: \${{ env.BASELINE_REF }}
69
73
  run: |
70
74
  mkdir -p temp
71
75
  if [[ "\${{ github.event_name }}" == "pull_request" ]]; then
72
- if [[ -n "\${EPIC_REF}" ]]; then
73
- npm run crap:check -- --epic-ref "\${EPIC_REF}" --json temp/crap-report.json
74
- else
75
- npm run crap:check -- --json temp/crap-report.json
76
- fi
76
+ npm run crap:check -- --json temp/crap-report.json
77
77
  else
78
78
  npm run crap:check -- --full-scope --json temp/crap-report.json
79
79
  fi
@@ -112,15 +112,14 @@ jobs:
112
112
  permissions:
113
113
  contents: read
114
114
  env:
115
- # Story #1120: when the PR head ref names an Epic story branch
116
- # (story-N or story/epic-<id>/<n>), the gate CLIs read the canonical
117
- # main baseline at the Epic branch HEAD via --epic-ref (git show).
118
- # Empty when the head ref is not an Epic story branch — both gates
119
- # fall through to the working-tree baseline, so the env-var
120
- # expansion is always safe. Per-epic ratchet snapshots themselves
121
- # live under temp/epic/<id>/baselines/ (Story #1467) — ephemeral
122
- # scratch state, never read by this CI gate.
123
- EPIC_REF: \${{ github.head_ref && contains(github.head_ref, 'story') && format('epic/{0}', github.event.pull_request.base.ref) || '' }}
115
+ # Story #1120: on a PR the gate CLIs read the canonical baseline at
116
+ # the BASE branch tip via --baseline-ref (git show), not from the
117
+ # PR head's working tree a branch that edited its own baseline
118
+ # must not be allowed to grade itself against the edit.
119
+ # Empty on push-to-${baseRef} (there is no base to compare against),
120
+ # where both gates fall through to the working-tree baseline, so the
121
+ # env-var expansion is always safe.
122
+ BASELINE_REF: \${{ github.event_name == 'pull_request' && format('origin/{0}', github.event.pull_request.base.ref) || '' }}
124
123
  BASE_REF: ${baseRef}
125
124
  steps:
126
125
  - name: Checkout Code
@@ -147,16 +146,12 @@ jobs:
147
146
  # the project's BASE_REF, so PRs no longer pass --changed-since
148
147
  # explicitly. push-to-${baseRef} runs with --full-scope so the
149
148
  # diff-scoped default doesn't degrade to a self-diff (= empty file
150
- # set). EPIC_REF carries the per-Epic snapshot when present.
149
+ # set). BASELINE_REF pins the baseline read to the base branch tip.
151
150
  env:
152
- EPIC_REF: \${{ env.EPIC_REF }}
151
+ BASELINE_REF: \${{ env.BASELINE_REF }}
153
152
  run: |
154
153
  if [[ "\${{ github.event_name }}" == "pull_request" ]]; then
155
- if [[ -n "\${EPIC_REF}" ]]; then
156
- npm run maintainability:check -- --epic-ref "\${EPIC_REF}"
157
- else
158
- npm run maintainability:check
159
- fi
154
+ npm run maintainability:check
160
155
  else
161
156
  npm run maintainability:check -- --full-scope
162
157
  fi
@@ -110,9 +110,14 @@ export function buildMutationManifest(ctx = {}) {
110
110
  const entries = [];
111
111
 
112
112
  // --- ide-wiring -------------------------------------------------------
113
- // Claude Code / IDE integration: the system-prompt import, the
114
- // UserPromptSubmit sync hook, the generated flat command surface, and the
115
- // .gitignore entry that keeps the generated tree out of git.
113
+ // Claude Code / IDE integration: the system-prompt import, the generated
114
+ // flat command surface, and the .gitignore entry that keeps the
115
+ // generated tree out of git. (Story #4527/#4530: the UserPromptSubmit
116
+ // per-prompt re-sync hook entry was removed alongside the hook itself —
117
+ // it raced the harness's own read of the same directory and reported
118
+ // "0 file(s) synced" on effectively every invocation; the real sync
119
+ // points — `prepare`, `mandrel sync`/`update`, doctor's
120
+ // `commands-in-sync` — already cover every case.)
116
121
  entries.push(
117
122
  {
118
123
  phaseGroup: PHASE_GROUPS.IDE_WIRING,
@@ -122,14 +127,6 @@ export function buildMutationManifest(ctx = {}) {
122
127
  'Wire the @.agents/instructions.md system-prompt import so Claude Code hydrates the framework on cold start.',
123
128
  reversible: true,
124
129
  },
125
- {
126
- phaseGroup: PHASE_GROUPS.IDE_WIRING,
127
- target: rel('.claude', 'settings.json'),
128
- action: 'merge',
129
- detail:
130
- 'Add the UserPromptSubmit re-sync hook so the /<command> tree stays current.',
131
- reversible: true,
132
- },
133
130
  {
134
131
  phaseGroup: PHASE_GROUPS.IDE_WIRING,
135
132
  target: rel('.claude', 'commands'),