mandrel 2.30.0 → 2.32.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 (253) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +4 -4
  4. package/.agents/docs/agentrc-reference.json +61 -57
  5. package/.agents/docs/configuration.md +274 -227
  6. package/.agents/docs/execution-reference.md +13 -14
  7. package/.agents/docs/quality-gates.md +195 -23
  8. package/.agents/instructions.md +2 -5
  9. package/.agents/rules/ci-remediation.md +44 -1
  10. package/.agents/rules/git-conventions-reference.md +27 -27
  11. package/.agents/rules/git-conventions.md +4 -2
  12. package/.agents/rules/known-tooling-behavior.md +66 -30
  13. package/.agents/rules/testing-standards.md +35 -71
  14. package/.agents/runtime-deps.json +0 -1
  15. package/.agents/schemas/agentrc.schema.json +1939 -1400
  16. package/.agents/schemas/lifecycle/README.md +21 -14
  17. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  18. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  19. package/.agents/scripts/README.md +7 -29
  20. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  21. package/.agents/scripts/bootstrap.js +28 -26
  22. package/.agents/scripts/check-baseline-drift.js +73 -13
  23. package/.agents/scripts/check-baseline-scope.js +362 -0
  24. package/.agents/scripts/check-dead-exports.js +9 -1
  25. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  26. package/.agents/scripts/check-knip-entries.js +136 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  28. package/.agents/scripts/check-schema-references.js +1 -1
  29. package/.agents/scripts/diagnose-friction.js +7 -4
  30. package/.agents/scripts/generate-config-docs.js +263 -171
  31. package/.agents/scripts/install-matrix-assert.js +0 -1
  32. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  33. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  34. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  35. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  36. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  37. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
  38. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  39. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  40. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  41. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  42. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  43. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  44. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  45. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  46. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  47. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  48. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  49. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  50. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  51. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  52. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  53. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  54. package/.agents/scripts/lib/child-exec.js +193 -0
  55. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  56. package/.agents/scripts/lib/cli-args.js +64 -0
  57. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  58. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  59. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  60. package/.agents/scripts/lib/config/commands.js +3 -5
  61. package/.agents/scripts/lib/config/explain.js +5 -7
  62. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  63. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  64. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  65. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  66. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  67. package/.agents/scripts/lib/config/gates/index.js +5 -2
  68. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  69. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  70. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  71. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  72. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  73. package/.agents/scripts/lib/config/github.js +0 -1
  74. package/.agents/scripts/lib/config/limits.js +3 -48
  75. package/.agents/scripts/lib/config/qa.js +105 -0
  76. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  77. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  78. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  79. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  80. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  81. package/.agents/scripts/lib/crap-utils.js +84 -520
  82. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  83. package/.agents/scripts/lib/degraded-mode.js +2 -2
  84. package/.agents/scripts/lib/doc-tiers.js +3 -3
  85. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  86. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  87. package/.agents/scripts/lib/fs-walk.js +52 -0
  88. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  89. package/.agents/scripts/lib/git-utils.js +16 -36
  90. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  91. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  92. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  93. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  94. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  95. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  96. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  97. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  98. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  99. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  100. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  101. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  102. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  103. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  104. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  105. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  106. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  107. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  108. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  109. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  110. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  111. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  112. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  113. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +17 -2
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  124. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +65 -25
  125. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  126. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  127. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  128. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  129. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  130. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  131. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  132. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  133. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  134. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  135. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  136. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  137. package/.agents/scripts/lib/signals/index.js +8 -6
  138. package/.agents/scripts/lib/signals/schema.js +20 -25
  139. package/.agents/scripts/lib/signals/write.js +8 -8
  140. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  141. package/.agents/scripts/lib/temp-retention.js +1 -1
  142. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  143. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  144. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  145. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  146. package/.agents/scripts/lib/worktree-manager.js +2 -3
  147. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  148. package/.agents/scripts/pr-watch-with-update.js +7 -5
  149. package/.agents/scripts/providers/github/cache.js +2 -2
  150. package/.agents/scripts/providers/github/comments.js +6 -28
  151. package/.agents/scripts/providers/github/compose.js +0 -15
  152. package/.agents/scripts/providers/github/errors.js +10 -27
  153. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  154. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  155. package/.agents/scripts/providers/github.js +4 -7
  156. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  157. package/.agents/scripts/resolve-stories.js +0 -2
  158. package/.agents/scripts/run-lint.js +61 -61
  159. package/.agents/scripts/run-test-profile.js +6 -6
  160. package/.agents/scripts/run-verify.js +48 -30
  161. package/.agents/scripts/single-story-close.js +20 -0
  162. package/.agents/scripts/single-story-init.js +12 -35
  163. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  164. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  165. package/.agents/skills/skills.index.json +2 -12
  166. package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
  167. package/.agents/workflows/audit-documentation.md +5 -6
  168. package/.agents/workflows/audit-to-stories.md +2 -2
  169. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  170. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  171. package/.agents/workflows/helpers/code-review.md +3 -8
  172. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  173. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  174. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  175. package/.agents/workflows/mandrel-update.md +10 -10
  176. package/.agents/workflows/qa-assist.md +15 -20
  177. package/.agents/workflows/qa-explore.md +9 -8
  178. package/README.md +1 -1
  179. package/docs/CHANGELOG.md +49 -0
  180. package/lib/migrations/index.js +2 -0
  181. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  182. package/package.json +12 -3
  183. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  184. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  185. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  186. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  187. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  188. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  189. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  190. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  191. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  192. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  193. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  194. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  195. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  196. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  197. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  198. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  199. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  200. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  201. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  202. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  203. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  204. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  205. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  206. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  207. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  208. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  209. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  210. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  211. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  212. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  213. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  214. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  215. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  216. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  217. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  218. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  219. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  220. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  221. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  222. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  223. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  224. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  225. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  226. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  227. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  228. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  229. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  230. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  231. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  232. package/.agents/scripts/lib/planning-corpus.js +0 -37
  233. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  234. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  235. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  236. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  237. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  238. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  239. package/.agents/scripts/lib/signals/read.js +0 -268
  240. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  241. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  242. package/.agents/scripts/lib/story-plan.js +0 -379
  243. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  244. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  245. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  246. package/.agents/scripts/lint-baseline.js +0 -507
  247. package/.agents/scripts/providers/github/prs.js +0 -103
  248. package/.agents/scripts/signals-view.js +0 -309
  249. package/.agents/scripts/story-plan.js +0 -370
  250. package/.agents/scripts/sync-branch-from-base.js +0 -149
  251. package/.agents/scripts/validate-docs-freshness.js +0 -314
  252. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  253. package/.agents/workflows/helpers/signals.md +0 -112
