mandrel 2.0.0 → 2.1.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 (311) hide show
  1. package/.agents/README.md +59 -28
  2. package/.agents/agents/acceptance-critic.md +9 -7
  3. package/.agents/agents/story-worker.md +41 -46
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +51 -44
  6. package/.agents/docs/agentrc-reference.json +8 -13
  7. package/.agents/docs/configuration.md +32 -56
  8. package/.agents/docs/execution-reference.md +39 -10
  9. package/.agents/docs/quality-gates.md +14 -16
  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/git-conventions-reference.md +42 -51
  14. package/.agents/schemas/agentrc.schema.json +34 -45
  15. package/.agents/schemas/audit-rules.json +59 -1
  16. package/.agents/schemas/audit-rules.schema.json +33 -1
  17. package/.agents/schemas/lifecycle/README.md +1 -2
  18. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  19. package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
  20. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
  21. package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
  22. package/.agents/schemas/signal-event.schema.json +3 -3
  23. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  24. package/.agents/schemas/validation-evidence.schema.json +1 -1
  25. package/.agents/scripts/acceptance-eval.js +22 -66
  26. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  27. package/.agents/scripts/bootstrap.js +3 -3
  28. package/.agents/scripts/check-dead-exports.js +43 -104
  29. package/.agents/scripts/check-doc-links.js +2 -2
  30. package/.agents/scripts/check-lifecycle-lint.js +1 -1
  31. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  32. package/.agents/scripts/deliver-recover.js +122 -0
  33. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  34. package/.agents/scripts/evidence-gate.js +20 -50
  35. package/.agents/scripts/generate-skills-index.js +17 -1
  36. package/.agents/scripts/generate-workflows-doc.js +4 -4
  37. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  38. package/.agents/scripts/lib/audit-suite/selector.js +323 -23
  39. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
  40. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  41. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  42. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  43. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  44. package/.agents/scripts/lib/checks/core-bare-clean.js +4 -1
  45. package/.agents/scripts/lib/checks/index.js +1 -1
  46. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  47. package/.agents/scripts/lib/checks/state.js +17 -248
  48. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +3 -3
  49. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  50. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  51. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  52. package/.agents/scripts/lib/cli-args.js +23 -2
  53. package/.agents/scripts/lib/close-validation/gates.js +13 -13
  54. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  55. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  56. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  57. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  58. package/.agents/scripts/lib/config/delivery-routing.js +7 -6
  59. package/.agents/scripts/lib/config/explain.js +10 -16
  60. package/.agents/scripts/lib/config/github.js +7 -5
  61. package/.agents/scripts/lib/config/limits.js +15 -25
  62. package/.agents/scripts/lib/config/quality.js +11 -14
  63. package/.agents/scripts/lib/config/runners.js +8 -21
  64. package/.agents/scripts/lib/config/temp-paths.js +18 -56
  65. package/.agents/scripts/lib/config-settings-schema-delivery.js +31 -13
  66. package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
  67. package/.agents/scripts/lib/config-settings-schema.js +48 -22
  68. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  69. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  70. package/.agents/scripts/lib/duplicate-search.js +38 -7
  71. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  72. package/.agents/scripts/lib/format-generated-json.js +97 -0
  73. package/.agents/scripts/lib/framework-version.js +19 -189
  74. package/.agents/scripts/lib/gh-exec.js +8 -0
  75. package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
  76. package/.agents/scripts/lib/git-utils.js +0 -14
  77. package/.agents/scripts/lib/json-utils.js +1 -2
  78. package/.agents/scripts/lib/label-constants.js +0 -15
  79. package/.agents/scripts/lib/label-taxonomy.js +1 -12
  80. package/.agents/scripts/lib/observability/active-story-env.js +42 -163
  81. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  82. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  83. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  84. package/.agents/scripts/lib/observability/source-classifier.js +2 -2
  85. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  86. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  87. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  88. package/.agents/scripts/lib/orchestration/ceremony-routing.js +43 -45
  89. package/.agents/scripts/lib/orchestration/code-review.js +58 -168
  90. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
  91. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  92. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  93. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  94. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  95. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  96. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +9 -11
  97. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  98. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  99. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +7 -3
  100. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  101. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  102. package/.agents/scripts/lib/orchestration/merge-block-class.js +76 -20
  103. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  104. package/.agents/scripts/lib/orchestration/plan-context.js +114 -24
  105. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +11 -22
  106. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +3 -7
  107. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  108. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
  109. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
  110. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  111. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -75
  112. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
  113. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
  114. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  115. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
  116. package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
  117. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  118. package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
  119. package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
  120. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  121. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  122. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  123. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  124. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  125. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  126. package/.agents/scripts/lib/orchestration/run-epilogue.js +374 -16
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
  128. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  129. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  130. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +3 -12
  131. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  132. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +72 -30
  133. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  134. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  135. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +12 -8
  136. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  137. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  138. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +264 -43
  139. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  140. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  141. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +15 -32
  142. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  143. package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
  144. package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
  145. package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
  146. package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
  147. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  148. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
  149. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
  150. package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
  151. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
  152. package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
  153. package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
  154. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  155. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  156. package/.agents/scripts/lib/planning-corpus.js +12 -286
  157. package/.agents/scripts/lib/preflight-runner.js +2 -2
  158. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  159. package/.agents/scripts/lib/signals/index.js +4 -17
  160. package/.agents/scripts/lib/signals/read.js +35 -35
  161. package/.agents/scripts/lib/signals/schema.js +8 -11
  162. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  163. package/.agents/scripts/lib/signals/write.js +0 -1
  164. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  165. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  166. package/.agents/scripts/lib/story-adjacency.js +8 -7
  167. package/.agents/scripts/lib/story-body/story-body.js +6 -5
  168. package/.agents/scripts/lib/templates/decomposer-prompts.js +7 -3
  169. package/.agents/scripts/lib/test-env.js +14 -1
  170. package/.agents/scripts/lib/test-tiers.js +0 -3
  171. package/.agents/scripts/lib/ticket-body-sections.js +0 -14
  172. package/.agents/scripts/lib/validation-evidence.js +31 -59
  173. package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
  174. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  175. package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
  176. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  177. package/.agents/scripts/plan-context.js +38 -6
  178. package/.agents/scripts/plan-persist.js +145 -35
  179. package/.agents/scripts/plan-run-epilogue.js +83 -38
  180. package/.agents/scripts/post-structured-comment.js +0 -38
  181. package/.agents/scripts/pr-watch-with-update.js +43 -22
  182. package/.agents/scripts/providers/github/compose.js +0 -1
  183. package/.agents/scripts/providers/github/errors.js +0 -19
  184. package/.agents/scripts/providers/github/issues.js +1 -11
  185. package/.agents/scripts/providers/github/mappers.js +5 -0
  186. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  187. package/.agents/scripts/providers/github/tickets.js +33 -153
  188. package/.agents/scripts/providers/github.js +17 -6
  189. package/.agents/scripts/resolve-stories.js +236 -0
  190. package/.agents/scripts/run-coverage.js +4 -1
  191. package/.agents/scripts/run-lint.js +2 -2
  192. package/.agents/scripts/run-verify.js +31 -2
  193. package/.agents/scripts/signals-view.js +9 -10
  194. package/.agents/scripts/single-story-close.js +173 -18
  195. package/.agents/scripts/single-story-confirm-merge.js +288 -15
  196. package/.agents/scripts/single-story-init.js +6 -10
  197. package/.agents/scripts/stories-wave-tick.js +79 -4
  198. package/.agents/scripts/story-plan.js +3 -3
  199. package/.agents/scripts/update-ticket-state.js +8 -50
  200. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  201. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  202. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  203. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  204. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  205. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  206. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  207. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  208. package/.agents/skills/core/idea-refinement/SKILL.md +3 -3
  209. package/.agents/skills/core/scope-triage/SKILL.md +3 -0
  210. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  211. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  212. package/.agents/skills/skills.index.json +2 -12
  213. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  214. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  215. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  216. package/.agents/workflows/audit-architecture.md +3 -4
  217. package/.agents/workflows/audit-clean-code.md +4 -4
  218. package/.agents/workflows/audit-documentation.md +4 -5
  219. package/.agents/workflows/audit-lighthouse.md +8 -0
  220. package/.agents/workflows/audit-navigability.md +10 -0
  221. package/.agents/workflows/audit-performance.md +2 -3
  222. package/.agents/workflows/audit-quality.md +8 -9
  223. package/.agents/workflows/audit-security.md +1 -2
  224. package/.agents/workflows/audit-seo.md +10 -0
  225. package/.agents/workflows/audit-ux-ui.md +7 -0
  226. package/.agents/workflows/deliver.md +98 -45
  227. package/.agents/workflows/git-cleanup.md +2 -2
  228. package/.agents/workflows/git-deliver.md +1 -1
  229. package/.agents/workflows/helpers/acceptance-self-eval.md +21 -13
  230. package/.agents/workflows/helpers/code-quality-guardrails.md +7 -7
  231. package/.agents/workflows/helpers/code-review.md +12 -10
  232. package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
  233. package/.agents/workflows/helpers/deliver-story.md +193 -118
  234. package/.agents/workflows/helpers/parallel-tooling.md +2 -2
  235. package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
  236. package/.agents/workflows/plan.md +184 -19
  237. package/.agents/workflows/qa-assist.md +6 -6
  238. package/.agents/workflows/qa-explore.md +3 -3
  239. package/.agents/workflows/qa-run.md +1 -5
  240. package/bin/mandrel.js +12 -1
  241. package/docs/CHANGELOG.md +40 -0
  242. package/lib/cli/registry.js +262 -19
  243. package/lib/cli/sync-agents.js +157 -0
  244. package/lib/cli/sync-commands.js +115 -6
  245. package/lib/cli/sync.js +168 -6
  246. package/lib/cli/update.js +105 -8
  247. package/lib/cli/version-helpers.js +131 -0
  248. package/lib/migrations/README.md +7 -5
  249. package/lib/migrations/index.js +12 -9
  250. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  251. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  252. package/package.json +1 -1
  253. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  254. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  255. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  256. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  257. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  258. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  259. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  260. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  261. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  262. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  263. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  264. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  265. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  266. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  267. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  268. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  269. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  270. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  271. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  272. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  273. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  274. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  275. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  276. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  277. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  278. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  279. package/.agents/schemas/risk-verdict.schema.json +0 -53
  280. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  281. package/.agents/scripts/analyze-execution.js +0 -444
  282. package/.agents/scripts/check-prepush-recovery.js +0 -90
  283. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  284. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  285. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -187
  286. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  287. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  288. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  289. package/.agents/scripts/lib/orchestration/audit-lens-routing.js +0 -128
  290. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
  291. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  292. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  293. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  294. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  295. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  296. package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
  297. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  298. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  299. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  300. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -21
  301. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  302. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  303. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  304. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -397
  305. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  306. package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
  307. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  308. package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
  309. package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
  310. package/.agents/scripts/resolve-plan-run.js +0 -117
  311. package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
