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
@@ -0,0 +1,344 @@
1
+ /**
2
+ * lib/orchestration/resolve-stories.js — resolve a set of Story ids into the
3
+ * `{ stories, dag, done }` envelope the delivery scheduler consumes.
4
+ *
5
+ * This is the ONE resolution step for `/deliver`. It generalizes the
6
+ * envelope shaping proven by the retired `resolve-plan-run.js` and fixes the
7
+ * two defects that shipped with it:
8
+ *
9
+ * - it fetched with `state: 'open'`, so an already-landed sibling vanished
10
+ * from the envelope and `done[]` could never be populated;
11
+ * - `done[]` was computed only over label-fetched issues, so a dependency
12
+ * outside the fetched set could never be satisfied.
13
+ *
14
+ * Both made cross-run, over-time delivery structurally impossible. Here every
15
+ * dependency — in-set or foreign — is resolved against live issue state, so a
16
+ * Story whose blocker landed weeks ago in another plan run is simply ready.
17
+ *
18
+ * Two contracts differ deliberately from the label-scoped ancestor:
19
+ *
20
+ * 1. **Id-scoped fetch means a named non-Story is an ERROR, not a filter.**
21
+ * `toStoryRecord` used to return `null` for a non-Story, which is right
22
+ * when a label query returns incidental noise and wrong when an operator
23
+ * names an id explicitly: silently dropping it yields a partial envelope
24
+ * that under-delivers without saying so.
25
+ * 2. **The native-edge read fails loud.** See {@link readNativeBlockedBy}.
26
+ *
27
+ * @module lib/orchestration/resolve-stories
28
+ */
29
+
30
+ import { extractEpicIdFromBody, parseBlockedBy } from '../dependency-parser.js';
31
+ import { TYPE_LABELS } from '../label-constants.js';
32
+ import { buildStoryAdjacency } from '../story-adjacency.js';
33
+ import {
34
+ extractChangePaths,
35
+ parse as parseStoryBody,
36
+ } from '../story-body/story-body.js';
37
+
38
+ /** Labels/state that mean a blocker no longer gates its dependents. */
39
+ const DONE_LABEL = 'agent::done';
40
+
41
+ /**
42
+ * Module-private: `toStoryRecord` and `isSatisfiedBlocker` are its only
43
+ * callers. The ancestor exported it with no external consumer, which is how
44
+ * a symbol ends up baselined as a dead export.
45
+ *
46
+ * @param {object} issue
47
+ * @returns {string[]}
48
+ */
49
+ function normalizeIssueLabels(issue) {
50
+ const raw = issue?.labels;
51
+ if (!Array.isArray(raw)) return [];
52
+ return raw
53
+ .map((l) => (typeof l === 'string' ? l : l?.name))
54
+ .filter((n) => typeof n === 'string' && n.length > 0);
55
+ }
56
+
57
+ /**
58
+ * Map one fetched issue into a Story record, or throw naming the id and the
59
+ * remedy. Unlike the label-scoped ancestor this **never** returns `null`:
60
+ * under `--ids` the operator named this issue, so dropping it silently would
61
+ * emit a partial envelope.
62
+ *
63
+ * @param {object} issue
64
+ * @param {number} [requestedId] The id the operator asked for, for error text.
65
+ * @returns {{ id, title, body, url, labels, state }}
66
+ */
67
+ export function toStoryRecord(issue, requestedId) {
68
+ const id = Number(issue?.number ?? issue?.id ?? requestedId);
69
+ if (!Number.isInteger(id) || id <= 0) {
70
+ throw new Error(
71
+ `[resolve-stories] #${requestedId ?? '?'} did not resolve to an issue number.`,
72
+ );
73
+ }
74
+ const labels = normalizeIssueLabels(issue);
75
+ if (!labels.includes(TYPE_LABELS.STORY)) {
76
+ throw new Error(
77
+ `[resolve-stories] Issue #${id} is not a Story (labels: ${labels.join(', ') || 'none'}). ` +
78
+ `/deliver accepts ${TYPE_LABELS.STORY} tickets only — close it or re-plan it as a v2 Story.`,
79
+ );
80
+ }
81
+ const body = String(issue?.body ?? '');
82
+ const epicId = extractEpicIdFromBody(body);
83
+ if (epicId !== null) {
84
+ throw new Error(
85
+ `[resolve-stories] Issue #${id} still carries an "Epic: #${epicId}" footer. ` +
86
+ `v2 is Story-only — re-plan it as a v2 Story or finish it on a pre-v2 checkout.`,
87
+ );
88
+ }
89
+ return {
90
+ id,
91
+ title: String(issue?.title ?? ''),
92
+ body,
93
+ url: issue?.html_url ?? issue?.url ?? null,
94
+ labels,
95
+ state: String(issue?.state ?? 'open').toLowerCase(),
96
+ };
97
+ }
98
+
99
+ /**
100
+ * A blocker stops gating once its issue is closed or carries `agent::done`.
101
+ *
102
+ * @param {{ state?: string, labels?: string[] }} issue
103
+ * @returns {boolean}
104
+ */
105
+ export function isSatisfiedBlocker(issue) {
106
+ const state = String(issue?.state ?? '').toLowerCase();
107
+ if (state === 'closed') return true;
108
+ return normalizeIssueLabels(issue).includes(DONE_LABEL);
109
+ }
110
+
111
+ /**
112
+ * Footprint emitted when a Story's declared changes cannot be READ. It is a
113
+ * glob, so `storiesOverlap` (`lib/wave-runner/ready-set.js`) treats it as
114
+ * overlapping every other **declared** footprint and the Story takes its
115
+ * beat alone. (A Story declaring nothing still overlaps nothing — the guard
116
+ * short-circuits on an empty footprint either side, which is the deliberate
117
+ * permissive escape hatch that keeps undeclared work parallel.)
118
+ *
119
+ * Deliberately NOT `[]`. An empty footprint means "declares nothing", which
120
+ * the guard reads as "overlaps nothing" and never withholds — correct for a
121
+ * Story that genuinely declares no changes, and wrong for one whose changes
122
+ * we failed to parse. Those are different facts: the second is *unknown*
123
+ * width, and the same argument that makes a glob overlap everything
124
+ * ("unknown width is not no width") applies to a body we could not read.
125
+ */
126
+ const UNKNOWN_FOOTPRINT = Object.freeze(['**']);
127
+
128
+ /**
129
+ * Extract a Story's declared file footprint as **plain path strings**.
130
+ *
131
+ * The shape matters: `stories-wave-tick.js`'s `parseDag` rejects any `files`
132
+ * entry that is not a string, while `extractChangePaths` returns
133
+ * `{ path, isGlob }` objects — so forwarding its output verbatim fails every
134
+ * multi-Story run with an input error. Map to `.path`.
135
+ *
136
+ * Never throws — these are live, human-editable issue bodies, and one
137
+ * malformed body must not take the whole resolution down. It fails **safe**
138
+ * rather than open: an unreadable footprint yields {@link UNKNOWN_FOOTPRINT},
139
+ * serializing that Story instead of silently letting it race.
140
+ *
141
+ * @param {string} body
142
+ * @param {number} [id] Story id, for the warning.
143
+ * @param {(msg: string) => void} [warn]
144
+ * @returns {string[]}
145
+ */
146
+ export function storyFootprintPaths(body, id, warn) {
147
+ let parsed;
148
+ try {
149
+ parsed = parseStoryBody(String(body ?? '')).body;
150
+ } catch (err) {
151
+ warn?.(
152
+ `[resolve-stories] #${id}: body is unparseable, so its file footprint is unknown ` +
153
+ `(${err?.message ?? err}). Treating it as overlapping every other Story, so it is ` +
154
+ `never co-dispatched — fix the body to restore parallelism.`,
155
+ );
156
+ return [...UNKNOWN_FOOTPRINT];
157
+ }
158
+ try {
159
+ // An empty `changes` is a real declaration of "no files", not a read
160
+ // failure — it keeps the permissive empty footprint.
161
+ return extractChangePaths(parsed?.changes ?? [])
162
+ .map((entry) => entry?.path)
163
+ .filter((p) => typeof p === 'string' && p.trim().length > 0)
164
+ .map((p) => p.trim());
165
+ } catch (err) {
166
+ warn?.(
167
+ `[resolve-stories] #${id}: malformed changes entry, so its file footprint is unknown ` +
168
+ `(${err?.message ?? err}). Treating it as overlapping every other Story.`,
169
+ );
170
+ return [...UNKNOWN_FOOTPRINT];
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Build the DAG nodes. `dependsOn` is the union the adjacency builder already
176
+ * computes (body-parsed `blocked by #N` + explicit fields), plus any native
177
+ * edges threaded in via `nativeEdges`. `files` is a plain `string[]`.
178
+ *
179
+ * @param {object[]} stories
180
+ * @param {Map<number, number[]>} [nativeEdges]
181
+ * @param {(msg: string) => void} [warn]
182
+ * @returns {{ id: number, dependsOn: number[], files: string[] }[]}
183
+ */
184
+ export function storiesToDag(stories, nativeEdges = new Map(), warn) {
185
+ const withNative = stories.map((s) => ({
186
+ ...s,
187
+ dependsOn: [
188
+ ...new Set([
189
+ ...parseBlockedBy(s.body ?? ''),
190
+ ...(nativeEdges.get(s.id) ?? []),
191
+ ]),
192
+ ],
193
+ }));
194
+ // dropForeign:false — a dependency outside the requested set is a real
195
+ // gate, not noise. What changes here is that such a gate is now
196
+ // *satisfiable*: `done[]` carries foreign blockers resolved from live state.
197
+ const adjacency = buildStoryAdjacency(withNative, { dropForeign: false });
198
+ return stories.map((s) => ({
199
+ id: s.id,
200
+ dependsOn: adjacency.get(s.id) ?? [],
201
+ files: storyFootprintPaths(s.body, s.id, warn),
202
+ }));
203
+ }
204
+
205
+ /**
206
+ * Project the dependencies API response onto issue **numbers**.
207
+ *
208
+ * The API returns both `id` (database id) and `number` (issue number); the
209
+ * write path (`providers/github/blocked-by-add.js`) reads `id` because its
210
+ * POST body needs `issue_id`. Reusing that projection here would build
211
+ * `dependsOn: [4902374986]` for a blocker whose issue number is 4530 — an id
212
+ * matching no Story, foreign to the set, never satisfiable, and (because
213
+ * foreign edges are real gates) a silent permanent wedge.
214
+ *
215
+ * Cross-repo blockers are rejected rather than matched: another repo's #4530
216
+ * is not this repo's #4530, and treating it as one could satisfy a gate that
217
+ * is still open.
218
+ *
219
+ * @param {unknown} data Parsed API response.
220
+ * @param {{ owner: string, repo: string, issueNumber: number }} ctx
221
+ * @returns {number[]}
222
+ */
223
+ export function nativeBlockedByNumbers(data, { owner, repo, issueNumber }) {
224
+ if (!Array.isArray(data)) return [];
225
+ const out = [];
226
+ for (const item of data) {
227
+ const repoUrl = item?.repository_url ?? item?.repository?.url ?? null;
228
+ if (typeof repoUrl === 'string' && repoUrl.length > 0) {
229
+ const expected = `/repos/${owner}/${repo}`;
230
+ if (!repoUrl.endsWith(expected)) {
231
+ throw new Error(
232
+ `[resolve-stories] #${issueNumber} is blocked by an issue in another repository ` +
233
+ `(${repoUrl}). Cross-repo dependency edges are not supported — its number cannot ` +
234
+ `be matched against this repo's Stories without risking a false match.`,
235
+ );
236
+ }
237
+ }
238
+ const number = Number(item?.number);
239
+ if (Number.isInteger(number) && number > 0) out.push(number);
240
+ }
241
+ return [...new Set(out)];
242
+ }
243
+
244
+ /**
245
+ * Read an issue's native `blocked_by` edges as issue numbers.
246
+ *
247
+ * **Fails loud**, deliberately inverting the write path's non-fatal contract.
248
+ * A dropped write-side edge is cosmetic (the ordering still lives in the
249
+ * `blocked by #N` body footer); a dropped READ-side edge silently removes a
250
+ * dispatch gate, so a 403 (dependencies API disabled, or a token without the
251
+ * scope) would erase every native edge at once and co-dispatch the whole run
252
+ * against unlanded blockers. A 404 means "no dependencies on this issue" and
253
+ * is a legitimate empty result.
254
+ *
255
+ * @param {{ gh: object, owner: string, repo: string, issueNumber: number, parseJson: Function }} opts
256
+ * @returns {Promise<number[]>}
257
+ */
258
+ export async function readNativeBlockedBy({
259
+ gh,
260
+ owner,
261
+ repo,
262
+ issueNumber,
263
+ parseJson,
264
+ }) {
265
+ let result;
266
+ try {
267
+ result = await gh.api({
268
+ method: 'GET',
269
+ endpoint: `/repos/${owner}/${repo}/issues/${issueNumber}/dependencies/blocked_by`,
270
+ });
271
+ } catch (err) {
272
+ const detail = String(err?.message ?? err);
273
+ if (/404|not found/i.test(detail)) return [];
274
+ throw new Error(
275
+ `[resolve-stories] Could not read native blocked_by edges for #${issueNumber}: ${detail}. ` +
276
+ `Refusing to continue: a dropped dependency edge would silently remove a dispatch gate ` +
277
+ `and co-dispatch this Story against an unlanded blocker.`,
278
+ );
279
+ }
280
+ return nativeBlockedByNumbers(parseJson(result), {
281
+ owner,
282
+ repo,
283
+ issueNumber,
284
+ });
285
+ }
286
+
287
+ /**
288
+ * Assemble the envelope from resolved records.
289
+ *
290
+ * @param {object[]} stories
291
+ * @param {Map<number, number[]>} nativeEdges
292
+ * @param {number[]} foreignDone Ids outside the set already satisfied.
293
+ * @param {(msg: string) => void} [warn]
294
+ * @returns {{ kind: string, stories: object[], dag: object[], done: number[] }}
295
+ */
296
+ export function buildStoriesEnvelope({
297
+ stories,
298
+ nativeEdges = new Map(),
299
+ foreignDone = [],
300
+ warn,
301
+ }) {
302
+ const sorted = [...stories].sort((a, b) => a.id - b.id);
303
+ const inSetDone = sorted.filter(isSatisfiedBlocker).map((s) => s.id);
304
+ return {
305
+ kind: 'stories',
306
+ stories: sorted.map(({ id, title, url, labels, state }) => ({
307
+ id,
308
+ title,
309
+ url,
310
+ labels,
311
+ state,
312
+ })),
313
+ dag: storiesToDag(sorted, nativeEdges, warn),
314
+ done: [...new Set([...inSetDone, ...foreignDone])].sort((a, b) => a - b),
315
+ };
316
+ }
317
+
318
+ /**
319
+ * Parse and validate the `--ids` list.
320
+ *
321
+ * @param {string|undefined} raw
322
+ * @returns {number[]}
323
+ */
324
+ export function parseIds(raw) {
325
+ const ids = String(raw ?? '')
326
+ .split(',')
327
+ .map((s) => s.trim())
328
+ .filter(Boolean)
329
+ .map((s) => {
330
+ const n = Number.parseInt(s, 10);
331
+ if (!Number.isInteger(n) || n <= 0 || String(n) !== s) {
332
+ throw new Error(
333
+ `[resolve-stories] --ids must be a comma-separated list of positive issue numbers (got "${s}").`,
334
+ );
335
+ }
336
+ return n;
337
+ });
338
+ if (ids.length === 0) {
339
+ throw new Error(
340
+ '[resolve-stories] --ids is required: node resolve-stories.js --ids 101,102',
341
+ );
342
+ }
343
+ return [...new Set(ids)];
344
+ }
@@ -43,7 +43,7 @@
43
43
  *
44
44
  * @typedef {Object} RoutedProposalsInput
45
45
  * @property {number} anchorId Story or run/Epic id in titles.
46
- * @property {'epic'|'story'|'run'} [anchorKind] Wording in titles/bodies (default `epic`).
46
+ * @property {'story'|'run'} [anchorKind] Wording in titles/bodies (default `story`).
47
47
  * @property {string} frameworkRepo `"<owner>/<repo>"`.
48
48
  * @property {string} consumerRepo `"<owner>/<repo>"`.
49
49
  * @property {FrictionSignal[]} [signals]
@@ -144,7 +144,7 @@ function dominantSource(entry) {
144
144
  }
145
145
 
146
146
  /**
147
- * @param {'epic'|'story'|'run'} kind
147
+ * @param {'story'|'run'} kind
148
148
  * @param {number} id
149
149
  * @returns {string}
150
150
  */
@@ -154,7 +154,7 @@ function formatAnchor(kind, id) {
154
154
  return `Epic #${id}`;
155
155
  }
156
156
 
157
- /** Story scope promotes single-occurrence friction; Epic/run keep ≥2. */
157
+ /** Story scope promotes single-occurrence friction; run keeps ≥2. */
158
158
  function isActionableFriction(total, force, anchorKind) {
159
159
  const threshold = anchorKind === 'story' ? 1 : 2;
160
160
  return total >= threshold || Boolean(force);
@@ -167,7 +167,7 @@ function isActionableFriction(total, force, anchorKind) {
167
167
  *
168
168
  * @param {{
169
169
  * anchorId: number,
170
- * anchorKind: 'epic'|'story'|'run',
170
+ * anchorKind: 'story'|'run',
171
171
  * category: string,
172
172
  * occurrences: number,
173
173
  * source: "framework"|"consumer",
@@ -225,7 +225,7 @@ function renderIssueCommand({ repo, title, metaLabel, category, body }) {
225
225
  *
226
226
  * @param {{
227
227
  * anchorId: number,
228
- * anchorKind: 'epic'|'story'|'run',
228
+ * anchorKind: 'story'|'run',
229
229
  * category: string,
230
230
  * occurrences: number,
231
231
  * source: "framework"|"consumer",
@@ -272,7 +272,7 @@ function buildRoutedItem({
272
272
  * @param {unknown} input
273
273
  * @returns {{
274
274
  * anchorId: number,
275
- * anchorKind: 'epic'|'story'|'run',
275
+ * anchorKind: 'story'|'run',
276
276
  * frameworkRepo: string,
277
277
  * consumerRepo: string,
278
278
  * signals: FrictionSignal[],
@@ -280,7 +280,7 @@ function buildRoutedItem({
280
280
  * } | null}
281
281
  */
282
282
  function normalizeAnchorKind(kind) {
283
- return kind === 'story' || kind === 'run' || kind === 'epic' ? kind : 'epic';
283
+ return kind === 'story' || kind === 'run' ? kind : 'story';
284
284
  }
285
285
 
286
286
  function normaliseInput(input) {
@@ -1,35 +1,52 @@
1
1
  /**
2
- * lib/orchestration/review-depth.js — Shared review-depth resolver combining
3
- * judged risk and mechanical diff width (Story #3938).
2
+ * lib/orchestration/review-depth.js — the review-depth authority: derives a
3
+ * change level from observable facts and folds it with diff width into a tier
4
+ * (Story #3938, re-based on a derived signal by Story #4542).
4
5
  *
5
- * The single depth authority for the code-review pipeline. It folds the two
6
- * signals the framework already computes — the planner-judged risk
7
- * (`planningRisk.overallLevel`) and the mechanical changed-file count of the
8
- * diff under review — into one tier (`light` / `standard` / `deep`) without
9
- * inventing a new complexity score or any new config knob.
6
+ * ## Why the level is derived, not authored
10
7
  *
11
- * Tier rules:
12
- * - `deep` — `overallLevel === 'high'` OR the changed-file count exceeds
13
- * the wide-change scale (`diffWidth.hardFiles`). A wide-
14
- * footprint Epic whose risk was judged low (or never judged)
15
- * still earns a deep pass on size alone.
16
- * - `light` — `overallLevel === 'low'` AND the changed-file count is at or
17
- * below the small-change scale (`diffWidth.softFiles`). An
18
- * *unknown* count does NOT block `light`: a missing diff width
19
- * is treated as "not wide", so a low-risk Epic with no count
20
- * still resolves to `light` (preserves the #3937 producer
21
- * contract where depth is resolved from risk before any diff
22
- * is enumerated).
23
- * - `standard` — everything else, including absent/malformed risk envelopes
24
- * and a `medium` level. Fail toward the middle, never toward
25
- * `light`: an Epic that skipped `/plan` has no risk
26
- * verdict, and treating it as `light` would under-review
27
- * unjudged work while `standard` preserves today's behaviour.
8
+ * This module used to consume `planningRisk.overallLevel` — a level the planner
9
+ * asserted about the plan it had just written. That signal was self-refereed and
10
+ * its fail-safe ran backwards: an *absent* verdict degraded toward MORE ceremony
11
+ * (standard depth), while a schema-valid all-low verdict bought `light`. Nothing
12
+ * cross-checked the claimed axes against the files actually touched, so the one
13
+ * input that could *reduce* review was exactly the one nobody verified. Story
14
+ * #4542 inverted that: the level is now derived at close time from the changed
15
+ * files themselves, which makes it a fact rather than a claim — and therefore
16
+ * trustworthy enough to reduce ceremony on.
28
17
  *
29
- * Pure and total: inputs in, tier out. No I/O, no throws. `null` / `undefined`
30
- * / malformed inputs all degrade to `standard` (or `deep`/`light` only when the
31
- * width signal unambiguously says so). Callers (e.g. `runCodeReview`) enumerate
32
- * the changed-file count from the diff they already run and supply it here.
18
+ * ## The two signals
19
+ *
20
+ * - **Sensitive paths** ({@link deriveChangeLevel}) the changed-file set
21
+ * intersected with the `sensitivePaths` classes registered in
22
+ * `.agents/schemas/audit-rules.json` (auth, migrations, billing, destructive
23
+ * mutation, public API). Registered as **configuration**, matched with the
24
+ * audit-suite's existing picomatch machinery. This is what keeps a *narrow*
25
+ * high-stakes diff — a three-file auth fix, a small migration — at `deep`,
26
+ * the one thing a pure width heuristic would miss.
27
+ * - **Diff width** — the mechanical changed-file count, unchanged in role.
28
+ *
29
+ * ## Tier rules ({@link resolveDepth})
30
+ *
31
+ * - `deep` — `derivedLevel === 'high'` (a sensitive path was touched) OR
32
+ * the changed-file count exceeds the wide-change scale
33
+ * (`diffWidth.hardFiles`). A wide diff earns a deep pass on
34
+ * size alone even when it touches nothing sensitive.
35
+ * - `light` — `derivedLevel === 'low'` (the change set is known and touches
36
+ * no sensitive path) AND the count is at or below the
37
+ * small-change scale (`diffWidth.softFiles`). An *unknown*
38
+ * count does not block `light`: a missing width is treated as
39
+ * "not wide".
40
+ * - `standard` — everything else, including an absent/underivable level. Fail
41
+ * toward the middle, never toward `light`: when the diff cannot
42
+ * be enumerated there is no evidence the change is safe, and
43
+ * `standard` preserves today's behaviour.
44
+ *
45
+ * {@link resolveDepth} is pure and total: inputs in, tier out. No I/O, no
46
+ * throws. `null` / `undefined` / malformed inputs degrade to `standard` (or
47
+ * `deep`/`light` only when a signal unambiguously says so). {@link
48
+ * deriveChangeLevel} owns the one manifest read and is likewise total — any
49
+ * failure degrades to the `null` (fail-safe) level.
33
50
  *
34
51
  * v2 Stage 2: review depth is **decoupled** from the planning model-capacity
35
52
  * advisory. Diff width is a mechanical review signal (files in the landed
@@ -37,8 +54,11 @@
37
54
  * The two no longer share a constant.
38
55
  *
39
56
  * @typedef {'light'|'standard'|'deep'} ReviewDepth
57
+ * @typedef {'low'|'high'} ChangeLevel
40
58
  */
41
59
 
60
+ import { selectSensitivePathClasses } from '../audit-suite/selector.js';
61
+
42
62
  /**
43
63
  * Mechanical diff-width scales for review-depth tiering. These are **not**
44
64
  * planning Story-sizing ceilings (those are gone in v2) — they only classify
@@ -49,6 +69,50 @@ export const DEFAULT_DIFF_WIDTH = Object.freeze({
49
69
  hardFiles: 30,
50
70
  });
51
71
 
72
+ /**
73
+ * Derive the change level for a change set from observable facts: does the
74
+ * changed-file set intersect any sensitive-path class registered in
75
+ * `audit-rules.json`?
76
+ *
77
+ * This is the **single source** of the derived level — both the review depth
78
+ * ({@link resolveDepth}) and the acceptance-critic fresh-vs-inline routing
79
+ * (`ceremony-routing.js#resolveCeremonyForRisk`) consume what this returns, so
80
+ * the two ceremony decisions can never disagree about how risky a change is.
81
+ *
82
+ * Returns `null` — the fail-safe "no derivable signal" level — when the change
83
+ * set is empty/unknown or the manifest cannot be read. Both downstream
84
+ * consumers treat `null` as the more thorough posture (`standard` depth, a
85
+ * `fresh` critic), so a derivation failure never buys a change less checking.
86
+ *
87
+ * Total: never throws.
88
+ *
89
+ * @param {{
90
+ * changedFiles?: string[]|null,
91
+ * injectedRules?: object,
92
+ * selectSensitivePathClassesFn?: typeof selectSensitivePathClasses,
93
+ * }} [input]
94
+ * @returns {{ level: ChangeLevel|null, classes: string[] }}
95
+ */
96
+ export function deriveChangeLevel(input = {}) {
97
+ const changedFiles =
98
+ input && typeof input === 'object' ? input.changedFiles : null;
99
+ if (!Array.isArray(changedFiles) || changedFiles.length === 0) {
100
+ return { level: null, classes: [] };
101
+ }
102
+
103
+ const select =
104
+ input.selectSensitivePathClassesFn ?? selectSensitivePathClasses;
105
+ let classes;
106
+ try {
107
+ classes = select({ changedFiles, injectedRules: input.injectedRules });
108
+ } catch {
109
+ // An unreadable/invalid manifest is not evidence the change is safe.
110
+ return { level: null, classes: [] };
111
+ }
112
+ const matched = Array.isArray(classes) ? classes : [];
113
+ return { level: matched.length > 0 ? 'high' : 'low', classes: matched };
114
+ }
115
+
52
116
  /**
53
117
  * Coerce an arbitrary input into a non-negative integer changed-file count, or
54
118
  * `null` when the count is unknown / unusable. A `null` count is the neutral
@@ -65,20 +129,20 @@ function normalizeChangedFileCount(value) {
65
129
  }
66
130
 
67
131
  /**
68
- * Resolve the review depth for a change set from its judged risk and diff
69
- * width. See the module header for the tier rules.
132
+ * Resolve the review depth for a change set from its derived level (see
133
+ * {@link deriveChangeLevel}) and diff width. See the module header for the tier
134
+ * rules.
70
135
  *
71
136
  * @param {{
72
- * overallLevel?: ('low'|'medium'|'high'|string|null|undefined),
137
+ * derivedLevel?: (ChangeLevel|string|null|undefined),
73
138
  * changedFileCount?: (number|null|undefined),
74
- * sizing?: { softFiles?: number, hardFiles?: number }|null,
75
139
  * diffWidth?: { softFiles?: number, hardFiles?: number }|null,
76
140
  * }} [input]
77
141
  * @returns {ReviewDepth}
78
142
  */
79
143
  export function resolveDepth(input = {}) {
80
- const overallLevel =
81
- input && typeof input === 'object' ? input.overallLevel : undefined;
144
+ const derivedLevel =
145
+ input && typeof input === 'object' ? input.derivedLevel : undefined;
82
146
  const changedFileCount =
83
147
  input && typeof input === 'object'
84
148
  ? normalizeChangedFileCount(input.changedFileCount)
@@ -97,11 +161,12 @@ export function resolveDepth(input = {}) {
97
161
  // An unknown count is treated as "not wide" — it does not block `light`.
98
162
  const isSmall = changedFileCount === null || changedFileCount <= softFiles;
99
163
 
100
- // deep — high judged risk OR a wide diff.
101
- if (overallLevel === 'high' || isWide) return 'deep';
102
- // light — low judged risk AND a (known-or-unknown-but-not-wide) small diff.
103
- if (overallLevel === 'low' && isSmall) return 'light';
104
- // standard everything else (medium, absent/malformed level, or a low-risk
105
- // diff whose width is between softFiles and hardFiles).
164
+ // deep — a sensitive path was touched OR the diff is wide.
165
+ if (derivedLevel === 'high' || isWide) return 'deep';
166
+ // light — nothing sensitive touched AND a (known-or-unknown-but-not-wide)
167
+ // small diff.
168
+ if (derivedLevel === 'low' && isSmall) return 'light';
169
+ // standard everything else: an underivable level, or a non-sensitive diff
170
+ // whose width sits between softFiles and hardFiles.
106
171
  return 'standard';
107
172
  }
@@ -13,7 +13,6 @@
13
13
  *
14
14
  * @typedef {import('./types.js').Finding} Finding
15
15
  * @typedef {import('./types.js').Severity} Severity
16
- * @typedef {import('./types.js').ReviewScope} ReviewScope
17
16
  */
18
17
 
19
18
  /**
@@ -117,7 +116,6 @@ export function renderManualPromptsSection(messages) {
117
116
  * non-empty.
118
117
  *
119
118
  * @param {{
120
- * scope: ReviewScope,
121
119
  * ticketId: number,
122
120
  * baseRef: string,
123
121
  * headRef: string,
@@ -128,26 +126,18 @@ export function renderManualPromptsSection(messages) {
128
126
  * @returns {string}
129
127
  */
130
128
  export function renderFindings(input) {
131
- const {
132
- scope,
133
- ticketId,
134
- baseRef,
135
- headRef,
136
- findings,
137
- provider,
138
- promptMessages,
139
- } = input;
129
+ const { ticketId, baseRef, headRef, findings, provider, promptMessages } =
130
+ input;
140
131
  const counts = countBySeverity(findings);
141
132
  const totalKnown =
142
133
  counts.critical + counts.high + counts.medium + counts.suggestion;
143
134
 
144
- const scopeLabel = scope === 'epic' ? 'Epic' : 'Story';
145
135
  const providerLine = provider
146
136
  ? `**Provider**: \`${provider}\``
147
137
  : '**Provider**: _(unspecified)_';
148
138
 
149
139
  const lines = [
150
- `## 🔬 Code Review — ${scopeLabel} #${ticketId}`,
140
+ `## 🔬 Code Review — Story #${ticketId}`,
151
141
  '',
152
142
  `**Comparison**: \`${baseRef}\` … \`${headRef}\``,
153
143
  providerLine,