@@ -1,13 +1,18 @@
1
- // .agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js
1
+ // .agents/scripts/lib/orchestration/pr-watch.js
2
2
  /**
3
- * Watcherlifecycle listener that owns the required-check poll loop
4
- * for an open Epic PR. Story #2256 / Task #2261 (Epic #2172).
3
+ * pr-watch.js — the required-check poll loop for an open PR.
4
+ * Story #2256 (Epic #2172).
5
5
  *
6
- * Subscribes to:
7
- * - `pr.created` resolve the required-check names from GitHub at
8
- * runtime via `gh pr checks <pr> --required`, then poll until every
9
- * check reaches a terminal state (or a per-listener wall-clock
10
- * deadline expires), recording the outcome in `classifications`.
6
+ * **Not a listener, and no longer shelved among them.** This shipped as the
7
+ * `Watcher` lifecycle listener subscribing to `pr.created`. Story #5006
8
+ * deleted the class but left the plain primitive at
9
+ * `lifecycle/listeners/watcher.js` so its CLI consumer's import stayed
10
+ * stable. Story #5024 then retired the bus itself, which took the listener
11
+ * concept — and the `listeners/` directory — with it, so the primitive moved
12
+ * here beside [`merge-poll.js`](./merge-poll.js), the home Story #4545 chose
13
+ * for `MergeWatcher`'s surviving parts. The only production consumer,
14
+ * `pr-watch-with-update.js`, has always driven {@link watchPrToTerminal}
15
+ * directly, with no bus.
11
16
  *
12
17
  * Critical contract:
13
18
  * - Required-check **names** are resolved from `gh pr checks` at
@@ -16,29 +21,18 @@
16
21
  * branch-protection ruleset on GitHub is the source of truth at
17
22
  * watch time. This guards against config drift (a config file that
18
23
  * hasn't been updated after a protection rule changed on GitHub
19
- * would otherwise cause the watcher to either skip a required check
24
+ * would otherwise cause the watch to either skip a required check
20
25
  * or wait for a removed one indefinitely).
21
26
  *
22
- * Idempotency contract (AC-10): per-instance `Set<string>` of
23
- * `${event}:${seqId}` keys. A repeat `(event, seqId)` short-circuits
24
- * without re-polling and emits nothing. Combined with the bus-level
25
- * replay defence, this is sufficient — re-running `/deliver` after
26
- * a crash will produce a NEW seqId and the listener legitimately
27
- * re-runs the poll loop (which is itself idempotent: the outcome map
28
- * always reflects the live GitHub state).
29
- *
30
- * Side-effect firewall: the listener shells out to `gh` and records
31
- * its outcome in the in-memory `classifications` log. It does NOT
32
- * mutate ticket labels, post comments, call `notify`, or emit on the
33
- * bus — the production consumer (`pr-watch-with-update.js`) drives
34
- * `watchPrToTerminal` directly without a bus, and the retired
35
- * `epic.watch.start` / `epic.watch.end` bus events went with the
36
- * Epic-orchestration stratum.
27
+ * Side-effect firewall: the loop shells out to `gh` through injectable
28
+ * ports and returns a verdict. It does NOT mutate ticket labels, post
29
+ * comments, call `notify`, or write any ledger.
37
30
  */