@@ -21,6 +21,11 @@
21
21
  // --out <file> Override the manifest output path (defaults to
22
22
  // `<root>/.agents/skills/skills.index.json`).
23
23
  //
24
+ // Written output is passed through the project formatter (Biome) so that
25
+ // regenerating on a clean tree leaves no format drift behind; the step is
26
+ // best-effort and degrades to plain `JSON.stringify` output where Biome is
27
+ // not installed. See `lib/format-generated-json.js`.
28
+ //
24
29
  // Honors AGENT_LOG_LEVEL via the shared `Logger`. Stdout is reserved for
25
30
  // the diff text in --check failure mode; informational progress goes to
26
31
  // stderr.
@@ -30,6 +35,7 @@ import path from 'node:path';
30
35
  import { fileURLToPath } from 'node:url';
31
36
  import { parseStandardCliArgs } from './lib/cli/standard-args.js';
32
37
  import { runAsCli } from './lib/cli-utils.js';
38
+ import { formatGeneratedJson } from './lib/format-generated-json.js';
33
39
  import { Logger } from './lib/Logger.js';
34
40
  import { parseSkill } from './lib/skills/parse-skill.js';
35
41
  import { collectSkillFiles } from './lib/skills/walk-skill-files.js';
@@ -125,6 +131,12 @@ export function buildManifest(repoRoot, { nowIso } = {}) {
125
131
  * Serialize a manifest object as canonical JSON: 2-space indent,
126
132
  * trailing newline. Two runs against an unchanged corpus produce
127
133
  * byte-identical output modulo `generatedAt`.
134
+ *
135
+ * This is the *pre-format* shape. `JSON.stringify` expands every array
136
+ * across multiple lines, while Biome collapses short ones that fit
137
+ * inside `lineWidth` (`"allowedTools": ["Read", "Bash"]`). Writing this
138
+ * text verbatim therefore leaves the tree format-dirty on every run —
139
+ * see `lib/format-generated-json.js`, which reconciles the two.
128
140
  */
129
141
  export function serializeManifest(manifest) {
130
142
  return `${JSON.stringify(manifest, null, 2)}\n`;
@@ -233,8 +245,12 @@ export function run({ argv = [], now = new Date(), repoRoot } = {}) {
233
245
  }
234
246
 
235
247
  const serialized = serializeManifest(fresh);
248
+ const opts = { cwd: root, filename: 'skills.index.json' };
236
249
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
237
- fs.writeFileSync(outPath, serialized);
250
+ fs.writeFileSync(
251
+ outPath,
252
+ formatGeneratedJson(serialized, opts) ?? serialized,
253
+ );
238
254
  Logger.info(
239
255
  `wrote ${path.relative(root, outPath).split(path.sep).join('/')} (${fresh.skills.length} entries)`,
240
256
  );
@@ -91,10 +91,10 @@ export function renderWorkflowsDoc(catalog, loopCatalog = []) {
91
91
  'compose. This file is only for "which command does X?" lookups.',
92
92
  '',
93
93
  'Every command file lives at `.agents/workflows/<name>.md` and is projected',
94
- 'into a flat `.claude/commands/` tree by `npm run sync:commands` (the',
95
- 'UserPromptSubmit hook keeps it current) so it shows up as a bare `/<name>`',
96
- 'slash command (e.g. `/deliver`). The projection writes only',
97
- '`.claude/commands/<name>.md` — there is no plugin manifest and no',
94
+ 'into a flat `.claude/commands/` tree by `npm run sync:commands` (kept',
95
+ 'current at install time and on every `mandrel sync`/`update`) so it shows',
96
+ 'up as a bare `/<name>` slash command (e.g. `/deliver`). The projection',
97
+ 'writes only `.claude/commands/<name>.md` — there is no plugin manifest and no',
98
98
  'marketplace listing. The commands load in every Claude Code environment.',
99
99
  '',
100
100
  'Loop units are the one namespaced exception: files under',
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ITicketingProvider — Abstract Ticketing Provider Interface
3
3
  *
4
- * All ticketing interactions in the v5 Epic-centric orchestration are mediated
4
+ * All ticketing interactions in the v5 Story-centric orchestration are mediated
5
5
  * through this interface. Concrete implementations (e.g., `providers/github.js`)
6
6
  * extend this class and override every method.
7
7
  *
@@ -17,21 +17,6 @@ export class ITicketingProvider {
17
17
  // Read Operations
18
18
  // ---------------------------------------------------------------------------
19
19
 
20
- /**
21
- * Fetch all Epic tickets in the repository.
22
- *
23
- * @param {{ state?: 'open'|'closed'|'all' }} [filters={}]
24
- * @returns {Promise<Array<{
25
- * id: number,
26
- * title: string,
27
- * labels: string[],
28
- * state: 'open'|'closed'
29
- * }>>}
30
- */
31
- async getEpics(_filters = {}) {
32
- throw new Error('Not implemented: getEpics');
33
- }
34
-
35
20
  /**
36
21
  * Fetch the Epic issue with its body — the single planning document
37
22
  * (ideation sections plus the folded Tech Spec / Acceptance Table
@@ -165,9 +150,6 @@ export class ITicketingProvider {
165
150
  * }} ticketData - The ticket content and metadata.
166
151
  * @returns {Promise<{ id: number, url: string }>}
167
152
  */
168
- async createTicket(_parentId, _ticketData) {
169
- throw new Error('Not implemented: createTicket');
170
- }
171
153
 
172
154
  /**
173
155
  * Link an existing issue as a sub-issue of a parent.
@@ -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