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
@@ -28,259 +28,111 @@
28
28
  * critical-halt path so the Epic-scoped lifecycle ledger still sees
29
29
  * the Story drop out.
30
30
  *
31
- * `runStoryReviewCore` is exported as the shared spine that the
32
- * `single-story-close` path imports, so both close paths call `runCodeReview`
33
- * through a single implementation rather than each maintaining its own
34
- * invocation pattern (Story #3653).
31
+ * The shared spine both close paths call `runCodeReview` through
32
+ * (`runStoryReviewCore`, Story #3653) and the shift-left local-lens pass
33
+ * (Epic #4405) live in `review-core.js` and `local-lens-review.js`
34
+ * respectively (extracted by Story #4603). This module is the Epic-attached
35
+ * phase entry point: it owns the advisory error posture and the
36
+ * critical-halt → blocked-envelope translation, and nothing else.
35
37
  */
36
38
 
37
- import {
38
- runAuditSuite,
39
- selectLocalLenses,
40
- } from '../../../audit-suite/index.js';
41
- import { gitSpawn } from '../../../git-utils.js';
42
39
  import { Logger } from '../../../Logger.js';
43
40
  import { runCodeReview } from '../../code-review.js';
44
41
  import { emitBlockedCloseResult } from '../emit-blocked.js';
42
+ import { runLocalLensReview } from './local-lens-review.js';
43
+ import { runStoryReviewCore } from './review-core.js';
45
44
 
46
45
  /**
47
- * The review depth the Story-scope local-lens pass runs at. Shift-left
48
- * (Epic #4405): local concerns are cheap to decide on a single Story's diff, so
49
- * the maker-blind Story-scope review runs its matched local lenses at `light`
50
- * depth here rather than paying a deeper pass at Epic close. Fixed for this
51
- * tier — it is not risk-scaled like the code-review pillar depth.
52
- */
53
- export const STORY_SCOPE_LENS_DEPTH = 'light';
54
-
55
- /**
56
- * Enumerate the files changed in the `baseRef...headRef` diff via
57
- * `git diff --name-only`. Best-effort: returns `[]` when the diff cannot be
58
- * enumerated (git failure, missing ref) and never throws, mirroring the
59
- * advisory posture of the surrounding review phase. Synchronous `gitSpawn`
60
- * (returns `{ status, stdout }`) is the same seam `code-review.js#countChangedFiles`
61
- * uses.
46
+ * Read a review envelope's severity counts, tolerating the partial envelope a
47
+ * misbehaving provider adapter can return. Pure.
62
48
  *
63
- * @param {{ baseRef: string, headRef: string, gitSpawnFn?: typeof gitSpawn }} args
64
- * @returns {string[]} Changed file paths, or `[]` on any failure.
49
+ * @param {object|null|undefined} reviewResult
50
+ * @returns {{ critical: number, high: number, medium: number, suggestion: number }}
65
51
  */
66
- export function enumerateChangedFiles({
67
- baseRef,
68
- headRef,
69
- gitSpawnFn = gitSpawn,
70
- }) {
71
- try {
72
- const result = gitSpawnFn(
73
- process.cwd(),
74
- 'diff',
75
- '--name-only',
76
- `${baseRef}...${headRef}`,
77
- );
78
- if (!result || result.status !== 0 || typeof result.stdout !== 'string') {
79
- return [];
52
+ function resolveSeverity(reviewResult) {
53
+ return (
54
+ reviewResult?.severity ?? {
55
+ critical: 0,
56
+ high: 0,
57
+ medium: 0,
58
+ suggestion: 0,
80
59
  }
81
- return result.stdout
82
- .split('\n')
83
- .map((f) => f.trim())
84
- .filter(Boolean);
85
- } catch {
86
- return [];
87
- }
60
+ );
88
61
  }
89
62
 
90
63
  /**
91
- * Run the Story-scope local-lens pass: select the LOCAL-tier lenses whose
92
- * `filePatterns` match the actual Story diff (`baseRef...headRef`) and
93
- * materialize their lens-prompt bodies at `light` depth. This is the
94
- * shift-left tier from Epic #4405 — it runs **inside** the story-close
95
- * subprocess spine (called only from {@link runStoryReviewCore}), never in the
96
- * delivering child's (maker's) context, so a maker never grades its own work.
97
- *
98
- * A diff that matches no local lens adds **no** lens work: the roster is empty
99
- * and `runAuditSuite` is never invoked. Best-effort and total — a git or
100
- * materialization failure degrades to `{ skipped: true, lenses: [] }` and is
101
- * logged via `progress`, matching the advisory posture the review phase already
102
- * takes for provider/transport failures.
64
+ * Collect the extra fields for the code-review-critical blocked envelope. Pure.
103
65
  *
104
- * @param {{
105
- * baseRef: string,
106
- * headRef: string,
107
- * progress: (tag: string, msg: string) => void,
108
- * progressTag?: string,
109
- * gitSpawnFn?: typeof gitSpawn,
110
- * selectLocalLensesFn?: typeof selectLocalLenses,
111
- * runAuditSuiteFn?: typeof runAuditSuite,
112
- * }} args
113
- * @returns {Promise<{
114
- * depth: 'light',
115
- * lenses: string[],
116
- * skipped: boolean,
117
- * materialized: object|null,
118
- * }>}
119
- */
120
- export async function runLocalLensReview({
121
- baseRef,
122
- headRef,
123
- progress,
124
- progressTag = 'CODE-REVIEW',
125
- gitSpawnFn = gitSpawn,
126
- selectLocalLensesFn = selectLocalLenses,
127
- runAuditSuiteFn = runAuditSuite,
128
- }) {
129
- const empty = {
130
- depth: STORY_SCOPE_LENS_DEPTH,
131
- lenses: [],
132
- skipped: true,
133
- materialized: null,
134
- };
135
- let lenses;
136
- try {
137
- const changedFiles = enumerateChangedFiles({
138
- baseRef,
139
- headRef,
140
- gitSpawnFn,
141
- });
142
- lenses = selectLocalLensesFn({ changedFiles });
143
- if (lenses.length === 0) {
144
- progress(
145
- progressTag,
146
- 'No local lens matched the Story diff — skipping the lens pass.',
147
- );
148
- return empty;
149
- }
150
- const materialized = await runAuditSuiteFn({ auditWorkflows: lenses });
151
- progress(
152
- progressTag,
153
- `Ran ${lenses.length} local lens(es) at ${STORY_SCOPE_LENS_DEPTH} depth: ${lenses.join(', ')}.`,
154
- );
155
- return {
156
- depth: STORY_SCOPE_LENS_DEPTH,
157
- lenses,
158
- skipped: false,
159
- materialized,
160
- };
161
- } catch (err) {
162
- // The lens pass is advisory: a git or materialization failure must not
163
- // fail the close. Log and degrade to a skipped envelope.
164
- progress(
165
- progressTag,
166
- `⚠️ local lens pass failed (continuing without it): ${err?.message ?? err}`,
167
- );
168
- return empty;
169
- }
170
- }
171
-
172
- /**
173
- * Collect the extra fields for the code-review-critical blocked envelope.
174
- * Pure; used by `runStoryCodeReview` to populate the `extra` argument of
175
- * `emitBlockedCloseResult`.
66
+ * @param {{ storyId: number, reviewResult: object }} args
67
+ * @returns {object}
176
68
  */
177
69
  function buildCodeReviewBlockedExtra({ storyId, reviewResult }) {
178
- const severity = reviewResult?.severity ?? {
179
- critical: 0,
180
- high: 0,
181
- medium: 0,
182
- suggestion: 0,
183
- };
184
70
  return {
185
- storyId: Number(storyId),
71
+ storyId,
186
72
  blockerReason: reviewResult?.blockerReason ?? null,
187
- severity,
73
+ severity: resolveSeverity(reviewResult),
188
74
  posted: reviewResult?.posted ?? false,
189
75
  exitCode: 1,
190
76
  };
191
77
  }
192
78
 
193
79
  /**
194
- * Invoke `runCodeReviewFn` with the canonical Story-scope envelope and return
195
- * the raw result. Shared by both the Epic-attached close path
196
- * (`runStoryCodeReview`) and the standalone close path
197
- * (`single-story-close/phases/code-review.js#runStoryScopeReview`) so the
198
- * invocation pattern lives in one place (Story #3653).
199
- *
200
- * The caller is responsible for error handling and result interpretation —
201
- * this function propagates throws rather than swallowing them, because the
202
- * two callers have different advisory postures:
80
+ * Render the operator-facing one-line summary of a completed (non-halting)
81
+ * review. Pure.
203
82
  *
204
- * - Epic-attached close: swallows throws (non-blocking advisory, same as
205
- * `refresh.js`).
206
- * - Standalone close: propagates throws (a review failure stops the close).
83
+ * @param {object} reviewResult
84
+ * @returns {string}
85
+ */
86
+ function formatReviewSummary(reviewResult) {
87
+ const { high, medium, suggestion } = resolveSeverity(reviewResult);
88
+ const posted = reviewResult?.posted ?? false;
89
+ return `Review complete — high=${high} medium=${medium} suggestion=${suggestion} (posted=${posted}).`;
90
+ }
91
+
92
+ /**
93
+ * Run the shared review spine, absorbing an adapter / wiring failure into this
94
+ * phase's advisory posture: the review is best-effort when the provider cannot
95
+ * complete, and the gates already vouched for the diff at this point.
207
96
  *
208
- * The optional `planningRisk` envelope (Story #3940) is forwarded verbatim to
209
- * `runCodeReview`, which folds its `overallLevel` together with the
210
- * `baseRef...headRef` changed-file count (which `runCodeReview` enumerates
211
- * itself, scoped to the diff under review) into the review depth via the
212
- * shared {@link resolveDepth} resolver. It is an **input-only** signal: it
213
- * tells the provider how thorough to be and never alters the review's output
214
- * envelope or the posted structured-comment body. Absent (`null` / `undefined`,
215
- * the standalone close path) → `runCodeReview` resolves the neutral depth from
216
- * diff width alone, byte-identical to the pre-#3940 behaviour.
97
+ * @param {object} args Spine arguments (see {@link runStoryReviewCore}).
98
+ * @returns {Promise<object|null>} The review envelope, or `null` when the
99
+ * review threw and the close should proceed unblocked.
100
+ */
101
+ async function invokeReviewCore(args) {
102
+ try {
103
+ return await runStoryReviewCore(args);
104
+ } catch (err) {
105
+ Logger.warn?.(
106
+ `[story-close] ⚠️ code-review phase failed (continuing without blocker): ${err?.message ?? err}`,
107
+ );
108
+ return null;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Translate a halting (critical-findings) review into the blocked envelope the
114
+ * caller returns verbatim, emitting `story.blocked` onto the bus on the way.
217
115
  *
218
116
  * @param {{
219
- * storyId: number|string,
220
- * baseRef: string,
221
- * headRef: string,
222
- * commentTargetId?: number|null,
223
- * provider: object,
117
+ * storyId: number,
118
+ * reviewResult: object,
119
+ * bus: { emit: Function }|null,
224
120
  * progress: (tag: string, msg: string) => void,
225
- * progressTag?: string,
226
- * planningRisk?: { overallLevel?: ('low'|'medium'|'high'), axes?: Array<{ axis?: string, level?: string }> }|null,
227
- * runCodeReviewFn?: typeof runCodeReview,
228
- * runLocalLensReviewFn?: typeof runLocalLensReview,
229
121
  * }} args
230
- * @returns {Promise<object>} Raw result envelope from `runCodeReview`, augmented
231
- * with a `localLensReview` field carrying the Story-scope local-lens pass
232
- * outcome (Epic #4405, Story #4409). Both close entry points reach the lens
233
- * pass through this single spine, so it runs on the Epic-attached and
234
- * standalone paths alike and always inside the close subprocess.
122
+ * @returns {Promise<object>} The blocked envelope.
235
123
  */
236
- export async function runStoryReviewCore({
237
- storyId,
238
- baseRef,
239
- headRef,
240
- commentTargetId = null,
241
- provider,
242
- progress,
243
- progressTag = 'CODE-REVIEW',
244
- planningRisk = null,
245
- runCodeReviewFn = runCodeReview,
246
- runLocalLensReviewFn = runLocalLensReview,
247
- }) {
248
- const storyIdNum = Number(storyId);
249
- const opts = {
250
- scope: 'story',
251
- ticketId: storyIdNum,
252
- baseRef,
253
- headRef,
254
- provider,
255
- logger: {
256
- info: (m) => progress(progressTag, m),
257
- warn: (m) => progress(progressTag, `⚠️ ${m}`),
258
- },
259
- };
260
- if (commentTargetId != null) {
261
- opts.commentTargetId = commentTargetId;
262
- }
263
- // Forward the parent Epic's judged risk only on the Epic-attached path; the
264
- // standalone caller leaves this null so `runCodeReview` resolves depth from
265
- // diff width alone (no plan checkpoint exists for a standalone Story).
266
- if (planningRisk != null) {
267
- opts.planningRisk = planningRisk;
268
- }
269
-
270
- // Shift-left local-lens pass (Epic #4405). Runs matched local lenses at
271
- // `light` depth against the actual Story diff, inside this close-subprocess
272
- // spine so the maker never grades its own work. Advisory — it never blocks
273
- // the close and its outcome rides on the returned envelope for downstream
274
- // consumers.
275
- const localLensReview = await runLocalLensReviewFn({
276
- baseRef,
277
- headRef,
124
+ async function emitCriticalBlock({ storyId, reviewResult, bus, progress }) {
125
+ const { critical } = resolveSeverity(reviewResult);
126
+ return emitBlockedCloseResult({
127
+ storyId,
128
+ phase: 'closing',
129
+ reason: 'code-review-critical',
130
+ extra: buildCodeReviewBlockedExtra({ storyId, reviewResult }),
131
+ bus,
278
132
  progress,
279
- progressTag,
133
+ blockedMessage: `Story #${storyId} blocked: code-review reported ${critical} critical blocker(s).`,
134
+ logger: Logger,
280
135
  });
281
-
282
- const result = await runCodeReviewFn(opts);
283
- return { ...result, localLensReview };
284
136
  }
285
137
 
286
138
  /**
@@ -291,23 +143,19 @@ export async function runStoryReviewCore({
291
143
  * `blocked` is either `null` (caller proceeds to open/merge the PR) or the
292
144
  * blocked-envelope (caller returns it verbatim and the CLI exits 1).
293
145
  *
294
- * The optional `planningRisk` envelope (Story #3940) is the parent Epic's
295
- * judged risk, read best-effort off the `epic-plan-state` checkpoint by the
296
- * locked pipeline. It is forwarded into `runCodeReview` so the review depth is
297
- * resolved from BOTH the parent Epic's judged risk (pre-v2) and the Story's own
298
- * base…head changed-file count a small Story under a
299
- * high-risk Epic still earns `deep`, a small Story under a low-risk Epic gets
300
- * `light`, and an absent envelope resolves `standard` (today's behaviour).
301
- * Depth is input-only: it never changes `{ blocked }` or the posted comment.
146
+ * Review depth is derived inside `runCodeReview` from this Story's own base…head
147
+ * diff a narrow change touching a registered sensitive path earns `deep`, a
148
+ * small change touching none gets `light`, a wide diff earns `deep` on size, and
149
+ * an unenumerable diff resolves `standard` (Story #4542). Depth is input-only:
150
+ * it never changes `{ blocked }` or the posted comment.
302
151
  *
303
152
  * @param {{
304
153
  * storyId: number|string,
305
- * epicBranch: string,
154
+ * baseBranch: string,
306
155
  * storyBranch: string,
307
156
  * provider: object,
308
157
  * bus: { emit: Function }|null,
309
158
  * progress: (tag: string, msg: string) => void,
310
- * planningRisk?: { overallLevel?: ('low'|'medium'|'high'), axes?: Array<{ axis?: string, level?: string }> }|null,
311
159
  * runCodeReviewFn?: typeof runCodeReview,
312
160
  * runLocalLensReviewFn?: typeof runLocalLensReview,
313
161
  * }} args
@@ -316,67 +164,44 @@ export async function runStoryReviewCore({
316
164
  * (Epic #4405, Story #4409) when the review completed; it is absent only when
317
165
  * the whole review phase threw (advisory failure).
318
166
  */
319
- export async function runStoryCodeReview(args) {
320
- const {
321
- storyId,
322
- epicBranch,
323
- storyBranch,
324
- provider,
325
- bus,
326
- progress,
327
- planningRisk = null,
328
- runCodeReviewFn = runCodeReview,
329
- runLocalLensReviewFn = runLocalLensReview,
330
- } = args;
331
-
167
+ export async function runStoryCodeReview({
168
+ storyId,
169
+ baseBranch,
170
+ storyBranch,
171
+ provider,
172
+ bus,
173
+ progress,
174
+ runCodeReviewFn = runCodeReview,
175
+ runLocalLensReviewFn = runLocalLensReview,
176
+ }) {
332
177
  const storyIdNum = Number(storyId);
333
178
  progress(
334
179
  'CODE-REVIEW',
335
- `Running Story-scope review (${epicBranch}…${storyBranch})...`,
180
+ `Running Story-scope review (${baseBranch}…${storyBranch})...`,
336
181
  );
337
182
 
338
- let reviewResult;
339
- try {
340
- reviewResult = await runStoryReviewCore({
341
- storyId: storyIdNum,
342
- baseRef: epicBranch,
343
- headRef: storyBranch,
344
- provider,
345
- progress,
346
- planningRisk,
347
- runCodeReviewFn,
348
- runLocalLensReviewFn,
349
- });
350
- } catch (err) {
351
- // Adapter / wiring failure — log and proceed. The review is advisory
352
- // when the provider cannot complete; the gates already vouched for
353
- // the diff at this point.
354
- Logger.warn?.(
355
- `[story-close] ⚠️ code-review phase failed (continuing without blocker): ${err?.message ?? err}`,
356
- );
357
- return { blocked: null };
358
- }
359
-
360
- const localLensReview = reviewResult?.localLensReview;
183
+ const reviewResult = await invokeReviewCore({
184
+ storyId: storyIdNum,
185
+ baseRef: baseBranch,
186
+ headRef: storyBranch,
187
+ provider,
188
+ progress,
189
+ runCodeReviewFn,
190
+ runLocalLensReviewFn,
191
+ });
192
+ if (reviewResult === null) return { blocked: null };
361
193
 
362
- if (reviewResult?.halted) {
363
- const blocked = await emitBlockedCloseResult({
194
+ const localLensReview = reviewResult.localLensReview;
195
+ if (reviewResult.halted) {
196
+ const blocked = await emitCriticalBlock({
364
197
  storyId: storyIdNum,
365
- phase: 'closing',
366
- reason: 'code-review-critical',
367
- extra: buildCodeReviewBlockedExtra({ storyId: storyIdNum, reviewResult }),
198
+ reviewResult,
368
199
  bus,
369
200
  progress,
370
- blockedMessage: `Story #${storyIdNum} blocked: code-review reported ${reviewResult.severity.critical} critical blocker(s).`,
371
- logger: Logger,
372
201
  });
373
202
  return { blocked, localLensReview };
374
203
  }
375
204
 
376
- const counts = reviewResult?.severity ?? {};
377
- progress(
378
- 'CODE-REVIEW',
379
- `Review complete — high=${counts.high ?? 0} medium=${counts.medium ?? 0} suggestion=${counts.suggestion ?? 0} (posted=${reviewResult?.posted ?? false}).`,
380
- );
205
+ progress('CODE-REVIEW', formatReviewSummary(reviewResult));
381
206
  return { blocked: null, localLensReview };
382
207
  }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * phases/local-lens-review.js — the Story-scope local-lens pass.
3
+ *
4
+ * Extracted from `phases/code-review.js` (Story #4603) so the review spine and
5
+ * the lens pass each carry one reason to change. The spine
6
+ * (`runStoryReviewCore`) owns the single per-close-run diff enumeration and the
7
+ * `runCodeReview` invocation; this module owns lens selection + materialization.
8
+ *
9
+ * Shift-left tier (Epic #4405): local concerns are cheap to decide on a single
10
+ * Story's diff, so the maker-blind Story-scope review runs its matched local
11
+ * lenses here, inside the story-close subprocess, rather than paying a deeper
12
+ * pass at Epic close.
13
+ */
14
+
15
+ import {
16
+ runAuditSuite,
17
+ selectLocalLenses,
18
+ } from '../../../audit-suite/index.js';
19
+ import { gitSpawn } from '../../../git-utils.js';
20
+ import { computeChangeSet } from '../../change-set.js';
21
+
22
+ /**
23
+ * The review depth the Story-scope local-lens pass runs at. Fixed for this
24
+ * tier — it is not risk-scaled like the code-review pillar depth.
25
+ *
26
+ * Module-local: it is an implementation detail of {@link runLocalLensReview}
27
+ * (it rides out on the returned envelope's `depth` field), not a public seam.
28
+ * Tests assert the observable `'light'` on that envelope rather than importing
29
+ * the constant, so it stays off the public surface (Story #4603).
30
+ */
31
+ const STORY_SCOPE_LENS_DEPTH = 'light';
32
+
33
+ /**
34
+ * Enumerate the files changed in the `baseRef...headRef` diff. Thin adapter over
35
+ * the shared {@link computeChangeSet} enumerator (Story #4593) that flattens its
36
+ * `files: string[]|null` envelope to this phase's historical `[]`-on-failure
37
+ * contract: the lens roster treats "nothing changed" and "diff unknown"
38
+ * identically, because an unknown diff matches no `filePatterns` and therefore
39
+ * adds no lens work either way.
40
+ *
41
+ * Best-effort and total — never throws, mirroring the advisory posture of the
42
+ * surrounding review phase. Retained as the self-enumeration fallback for
43
+ * {@link runLocalLensReview} when no change set is injected.
44
+ *
45
+ * @param {{
46
+ * baseRef: string,
47
+ * headRef: string,
48
+ * gitSpawnFn?: import('../../change-set.js').GitSpawnFn,
49
+ * }} args
50
+ * Module-local (Story #4603): a private fallback of {@link runLocalLensReview},
51
+ * exercised through that public entry point rather than imported directly, so it
52
+ * adds no public export a production path fails to reach.
53
+ *
54
+ * @returns {string[]} Changed file paths, or `[]` on any failure.
55
+ */
56
+ function enumerateChangedFiles({ baseRef, headRef, gitSpawnFn = gitSpawn }) {
57
+ return computeChangeSet({ baseRef, headRef, gitSpawnFn }).files ?? [];
58
+ }
59
+
60
+ /**
61
+ * Resolve the change set the lens roster reads, honouring all THREE injection
62
+ * states (Story #4603 — the fix for #4593's single-enumeration leak).
63
+ *
64
+ * The distinction between `null` and `undefined` is load-bearing and mirrors
65
+ * the sibling contract in `orchestration/code-review.js#resolveInjectedChangedFiles`:
66
+ *
67
+ * - **array** — the caller's change set; use it verbatim.
68
+ * - **`null`** — the caller (`runStoryReviewCore`) already tried and the
69
+ * diff is unenumerable. Re-running git here would only fail
70
+ * again, so degrade straight to the fail-safe empty roster.
71
+ * - **`undefined`** — nobody enumerated (standalone callers), so the shared
72
+ * enumerator runs as the fallback.
73
+ *
74
+ * The prior `Array.isArray()` discriminator collapsed `null` and `undefined`
75
+ * into one branch and re-spawned git on the unenumerable path, contradicting the
76
+ * spine's documented "the one enumeration per close run" invariant.
77
+ *
78
+ * Module-local (Story #4603): a private detail of {@link runLocalLensReview}.
79
+ * The three-state contract is asserted through that public entry point (does an
80
+ * injected `null` re-spawn git? does `undefined` self-enumerate?), so it needs
81
+ * no public export — keeping the fix from re-introducing the very kind of
82
+ * production-dead public symbol this Story's ratchet root-cause is about.
83
+ *
84
+ * @param {{
85
+ * changedFiles: string[]|null|undefined,
86
+ * baseRef: string,
87
+ * headRef: string,
88
+ * gitSpawnFn?: import('../../change-set.js').GitSpawnFn,
89
+ * }} args
90
+ * @returns {string[]}
91
+ */
92
+ function resolveLensChangeSet({
93
+ changedFiles,
94
+ baseRef,
95
+ headRef,
96
+ gitSpawnFn = gitSpawn,
97
+ }) {
98
+ if (changedFiles === undefined) {
99
+ return enumerateChangedFiles({ baseRef, headRef, gitSpawnFn });
100
+ }
101
+ return changedFiles ?? [];
102
+ }
103
+
104
+ /**
105
+ * Run the Story-scope local-lens pass: select the LOCAL-tier lenses whose
106
+ * `filePatterns` match the actual Story diff (`baseRef...headRef`) and
107
+ * materialize their lens-prompt bodies at `light` depth. Called only from
108
+ * `runStoryReviewCore`, never in the delivering child's (maker's) context, so a
109
+ * maker never grades its own work.
110
+ *
111
+ * A diff that matches no local lens adds **no** lens work: the roster is empty
112
+ * and `runAuditSuite` is never invoked. Best-effort and total — a git or
113
+ * materialization failure degrades to `{ skipped: true, lenses: [] }` and is
114
+ * logged via `progress`, matching the advisory posture the review phase already
115
+ * takes for provider/transport failures.
116
+ *
117
+ * Story #4593 — `changedFiles` is injected by `runStoryReviewCore`, which
118
+ * computes the change set once per close run and hands the same list to this
119
+ * pass and to `runCodeReview`. Self-enumeration is the **fallback only**, kept
120
+ * for standalone callers that supply no list; see {@link resolveLensChangeSet}
121
+ * for the three-state contract.
122
+ *
123
+ * @param {{
124
+ * baseRef: string,
125
+ * headRef: string,
126
+ * changedFiles?: string[]|null,
127
+ * progress: (tag: string, msg: string) => void,
128
+ * progressTag?: string,
129
+ * gitSpawnFn?: import('../../change-set.js').GitSpawnFn,
130
+ * selectLocalLensesFn?: typeof selectLocalLenses,
131
+ * runAuditSuiteFn?: typeof runAuditSuite,
132
+ * }} args
133
+ * @returns {Promise<{
134
+ * depth: 'light',
135
+ * lenses: string[],
136
+ * skipped: boolean,
137
+ * materialized: object|null,
138
+ * }>}
139
+ */
140
+ export async function runLocalLensReview({
141
+ baseRef,
142
+ headRef,
143
+ changedFiles: injectedChangedFiles,
144
+ progress,
145
+ progressTag = 'CODE-REVIEW',
146
+ gitSpawnFn = gitSpawn,
147
+ selectLocalLensesFn = selectLocalLenses,
148
+ runAuditSuiteFn = runAuditSuite,
149
+ }) {
150
+ const empty = {
151
+ depth: STORY_SCOPE_LENS_DEPTH,
152
+ lenses: [],
153
+ skipped: true,
154
+ materialized: null,
155
+ };
156
+ try {
157
+ const changedFiles = resolveLensChangeSet({
158
+ changedFiles: injectedChangedFiles,
159
+ baseRef,
160
+ headRef,
161
+ gitSpawnFn,
162
+ });
163
+ const lenses = selectLocalLensesFn({ changedFiles });
164
+ if (lenses.length === 0) {
165
+ progress(
166
+ progressTag,
167
+ 'No local lens matched the Story diff — skipping the lens pass.',
168
+ );
169
+ return empty;
170
+ }
171
+ const materialized = await runAuditSuiteFn({ auditWorkflows: lenses });
172
+ progress(
173
+ progressTag,
174
+ `Ran ${lenses.length} local lens(es) at ${STORY_SCOPE_LENS_DEPTH} depth: ${lenses.join(', ')}.`,
175
+ );
176
+ return {
177
+ depth: STORY_SCOPE_LENS_DEPTH,
178
+ lenses,
179
+ skipped: false,
180
+ materialized,
181
+ };
182
+ } catch (err) {
183
+ // The lens pass is advisory: a git or materialization failure must not
184
+ // fail the close. Log and degrade to a skipped envelope.
185
+ progress(
186
+ progressTag,
187
+ `⚠️ local lens pass failed (continuing without it): ${err?.message ?? err}`,
188
+ );
189
+ return empty;
190
+ }
191
+ }