38
31
 
39
32
  import { spawnSync } from 'node:child_process';
40
33
 
41
- import { parsePrNumberFromUrl } from '../../../github-url.js';
34
+ import { parsePrNumberFromUrl } from '../github-url.js';
35
+ import { applyBehindUpdate } from './behind-recovery.js';
42
36
 
43
37
  /**
44
38
  * Map `gh pr checks` `state` values to the canonical lowercase outcome
@@ -121,8 +115,8 @@ export function normalizeCheckState(raw) {
121
115
  }
122
116
 
123
117
  /**
124
- * Parse a PR number out of a PR URL. The bus contract gives us
125
- * `pr.created.prUrl`; `gh pr checks` accepts either the URL or the
118
+ * Parse a PR number out of a PR URL. Callers hand in the URL `gh pr
119
+ * create` returned; `gh pr checks` accepts either the URL or the
126
120
  * number — we pass the URL through verbatim, but the helper still
127
121
  * exists for tests asserting we never silently coerce a malformed URL.
128
122
  *
@@ -184,9 +178,9 @@ function ghPrChecks({ prUrl, cwd, repo, spawnFn = spawnSync }) {
184
178
  }
185
179
 
186
180
  /**
187
- * Default `gh pr view` spawn — probes `mergeStateStatus` so the Watcher
181
+ * Default `gh pr view` spawn — probes `mergeStateStatus` so the watch loop
188
182
  * can detect the BEHIND condition (PR head is behind its base branch)
189
- * AFTER every required check is green. Exported so tests can stub.
183
+ * AFTER every required check is green. Injectable so tests can stub.
190
184
  */
191
185
  function ghPrView({ prUrl, cwd, repo, spawnFn = spawnSync }) {
192
186
  const result = spawnFn(
@@ -377,8 +371,8 @@ function defaultSleep(ms) {
377
371
  * an empty stdout) are logged and skipped — the outer cap eventually
378
372
  * short-circuits if `gh` is unrecoverably broken.
379
373
  *
380
- * Exported so the BEHIND-recovery outer loop in `Watcher.handle()` can
381
- * call this for each CI cycle without duplicating the inner logic.
374
+ * Exported so {@link watchPrToTerminal}'s BEHIND-recovery outer loop can
375
+ * call it once per CI cycle without duplicating the inner logic.
382
376
  *
383
377
  * @param {object} opts
384
378
  * @param {string} opts.prUrl
@@ -434,9 +428,10 @@ export async function pollUntilTerminal({
434
428
  * commit after each. Plain async function with NO bus coupling: it
435
429
  * shells out to `gh` (via injectable spawns) and returns the verdict.
436
430
  *
437
- * This is the load-bearing primitive shared by the `Watcher` lifecycle
438
- * listener (`handle()`) and the `pr-watch-with-update.js` CLI, so both
439
- * paths perform identical polling and BEHIND-recovery. Story #3902.
431
+ * The load-bearing primitive the `pr-watch-with-update.js` CLI drives.
432
+ * Story #3902 introduced it so the (since-retired) `Watcher` listener and
433
+ * the CLI could not drift apart on polling or BEHIND-recovery; the CLI is
434
+ * now its only caller.
440
435
  *
441
436
  * @param {object} opts
442
437
  * @param {string} opts.prUrl PR URL or number (passed to `gh` verbatim).
@@ -463,11 +458,10 @@ export async function pollUntilTerminal({
463
458
  * real `setTimeout`-backed sleep; tests override with a no-op.
464
459
  * @param {{ info?: Function, warn?: Function, debug?: Function }} opts.logger
465
460
  * @param {{status:number,stdout:string,stderr:string}} [opts.firstProbe]
466
- * Optional already-issued `gh pr checks` result. When the caller (the
467
- * `Watcher` listener) has already probed once to resolve the required
468
- * check names, it threads that result here so the loop does not
469
- * double-spend the first `gh pr checks` call. Omit it (the CLI path)
470
- * and the loop issues the first probe itself.
461
+ * Optional already-issued `gh pr checks` result. A caller that has
462
+ * already probed once to resolve the required-check names threads it
463
+ * here so the loop does not double-spend the first `gh pr checks` call.
464
+ * Omit it (the CLI path) and the loop issues the first probe itself.
471
465
  * @returns {Promise<{
472
466
  * outcomes: object,
473
467
  * requiredChecks: string[],
@@ -502,8 +496,8 @@ export async function watchPrToTerminal({
502
496
  firstProbe,
503
497
  }) {
504
498
  // First probe: resolve the required-check name set at runtime. Reuse a
505
- // caller-supplied probe (the listener already issued one to resolve the
506
- // required check names) so we never double-spend the first `gh` call.
499
+ // caller-supplied probe (issued to resolve the required-check names) so
500
+ // we never double-spend the first `gh` call.
507
501
  const first = firstProbe ?? ghPrChecksFn({ prUrl, cwd, repo });
508
502
  // `gh` exits 8 when checks are still pending; this is expected and
509
503
  // does not indicate failure. Any other non-zero status with no
@@ -579,6 +573,9 @@ export async function watchPrToTerminal({
579
573
  // Bounded by `maxUpdates` so a racing base branch can't ping-pong
580
574
  // indefinitely.
581
575
  if (!allTerminal(outcomes) || !allGreen(outcomes)) break;
576
+ // Budget is checked before the `gh pr view` spawn so an exhausted arm
577
+ // costs no extra round-trip; the shared helper re-checks it as a
578
+ // fail-safe, and its callback therefore never fires on this path.
582
579
  if (updatesApplied >= maxUpdates) break;
583
580
  const view = ghPrViewFn({ prUrl, cwd, repo });
584
581
  if (view.status !== 0) {
@@ -587,15 +584,26 @@ export async function watchPrToTerminal({
587
584
  );
588
585
  break;
589
586
  }
590
- const mergeStateStatus = parseMergeStateStatus(view.stdout);
591
- if (mergeStateStatus !== 'BEHIND') break;
592
- const update = ghPrUpdateBranchFn({ prUrl, cwd, repo });
593
- if (update.status !== 0) {
594
- logger.warn?.(
595
- `[Watcher] gh pr update-branch failed (status=${update.status}): ${update.stderr}`,
596
- );
597
- break;
598
- }
587
+ const recovery = await applyBehindUpdate({
588
+ mergeStateStatus: parseMergeStateStatus(view.stdout),
589
+ updatesUsed: updatesApplied,
590
+ maxUpdates,
591
+ updateBranch: async () => {
592
+ const update = ghPrUpdateBranchFn({ prUrl, cwd, repo });
593
+ return update.status === 0
594
+ ? { ok: true }
595
+ : {
596
+ ok: false,
597
+ detail: `status=${update.status}: ${update.stderr}`,
598
+ };
599
+ },
600
+ onUpdateFailed: (detail) =>
601
+ logger.warn?.(`[Watcher] gh pr update-branch failed (${detail})`),
602
+ });
603
+ // Anything but a landed fast-forward ends the arm: not BEHIND means
604
+ // there is nothing to recover, and a failed update must not silently
605
+ // re-poll as though the head moved.
606
+ if (!recovery.updated) break;
599
607
  updatesApplied += 1;
600
608
  logger.info?.(
601
609
  `[Watcher] PR BEHIND base — issued gh pr update-branch (#${updatesApplied}/${maxUpdates}); re-polling required checks.`,
@@ -643,161 +651,3 @@ export async function watchPrToTerminal({
643
651
  stillRunning,
644
652
  };
645
653
  }
646
-
647
- /**
648
- * Watcher listener.
649
- */
650
- export class Watcher {
651
- /**
652
- * @param {object} opts
653
- * @param {object} opts.bus
654
- * @param {string} [opts.cwd]
655
- * @param {number} [opts.pollIntervalMs] default 10_000.
656
- * @param {number} [opts.maxPolls] safety cap on iterations; default
657
- * 180 (≈30 min @ 10s).
658
- * @param {number} [opts.maxUpdates] cap on `gh pr update-branch`
659
- * recovery calls per `pr.created` event; default 3. Mirrors the
660
- * legacy `pr-watch-with-update` cap so a racing base branch
661
- * can't induce an infinite update-branch ping-pong.
662
- * @param {number} [opts.maxResumes] Story #4358: how many times to
663
- * re-arm the poll loop after the cap fires with checks still pending
664
- * (and none failed) before declaring `still-running`; default 0.
665
- * @param {Function} [opts.ghPrChecksFn] override for tests.
666
- * @param {Function} [opts.ghPrViewFn] override for tests; resolves
667
- * `mergeStateStatus` for the BEHIND-recovery gate.
668
- * @param {Function} [opts.ghPrUpdateBranchFn] override for tests;
669
- * issues the fast-forward update on the PR.
670
- * @param {Function} [opts.sleepFn] override for tests.
671
- * @param {{ info?: Function, warn?: Function, debug?: Function }} [opts.logger]
672
- */
673
- constructor(opts = {}) {
674
- if (!opts.bus || typeof opts.bus.on !== 'function') {
675
- throw new TypeError('Watcher requires a bus with on()');
676
- }
677
- this.bus = opts.bus;
678
- this.cwd = opts.cwd ?? process.cwd();
679
- this.pollIntervalMs = Number.isInteger(opts.pollIntervalMs)
680
- ? opts.pollIntervalMs
681
- : 10_000;
682
- this.maxPolls = Number.isInteger(opts.maxPolls) ? opts.maxPolls : 180;
683
- this.maxUpdates =
684
- Number.isInteger(opts.maxUpdates) && opts.maxUpdates >= 0
685
- ? opts.maxUpdates
686
- : 3;
687
- this.maxResumes =
688
- Number.isInteger(opts.maxResumes) && opts.maxResumes >= 0
689
- ? opts.maxResumes
690
- : 0;
691
- this.ghPrChecksFn = opts.ghPrChecksFn ?? ghPrChecks;
692
- this.ghPrViewFn = opts.ghPrViewFn ?? ghPrView;
693
- this.ghPrUpdateBranchFn = opts.ghPrUpdateBranchFn ?? ghPrUpdateBranch;
694
- this.sleepFn = opts.sleepFn ?? defaultSleep;
695
- this.logger = opts.logger ?? console;
696
- /** @type {Set<string>} `${event}:${seqId}` idempotency cache. */
697
- this._seen = new Set();
698
- /**
699
- * Classification log — every `pr.created` we observe lands here
700
- * with the outcome (`watched`, `failed`, `skipped-duplicate`,
701
- * `still-running`, `timed-out`). Mirrors the Finalizer / Reconciler
702
- * "no silent skip" surface.
703
- */
704
- this.classifications = [];
705
- this.events = Object.freeze(['pr.created']);
706
- }
707
-
708
- register() {
709
- return this.events.map((event) =>
710
- this.bus.on(event, async (ctx) => this.handle(ctx)),
711
- );
712
- }
713
-
714
- async handle({ event, seqId, payload }) {
715
- const key = `${event}:${seqId}`;
716
- if (this._seen.has(key)) {
717
- this.classifications.push({
718
- event,
719
- seqId,
720
- outcome: 'skipped',
721
- reason: 'duplicate-seqId',
722
- });
723
- this.logger.debug?.(`[Watcher] skip duplicate ${key} (idempotent)`);
724
- return;
725
- }
726
- this._seen.add(key);
727
-
728
- const prUrl = payload?.prUrl;
729
- if (typeof prUrl !== 'string' || prUrl.length === 0) {
730
- this.classifications.push({
731
- event,
732
- seqId,
733
- outcome: 'failed',
734
- reason: 'no-pr-url',
735
- });
736
- return;
737
- }
738
-
739
- // First probe: resolve the required-check name set at runtime BEFORE
740
- // the (potentially long) poll loop runs. We thread this probe into
741
- // `watchPrToTerminal` (via `firstProbe`) so the shared loop reuses it
742
- // instead of double-spending the first `gh pr checks` call.
743
- const first = this.ghPrChecksFn({ prUrl, cwd: this.cwd });
744
- const firstEntries = parseGhPrChecks(first.stdout);
745
- if (firstEntries.length === 0 && first.status !== 0 && first.status !== 8) {
746
- this.classifications.push({
747
- event,
748
- seqId,
749
- outcome: 'failed',
750
- reason: `gh-checks-failed:status=${first.status}`,
751
- });
752
- this.logger.warn?.(
753
- `[Watcher] gh pr checks failed (status=${first.status}): ${first.stderr}`,
754
- );
755
- return;
756
- }
757
-
758
- const requiredChecks = firstEntries.map((e) => e.name);
759
-
760
- // Delegate the poll + BEHIND-recovery loop to the shared plain
761
- // primitive so the CLI (`pr-watch-with-update.js`) and this listener
762
- // run identical logic.
763
- const { polls, updatesApplied, resumesApplied, terminal, stillRunning } =
764
- await watchPrToTerminal({
765
- prUrl,
766
- cwd: this.cwd,
767
- maxPolls: this.maxPolls,
768
- maxUpdates: this.maxUpdates,
769
- maxResumes: this.maxResumes,
770
- pollIntervalMs: this.pollIntervalMs,
771
- ghPrChecksFn: this.ghPrChecksFn,
772
- ghPrViewFn: this.ghPrViewFn,
773
- ghPrUpdateBranchFn: this.ghPrUpdateBranchFn,
774
- sleepFn: this.sleepFn,
775
- logger: this.logger,
776
- firstProbe: first,
777
- });
778
-
779
- // `still-running` (slow CI, not red) is a distinct classification from
780
- // a genuine `timed-out` — reserved for a check that never went
781
- // terminal within the poll cap AND the resume budget while none
782
- // failed. `watched` covers every terminal arm (green or red).
783
- const outcome = terminal
784
- ? 'watched'
785
- : stillRunning
786
- ? 'still-running'
787
- : 'timed-out';
788
- this.classifications.push({
789
- event,
790
- seqId,
791
- outcome,
792
- polls,
793
- updatesApplied,
794
- resumesApplied,
795
- requiredChecks: requiredChecks.length,
796
- });
797
- }
798
-
799
- reset() {
800
- this._seen.clear();
801
- this.classifications = [];
802
- }
803
- }
@@ -301,9 +301,6 @@ export async function readNativeBlockedBy({
301
301
  * @param {Map<number, number[]>} nativeEdges
302
302
  * @param {number[]} foreignDone Ids outside the set already satisfied.
303
303
  * @param {(msg: string) => void} [warn]
304
- * @param {object} [injectedRules] Test seam forwarded to the shape
305
- * derivation — skips the `audit-rules.json` disk read. Production callers
306
- * omit it (the real manifest, memoized per process, is the default).
307
304
  * @returns {{ kind: string, stories: object[], dag: object[], done: number[] }}
308
305
  */
309
306
  export function buildStoriesEnvelope({
@@ -311,8 +308,6 @@ export function buildStoriesEnvelope({
311
308
  nativeEdges = new Map(),
312
309
  foreignDone = [],
313
310
  warn,
314
- config,
315
- injectedRules,
316
311
  }) {
317
312
  const sorted = [...stories].sort((a, b) => a.id - b.id);
318
313
  const inSetDone = sorted.filter(isSatisfiedBlocker).map((s) => s.id);
@@ -324,8 +319,8 @@ export function buildStoriesEnvelope({
324
319
  // acceptance-critic sub-agent boots) or `subagent` (the conservative
325
320
  // default). Model-side fan-out only; close gates are untouched.
326
321
  //
327
- // `storyCount` is the premise that decides it, and it is this call site's
328
- // load-bearing argument: `inline` names the router's ONE session, so it is
322
+ // `storyCount` is the ONLY premise that decides it, and it is this call
323
+ // site's whole argument: `inline` names the router's ONE session, so it is
329
324
  // granted only to a run resolving exactly ONE Story, which has no
330
325
  // concurrent sibling to share that session with. Passing the resolved set
331
326
  // size here is therefore what makes the envelope self-consistent with the
@@ -336,19 +331,14 @@ export function buildStoriesEnvelope({
336
331
  // a caller reads for a given `--ids` list never changes as siblings land
337
332
  // mid-run. The `route::lite` label is a human-visible hint only, never the
338
333
  // control signal.
339
- stories: sorted.map(({ id, title, body, url, labels, state }) => ({
334
+ stories: sorted.map(({ id, title, url, labels, state }) => ({
340
335
  id,
341
336
  title,
342
337
  url,
343
338
  labels,
344
339
  state,
345
- dispatchMode: resolveStoryDispatchMode({
346
- body,
347
- labels,
348
- config,
349
- storyCount: sorted.length,
350
- injectedRules,
351
- }).mode,
340
+ dispatchMode: resolveStoryDispatchMode({ storyCount: sorted.length })
341
+ .mode,
352
342
  })),
353
343
  dag: storiesToDag(sorted, nativeEdges, warn),
354
344
  done: [...new Set([...inSetDone, ...foreignDone])].sort((a, b) => a - b),
@@ -19,7 +19,7 @@
19
19
  * `provider: native` explicitly; the probe is the only thing that
20
20
  * routes between "configured backend present" and "configured backend
21
21
  * missing". The adapter never consults a GitHub provider — the
22
- * orchestrator owns posting/upserting and the lifecycle bus.
22
+ * orchestrator owns posting and upserting.
23
23
  *
24
24
  * @typedef {import('./types.js').Finding} Finding
25
25
  * @typedef {import('./types.js').ReviewInput} ReviewInput
@@ -0,0 +1,130 @@
1
+ /**
2
+ * review-providers/mi-exemptions.js — the maintainability gate's exemption list,
3
+ * as the native review provider reads it.
4
+ *
5
+ * `delivery.quality.gates.maintainability.ignoreGlobs` is the one declared
6
+ * answer to "which files is the maintainability index meaningless for" —
7
+ * declarative schema blobs, generated code, vendored trees. Every other MI
8
+ * consumer already honours it: the baseline writer excludes those files
9
+ * (`update-maintainability-baseline.js`), so the `check-baselines.js` ratchet
10
+ * and the pre-merge MI advisory never see them either.
11
+ *
12
+ * `review-providers/native.js` did not, and the split produced a live
13
+ * contradiction on Story #5007 / PR #5022: the ratchet PASSED while the review
14
+ * lens raised a **critical blocker** on three exempted
15
+ * `config-settings-schema*.js` modules in the same close run. Because a critical
16
+ * finding halts `single-story-close.js` before auto-merge, the only way to land
17
+ * legitimate work was to merge the PR by hand. A gate that must be
18
+ * hand-bypassed to ship is not a gate.
19
+ *
20
+ * This module is that reconciliation, kept separate from the provider so the
21
+ * exemption concern has one home and the provider keeps one reason to change.
22
+ */
23
+
24
+ import { getQuality } from '../../config/quality.js';
25
+ import { resolveConfig } from '../../config-resolver.js';
26
+ import { isIgnoredByGlobs } from '../../maintainability-utils.js';
27
+
28
+ /**
29
+ * Read `delivery.quality.gates.maintainability.ignoreGlobs` so a review's
30
+ * maintainability dimension scores the same file set the ratchet does.
31
+ *
32
+ * Best-effort and total. A config that cannot be resolved yields `[]`, which
33
+ * scores every changed JS file. That direction is deliberate: degrading to
34
+ * "score everything" can only produce an advisory the operator must read,
35
+ * whereas degrading to "score nothing" would silently retire the dimension.
36
+ *
37
+ * @param {{ resolveConfigFn?: typeof resolveConfig, getQualityFn?: typeof getQuality }} [deps]
38
+ * @returns {string[]} minimatch patterns; `[]` when unset or unresolvable.
39
+ */
40
+ export function resolveMaintainabilityIgnoreGlobs({
41
+ resolveConfigFn = resolveConfig,
42
+ getQualityFn = getQuality,
43
+ } = {}) {
44
+ try {
45
+ const globs = getQualityFn(resolveConfigFn())?.maintainability?.ignoreGlobs;
46
+ return Array.isArray(globs) ? globs.slice() : [];
47
+ } catch {
48
+ return [];
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Pure: split a changed-file list into the set to score and the set the
54
+ * maintainability gate exempts.
55
+ *
56
+ * Matching funnels through `maintainability-utils.js#isIgnoredByGlobs` — the
57
+ * declared single source of truth for how the MI scorer decides a file is
58
+ * ignored — so an exempted file is excluded here by exactly the same rule that
59
+ * kept it out of the baseline. Re-implementing the match with a local
60
+ * `minimatch` call is what let the two surfaces disagree in the first place.
61
+ *
62
+ * Module-local: {@link scopeMaintainabilityFiles} is the single door, and its
63
+ * `scored` / `ignored` split is where this behaviour is observable.
64
+ *
65
+ * @param {string[]} files
66
+ * @param {string[]} ignoreGlobs
67
+ * @param {string} cwd root for repo-relative glob resolution
68
+ * @returns {{ scored: string[], ignored: string[] }}
69
+ */
70
+ function partitionByIgnoreGlobs(files, ignoreGlobs, cwd) {
71
+ if (!Array.isArray(ignoreGlobs) || ignoreGlobs.length === 0) {
72
+ return { scored: files, ignored: [] };
73
+ }
74
+ const scored = [];
75
+ const ignored = [];
76
+ for (const relPath of files) {
77
+ if (isIgnoredByGlobs(relPath, ignoreGlobs, cwd)) ignored.push(relPath);
78
+ else scored.push(relPath);
79
+ }
80
+ return { scored, ignored };
81
+ }
82
+
83
+ /**
84
+ * Render the operator-facing notice naming the files the gate exempted, or
85
+ * `null` when nothing was exempted so the caller can `if` past the log call.
86
+ *
87
+ * The notice exists because silence is ambiguous: an operator reading a review
88
+ * that says nothing about three changed schema modules cannot tell "scored and
89
+ * healthy" from "never scored".
90
+ *
91
+ * Module-local: reached through {@link scopeMaintainabilityFiles}'s `notice`.
92
+ *
93
+ * @param {string[]|undefined} ignoredFiles
94
+ * @returns {string|null}
95
+ */
96
+ function formatExemptionNotice(ignoredFiles) {
97
+ const files = Array.isArray(ignoredFiles) ? ignoredFiles : [];
98
+ if (files.length === 0) return null;
99
+ return (
100
+ `[native-review] Maintainability: ${files.length} changed file(s) exempt via ` +
101
+ `delivery.quality.gates.maintainability.ignoreGlobs — not scored: ${files.join(', ')}.`
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Resolve the exemption list and split a review's changed-file set into the
107
+ * paths whose maintainability should be scored and the paths the gate exempts.
108
+ *
109
+ * This is the one door the native provider uses: it keeps the resolve → match →
110
+ * report sequence here rather than spread across the provider, so the provider
111
+ * carries no knowledge of how an exemption is decided.
112
+ *
113
+ * @param {string[]} changedFiles
114
+ * @param {{
115
+ * cwd: string,
116
+ * resolveIgnoreGlobsFn?: typeof resolveMaintainabilityIgnoreGlobs,
117
+ * }} opts
118
+ * @returns {{ scored: string[], ignored: string[], notice: string|null }}
119
+ */
120
+ export function scopeMaintainabilityFiles(
121
+ changedFiles,
122
+ { cwd, resolveIgnoreGlobsFn = resolveMaintainabilityIgnoreGlobs } = {},
123
+ ) {
124
+ const { scored, ignored } = partitionByIgnoreGlobs(
125
+ changedFiles,
126
+ resolveIgnoreGlobsFn(),
127
+ cwd,
128
+ );
129
+ return { scored, ignored, notice: formatExemptionNotice(ignored) };
130
+ }
@@ -7,14 +7,24 @@
7
7
  *
8
8
  * 1. Diffs `headRef` against `baseRef` to enumerate changed files.
9
9
  * 2. Runs scoped lint (biome + markdownlint) over the changed surface.
10
- * 3. Computes per-file maintainability reports for changed JS files.
10
+ * 3. Computes per-file maintainability reports for changed JS files, minus
11
+ * the files the maintainability gate exempts (see below).
11
12
  * 4. Maps each signal to a `Finding` with a `severity` ∈ {critical, high,
12
13
  * medium, suggestion}.
13
14
  *
14
- * The adapter does NOT post to GitHub, does NOT render a markdown body,
15
- * and does NOT consult the lifecycle bus. Those concerns belong to
16
- * `runCodeReview()` (which calls the renderer + the structured-comment
17
- * upserter) and the listener chain.
15
+ * **The maintainability dimension honours the gate's exemption list**, read via
16
+ * [`mi-exemptions.js`](mi-exemptions.js) see that module for why this
17
+ * provider disagreeing with the ratchet was a live delivery blocker. Exempted
18
+ * files are named on the log rather than silently dropped.
19
+ *
20
+ * The lint dimension is deliberately NOT filtered through the same list:
21
+ * lint carries its own exclusion surface (biome's `files.includes`,
22
+ * `.markdownlintignore`), and a quality-gate ignore glob makes no claim about
23
+ * whether a file should parse or format cleanly.
24
+ *
25
+ * The adapter does NOT post to GitHub and does NOT render a markdown body.
26
+ * Those concerns belong to `runCodeReview()`, which calls the renderer + the
27
+ * structured-comment upserter.
18
28
  *
19
29
  * Construction is intentionally zero-arg so the factory can instantiate
20
30
  * it without threading config through every call. Per-invocation config
@@ -53,6 +63,10 @@ import {
53
63
  } from '../../observability/runtime-friction.js';
54
64
  import { PROJECT_ROOT } from '../../project-root.js';
55
65
  import { transpileIfNeeded } from '../../transpile.js';
66
+ import {
67
+ resolveMaintainabilityIgnoreGlobs,
68
+ scopeMaintainabilityFiles,
69
+ } from './mi-exemptions.js';
56
70
  import {
57
71
  parseLintOutput,
58
72
  partitionFilesForLint,
@@ -389,16 +403,6 @@ export function buildLintFindings(lintSummary) {
389
403
  return findings;
390
404
  }
391
405
 
392
- function _emptyResults() {
393
- return {
394
- totalFiles: 0,
395
- jsFiles: 0,
396
- maintainability: [],
397
- criticalFindings: [],
398
- mediumFindings: [],
399
- };
400
- }
401
-
402
406
  async function runLintPhase({
403
407
  scopeLint,
404
408
  changedFiles,
@@ -467,6 +471,7 @@ function buildLintDegradations(lintSummary) {
467
471
  * analyzeChangedFilesFn?: typeof analyzeChangedFiles,
468
472
  * buildLintFindingsFn?: typeof buildLintFindings,
469
473
  * emitToolDegradationFn?: typeof emitRuntimeFriction,
474
+ * resolveIgnoreGlobsFn?: typeof resolveMaintainabilityIgnoreGlobs,
470
475
  * logger?: { info?: Function, warn?: Function, error?: Function },
471
476
  * scopeLint?: 'changed-only'|'off',
472
477
  * }} [deps]
@@ -479,6 +484,10 @@ export function createNativeProvider(deps = {}) {
479
484
  analyzeChangedFilesFn = analyzeChangedFiles,
480
485
  buildLintFindingsFn = buildLintFindings,
481
486
  emitToolDegradationFn = emitRuntimeFriction,
487
+ // The maintainability-gate exemption seam. The resolution itself — gate-key
488
+ // read and fail-open — is unit-tested in `mi-exemptions.js`; this dep is
489
+ // here so a provider test can pin the WIRING without a config on disk.
490
+ resolveIgnoreGlobsFn = resolveMaintainabilityIgnoreGlobs,
482
491
  logger,
483
492
  scopeLint = 'changed-only',
484
493
  } = deps;
@@ -551,7 +560,12 @@ export function createNativeProvider(deps = {}) {
551
560
  logger?.info?.(
552
561
  `[native-review] Analyzing ${changedFiles.length} changed file(s)...`,
553
562
  );
554
- const results = await analyzeChangedFilesFn(changedFiles, {
563
+ const mi = scopeMaintainabilityFiles(changedFiles, {
564
+ cwd: PROJECT_ROOT,
565
+ resolveIgnoreGlobsFn,
566
+ });
567
+ if (mi.notice) logger?.info?.(mi.notice);
568
+ const results = await analyzeChangedFilesFn(mi.scored, {
555
569
  headRef,
556
570
  gitSpawnFn,
557
571
  });
@@ -21,7 +21,7 @@
21
21
  * the caller raises that to a thrown error so auto-merge is not enabled.
22
22
  *
23
23
  * Delegates the `runCodeReview` invocation to `runStoryReviewCore`
24
- * (exported from `story-close/phases/code-review.js`) so the close path
24
+ * (exported from `story-close/phases/review-core.js`) so the close path
25
25
  * shares a single invocation pattern (Story #3653). Review depth needs no
26
26
  * input here: it is derived from this Story's own diff inside `runCodeReview`
27
27
  * (Story #4542).