@humanbased/crosscheck 1.2.0-beta.80 → 1.2.0-beta.81

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 (53) hide show
  1. package/README.md +51 -5
  2. package/crosscheck.config.example.yml +39 -8
  3. package/dist/__tests__/board.test.js +11 -0
  4. package/dist/__tests__/board.test.js.map +1 -1
  5. package/dist/__tests__/onboard-preservation.test.js +54 -3
  6. package/dist/__tests__/onboard-preservation.test.js.map +1 -1
  7. package/dist/__tests__/review-strategy.test.js +202 -0
  8. package/dist/__tests__/review-strategy.test.js.map +1 -1
  9. package/dist/commands/onboard.d.ts +25 -3
  10. package/dist/commands/onboard.d.ts.map +1 -1
  11. package/dist/commands/onboard.js +151 -46
  12. package/dist/commands/onboard.js.map +1 -1
  13. package/dist/commands/run.d.ts.map +1 -1
  14. package/dist/commands/run.js +22 -4
  15. package/dist/commands/run.js.map +1 -1
  16. package/dist/commands/watch.d.ts.map +1 -1
  17. package/dist/commands/watch.js +38 -6
  18. package/dist/commands/watch.js.map +1 -1
  19. package/dist/config/review-strategy.json +8 -6
  20. package/dist/config/schema.d.ts +7 -5
  21. package/dist/config/schema.d.ts.map +1 -1
  22. package/dist/config/schema.js +29 -7
  23. package/dist/config/schema.js.map +1 -1
  24. package/dist/github/client.d.ts +16 -1
  25. package/dist/github/client.d.ts.map +1 -1
  26. package/dist/github/client.js +32 -1
  27. package/dist/github/client.js.map +1 -1
  28. package/dist/github/webhook.d.ts +4 -0
  29. package/dist/github/webhook.d.ts.map +1 -1
  30. package/dist/github/webhook.js.map +1 -1
  31. package/dist/lib/annotation.d.ts +7 -0
  32. package/dist/lib/annotation.d.ts.map +1 -1
  33. package/dist/lib/annotation.js +11 -1
  34. package/dist/lib/annotation.js.map +1 -1
  35. package/dist/lib/board.d.ts +3 -0
  36. package/dist/lib/board.d.ts.map +1 -1
  37. package/dist/lib/board.js +4 -2
  38. package/dist/lib/board.js.map +1 -1
  39. package/dist/lib/comment-bodies.d.ts +2 -0
  40. package/dist/lib/comment-bodies.d.ts.map +1 -1
  41. package/dist/lib/comment-bodies.js +5 -1
  42. package/dist/lib/comment-bodies.js.map +1 -1
  43. package/dist/lib/review-strategy.d.ts +34 -4
  44. package/dist/lib/review-strategy.d.ts.map +1 -1
  45. package/dist/lib/review-strategy.js +71 -24
  46. package/dist/lib/review-strategy.js.map +1 -1
  47. package/dist/lib/runner.d.ts +80 -0
  48. package/dist/lib/runner.d.ts.map +1 -1
  49. package/dist/lib/runner.js +289 -17
  50. package/dist/lib/runner.js.map +1 -1
  51. package/docs/dynamic-thoroughness.md +677 -225
  52. package/get-started.md +67 -6
  53. package/package.json +1 -1
@@ -1,286 +1,738 @@
1
- # Dynamic thoroughness
1
+ # Dynamic thoroughness — design & evidence
2
2
 
3
- **Strategy version 1.0.0 · updated 2026-08-07 · re-verify every 60 days.**
4
-
5
- This document is the analysis behind [`src/config/review-strategy.json`](../src/config/review-strategy.json).
6
- The JSON is the policy crosscheck executes; this file is why the policy says what it
7
- says. They are versioned together: a change to one without the other is drift, and
8
- `npm run verify:strategy` is the check that makes that drift loud.
3
+ | | |
4
+ |---|---|
5
+ | **Strategy version** | `1.1.0` [`src/config/review-strategy.json`](../src/config/review-strategy.json) |
6
+ | **Analysis run** | 2026-08-07T05:00:00Z |
7
+ | **Census window** | 2026-07-30 2026-08-07 |
8
+ | **Corpus** | 400 merged PRs · 199 crosscheck-engaged · 597 agent calls · 43 locally-logged runs |
9
+ | **Hand-labelled** | 40 BLOCK findings read against cited source |
10
+ | **Next review due** | 2026-10-06 (60-day interval) |
11
+ | **Re-verify** | `npm run verify:strategy` |
12
+
13
+ Status: **adopted and active**. This document is the evidence behind
14
+ [`review-strategy.json`](../src/config/review-strategy.json); the JSON is what
15
+ crosscheck reads at runtime. The runner classifies every PR from its cloned
16
+ working copy, folds the result into the quality config the reviewers receive,
17
+ and stamps the version, class, and tier into each annotation. When they disagree, the JSON wins and this document is
18
+ stale — the weekly `Review Strategy` workflow files an issue when either drifts.
19
+
20
+ **Enforced today:** the `Tier`, `Effort`, and `Steps` columns below all reach the
21
+ run. A class resolving to a null tier (class 1) skips the workflow outright; the
22
+ per-class step set narrows the configured pipeline (never widens it — a repo
23
+ pinned to review-only stays review-only); and rounds past the first escalate via
24
+ `escalate()`, raising effort where the model supports it and promoting the tier
25
+ where it does not.
26
+
27
+ Comments cite only what actually applied — no tier is named when the class
28
+ selects none, or when an explicit `vendors.*.model` outranks the strategy — so
29
+ the citation never claims more than the runtime does.
30
+
31
+ Every crosscheck comment cites the strategy version it ran under, so a review from
32
+ last month can be explained by the policy in force when it ran rather than the policy
33
+ in force today.
9
34
 
10
35
  ---
11
36
 
12
- ## 1. The problem
37
+ ## 0. The goal, and what it implies
13
38
 
14
- One PR is not like another. A lockfile bump and a rewrite of the session handler
15
- are both "a PR", and crosscheck used to review them identically one configured
16
- `quality.tier`, one model, one effort level, the same review→fix→recheck loop.
17
- That is wrong in both directions at once: it overpays on the lockfile and
18
- under-reviews the session handler.
39
+ The objective is **shipping quality code at a faster pace**. Code review is
40
+ instrumental to that, not an end in itself. So the metric that matters is not cost per
41
+ review, and not findings per review. It is:
19
42
 
20
- The failure that prompted this was quieter than either. `review-model-tiers.json`
21
- pinned `claude-opus-4-8` for the `thorough` tier and kept pinning it after
22
- `claude-opus-5` shipped at identical $5/$25 pricing with a higher coding score.
23
- Nothing broke. Nothing warned. Every `thorough` review just got worse than it had
24
- to be, for months. A model pin is a perishable asset, and nothing in the repo
25
- treated it as one.
43
+ > **Actioned-finding rate** the share of real defects that get fixed *inside the
44
+ > merge window*.
26
45
 
27
- So the policy has two jobs: choose per PR, and tell a human when the inputs it
28
- chose from have moved.
46
+ A finding that arrives after merge is worth nothing. A finding the author declines to
47
+ act on is worth nothing. A correct finding nobody reads is worth nothing. Everything
48
+ below is organized around that.
29
49
 
30
- ---
50
+ Measured against it, the current system has a specific and non-obvious problem:
51
+
52
+ | | Measured | Reading |
53
+ |---|---|---|
54
+ | Review lands before merge | **94%** | Delivery works. |
55
+ | Findings that are real defects | **~85%** (hand-labelled, n=39) | Quality works. Noise is not the bottleneck. |
56
+ | PRs reaching APPROVE | **38%** | Action does not happen. |
57
+ | Median gap, last review → merge | **4 minutes** | **The author was already merging.** |
31
58
 
32
- ## 2. Sources, and how to re-verify
59
+ The reviewer is fast and it is right, and 62% of its output is merged past anyway — with
60
+ 56% of those merges landing within five minutes of the review, or before it.
33
61
 
34
- `sources[]` in the JSON lists the pages the numbers came from, each with the
35
- strings that must still appear on it. `scripts/verify-review-strategy.mjs`
36
- fetches them and compares.
62
+ **The structural reason: crosscheck never publishes a merge-blocking signal.** It reads
63
+ commit statuses and check-runs (`client.ts:555`, `client.ts:684`) but creates neither.
64
+ Its entire output is a comment. Branch protection requires:
37
65
 
38
- | Source | What it fixes |
66
+ | Branch | Required checks |
39
67
  |---|---|
40
- | [Anthropic model catalog](https://platform.claude.com/docs/en/about-claude/models/overview) | Claude model IDs and availability |
41
- | [Anthropic effort parameter](https://platform.claude.com/docs/en/build-with-claude/effort) | The effort ladder, including `xhigh` |
42
- | [OpenAI model catalog](https://developers.openai.com/api/docs/models) | Codex model IDs |
43
-
44
- Benchmarks are recorded in `benchmarks` with their own `as_of` dates and caveats,
45
- because they age faster than prices:
46
-
47
- - **SWE-bench Verified** — resolving real GitHub issues in Python repos. A
48
- backend/general proxy only. Comparable within one leaderboard run; cross-run
49
- positions are indicative.
50
- - **Frontend Code Arena** — blind pairwise human preference on generated frontend
51
- output. Measures *generation*, not defect detection. Claude Opus 5 is absent
52
- from the retrieved leaderboard: a missing measurement, not a low score.
53
-
54
- Re-verification is not automatic and is not meant to be. Model choice is a
55
- judgement call backed by benchmarks that need reading. The weekly
56
- `Review Strategy` workflow fetches the sources and opens an issue when a check
57
- string disappears; a human then re-reads and edits. The job's job is to say *the
58
- inputs moved*, never to move the policy itself.
59
-
60
- Network failures are deliberately not drift. A fetch that never landed says
61
- nothing about the policy, so the verifier treats unreachable sources as warnings
62
- on `pull_request` and as errors only on `schedule`, where someone is looking.
68
+ | `staging` | `PR size, template, and reviewability`, `ci-gate` |
69
+ | `main` | + `release-guard`, 1 approving review |
63
70
 
64
- ---
71
+ Crosscheck appears in neither list, so GitHub merges the moment `ci-gate` goes green.
72
+ Nothing stops a merge at minute four. (`enforce_admins` is `false` on both branches, so
73
+ even a required check would be admin-bypassable.)
74
+
75
+ Two distinct failure modes follow, and they need different fixes:
76
+
77
+ 1. **The fast majority — no gate.** Merging before the review lands is undisciplined
78
+ practice, not a considered trade-off. No improvement to review quality, latency, or
79
+ phrasing changes it, because the review is not part of the merge decision at all.
80
+ Only a gate does.
81
+ 2. **The slow tail — no terminal state.** Findings never stop arriving (§3), so the
82
+ loop is an open-ended commitment. This is what makes the process intolerable *once
83
+ authors are forced to engage with it*.
65
84
 
66
- ## 3. The model catalog
67
-
68
- Prices are USD per million tokens.
69
-
70
- | Model | Vendor | In | Out | SWE-bench V | Frontend Elo | Effort levels |
71
- |---|---|---:|---:|---:|---:|---|
72
- | `claude-opus-5` | claude | 5 | 25 | 0.960 | — | low…max |
73
- | `claude-sonnet-5` | claude | 3 | 15 | 0.852 | — | low…max |
74
- | `claude-haiku-4-5-20251001` | claude | 1 | 5 | 0.733 | — | *(none)* |
75
- | `gpt-5.6-sol` | codex | 5 | 30 | — | 1618 | none…max |
76
- | `gpt-5.6-terra` | codex | 2 | 12 | — | — | none…max |
77
- | `gpt-5.6-luna` | codex | 0.2 | 1.2 | — | — | none…max |
78
- | `kimi-k3` | opencode *(planned)* | 3 | 15 | — | 1679 | *(none)* |
79
- | `glm-5.2` | opencode *(planned)* | 1.4 | 4.4 | — | 1587 | *(none)* |
80
- | `deepseek-v4-pro` | opencode *(planned)* | 0.435 | 0.87 | 0.806 | — | none, high, max |
81
- | `deepseek-v4-flash` | opencode *(planned)* | 0.14 | 0.28 | 0.790 | — | none, high, max |
82
-
83
- `banned_models` is a separate list from "not currently routed", because the
84
- reason matters. `claude-fable-5` is banned on cost: $50/MTok output is 2× Opus 5
85
- for a *lower* SWE-bench Verified score (95.0 vs 96.0). At the measured ~48k
86
- output tokens per review that is $2.40 a call with no capability justification.
87
- The verifier fails if any vendor tier or domain preference ever routes to it.
85
+ These compose in a specific order. **Gating without first bounding the loop would be
86
+ actively harmful** — it converts "merged past findings in 4 minutes" into "blocked for
87
+ 51 hours," which is the opposite of the goal. Bound the loop first, then gate, then
88
+ optimize latency.
88
89
 
89
90
  ---
90
91
 
91
- ## 4. Tiers and domains
92
+ ## 1. Methodology
92
93
 
93
- Tiers are the vendor-facing ladder:
94
+ | Input | What it gives | Confidence |
95
+ |---|---|---|
96
+ | **A.** Census of all 400 merged PRs | Population-scale cost and outcome | High |
97
+ | **B.** 40 BLOCK findings read against the cited code | Whether findings are real | High — hand-labelled |
98
+ | **C.** Local run logs (43 runs) | Tokens + wall-clock per step | High — measured |
99
+ | **D.** Vendor catalogs + live CLI probe | Price, effort, what actually runs | High — primary |
100
+ | **E.** SWE-bench, CR-Bench | Relative capability by task shape | Medium — see §5 |
94
101
 
95
- | Tier | Claude | Codex | OpenCode *(planned)* |
96
- |---|---|---|---|
97
- | `fast` | Haiku 4.5 | GPT-5.6 Luna | deepseek-v4-flash |
98
- | `balanced` | Sonnet 5 | GPT-5.6 Terra | glm-5.2 |
99
- | `thorough` | Opus 5 | GPT-5.6 Sol | kimi-k3 |
102
+ Two earlier approaches were abandoned, recorded here so they are not re-derived:
103
+
104
+ - **Sampling 30 PRs and fitting a complexity score to agent-call count.** Circular:
105
+ call count is set by whether the loop converged, which is a property of the reviewer
106
+ configuration, not the PR. The census replaces it.
107
+ - **`breadth = dirs × languages` as a complexity metric.** Scored a perfect AUC on the
108
+ 30-PR sample; the census falsifies it outright (§4).
100
109
 
101
- Capability, though, is domain-shaped rather than scalar. Opus 5 leads backend
102
- defect resolution; kimi-k3 leads the frontend arena. A single ladder cannot
103
- express that, so `domains` carries a preference list per tier. The domain is
104
- detected from changed-file extensions; `mixed` and `unknown` both fall back to
105
- `backend`, the conservative default — the backend list is `measured`, the
106
- frontend list is still a `hypothesis` pending a local A/B.
110
+ For the labelling pass, BLOCK findings were extracted from PRs that merged *without*
111
+ reaching APPROVE, deduplicated, sampled 40 (seeded; 20 P1-code / 12 P2-code / 8 docs),
112
+ and each was read against the file it cites at the SHA it was reviewed at.
107
113
 
108
114
  ---
109
115
 
110
- ## 5. PR classes
116
+ ## 2. Finding 1 — the findings are real
117
+
118
+ This was the decisive experiment, and it inverted the design.
119
+
120
+ Of 40 sampled findings: 1 unverifiable (file since deleted), **0 false**. Every
121
+ remaining claim was a true statement about the code it cited.
122
+
123
+ | Label | n | Meaning |
124
+ |---|---|---|
125
+ | Real, defect-grade | **33** | Genuine bug, security gap, or documented-convention violation |
126
+ | Real, severity-inflated | 6 | True, but should not have contributed to BLOCK |
127
+ | False | **0** | — |
128
+ | Unverifiable | 1 | Cited file no longer retrievable |
129
+
130
+ Representative confirmed defects, each verified against source:
131
+
132
+ - **#8** — `change_rule_mode` accepts an `expected_mode` parameter and never puts it in
133
+ the POST body. Dual-control bypass. Two lines apart in the same function.
134
+ - **#6** — `body.get("data")` sits *outside* the `try` that catches JSON errors; a
135
+ non-object response raises an uncaught `AttributeError`.
136
+ - **#10** — `_validated_gate` hard-rejects a gate response missing `profile`, so during
137
+ a normal rolling deploy every enroll returns 503 until both services are upgraded.
138
+ - **#13** — the promote payload sends three fields the receiving `extra="forbid"` model
139
+ no longer defines: every promotion and rollback 422s. Feature entirely broken.
140
+ - **#26** — `min_a2_shared_wrong` bounded at `_Bound(int, 0)` while its sibling and the
141
+ request contract require ≥ 1.
142
+ - **#4** — migration named `20260820000000`. `docs/architecture/database-conventions.md`
143
+ says verbatim: *"**Never hand-write a round-number timestamp**"*, and documents a
144
+ prior silent-skip incident. The reviewer cited the correct line range.
145
+
146
+ The reviews also demonstrate project-specific reasoning that a generic linter cannot
147
+ do: they read repo convention docs, cross-reference design ADRs, and trace call paths
148
+ across services.
149
+
150
+ **The six severity-inflated cases are the real defect in the output**, not the
151
+ findings themselves: a webhook-secret hardening that requires operator misconfiguration
152
+ to exploit (#2), a request for validation stricter than the contract claims (#3), and
153
+ screenshot binaries committed to a feature branch (#20) — a process violation, correctly
154
+ identified, that should never gate a merge.
155
+
156
+ > **Calibration note.** CR-Bench reports 3.6–5.1% precision for review agents (§5).
157
+ > This system measures ~85% defect-grade. The gap is real and worth naming: CR-Bench
158
+ > scores against a fixed ground-truth defect list, penalizing any finding outside it,
159
+ > whereas this labelling asked "is the claim true and worth fixing." Those measure
160
+ > different things. The operational conclusion stands regardless — **do not tune this
161
+ > system as though its output were noise.**
162
+
163
+ ---
111
164
 
112
- **Order is the routing logic first match wins.** The list is in
113
- `pr_classes`, and `risky` sits second, immediately after `generated`, so
114
- consequence-based promotion dominates every cheapening rule below it. A
115
- deletion-only PR that removes auth code, or a two-file change to a migration,
116
- must not fall through to `fast`.
165
+ ## 3. Finding 2the loop cannot converge, by construction
117
166
 
118
- | # | Class | Tier · effort | Steps |
167
+ Findings per review pass, across the census:
168
+
169
+ | Pass # | Passes | Mean findings | Total |
119
170
  |---|---|---|---|
120
- | 1 | `generated` — every file generated or vendored | *skip* | |
121
- | 2 | `risky` — security / data-critical | thorough · high | review, fix, recheck |
122
- | 3 | `deletion_only` | fast · medium | review |
123
- | 4 | `docs` | balanced · high | review |
124
- | 5 | `test_only` | fast · medium | review, fix |
125
- | 6 | `config_infra` | balanced · high | review, fix, recheck |
126
- | 7 | `trivial` | fast · medium | review, fix |
127
- | 8 | `standard` — fallthrough | balanced · medium | review, fix, recheck |
128
-
129
- Two classes drop the fix loop on purpose. `docs` is open-ended design
130
- negotiation that no automated fix loop terminates. `deletion_only` carries
131
- reference-breakage risk rather than defect density — a wrong deletion is
132
- reverted, not patched — so it gets a review focused on dangling imports, call
133
- sites, config keys, route registrations and stale doc pointers.
134
-
135
- The last entry must have an empty `match`: it is the fallthrough, and
136
- `resolveReviewStrategy` falls back to it when nothing matches. The verifier
137
- asserts this.
138
-
139
- ### Path patterns are anchored
140
-
141
- Class patterns match path *segments and extensions*, not substrings. An
142
- unanchored list is worse than no list:
143
-
144
- - `auth` matched `author.ts`, `authors.ts` and `oauth.ts`;
145
- - `migration` matched this repo's own `MIGRATION_AND_GROWTH.md`, promoting a
146
- pure-docs PR to `thorough` with a full fix/recheck loop;
147
- - `spec` matched `src/lib/pr-spec.ts` and `test` matched any `latest.ts`,
148
- routing ordinary source changes to `test_only` with a test-focused prompt.
149
-
150
- Over-promotion is safe but expensive; under-review is neither. Both are bugs.
151
-
152
- ### Size caps fail closed
153
-
154
- `src_churn_max` can only be evaluated when churn is attributable — the PR
155
- context carries whole-PR `additions`/`deletions`, not per-file counts, so the
156
- total is only meaningful when every changed file is source. When it is not, the
157
- cap fails **closed** (the churn reads as `Infinity` and the class does not
158
- match). Failing open cost us a real misroute: `['package.json',
159
- 'src/lib/runner.ts', 'src/lib/board.ts']` at +3000/−2000 satisfied
160
- `files_max: 3`, the unattributable churn read as `0`, and a 5,000-line change to
161
- the core runner was routed to `fast` with no recheck.
171
+ | 1 | 198 | 2.0 | 391 |
172
+ | 2 | 90 | 2.5 | 221 |
173
+ | 3 | 46 | 2.5 | 117 |
174
+ | 4 | 32 | **3.3** | 105 |
175
+ | 5 | 23 | **3.7** | 86 |
176
+ | 6+ | 94 | 2.3 | 216 |
162
177
 
163
- ---
178
+ **Later passes find more, not fewer.** And the findings do not repeat: across 684 raw
179
+ findings there are 676 distinct ones — a dedup ratio of **1.0**. Nothing recurs.
180
+
181
+ So the fixer *is* fixing (issues do not come back), and each pass mines a fresh seam.
182
+ Two sampled findings are regressions introduced by the *previous* round's fix:
164
183
 
165
- ## 6. Design decisions
184
+ - **#15 #19** — round N flags a dual-control TOCTOU; the fix adds
185
+ `expected_mode=current_mode`; round N+1 correctly observes that this now 409s every
186
+ legitimate retry.
187
+ - **#32 → #33** — round N flags two data streams missing from a legal retention table;
188
+ the fix adds them but describes the device ID as `raw`, contradicting the ADR that
189
+ requires `key_version:HMAC`. Round N+1 catches it.
166
190
 
167
- ### 6.1 Why a versioned policy file, not code
191
+ This is legitimate, valuable work. It is also **an unbounded process**: there is no
192
+ round at which the supply is exhausted, so "iterate until APPROVE" has no terminal
193
+ state. PR #1998 ran 12 recheck rounds — 26 agent calls, 4.6 hours — and merged unresolved.
168
194
 
169
- Every crosscheck comment cites `strategy=<version>`. A review that ran three
170
- months ago has to stay explicable by the policy in force when it ran, and that is
171
- only possible if the policy is a versioned artifact rather than a diff of
172
- `if` statements. It also puts the routing decision and the sentence explaining it
173
- in the same record, so the two cannot drift apart: `reason` is quoted verbatim
174
- into the PR comment, which is why the verifier fails any class missing one.
195
+ The remedy is therefore not a better fixer or a stronger recheck model. It is to
196
+ **stop treating APPROVE as the exit condition** and bound the process explicitly.
175
197
 
176
- `review-model-tiers.json` is retained for backwards compatibility and is
177
- superseded by this file.
198
+ ---
199
+
200
+ ## 4. Finding 3 — crosscheck is racing the merge, and the race is tight
178
201
 
179
- ### 6.2 Why capability is not a single number
202
+ Across the 122 PRs that merged unresolved, time from the **last** review comment to
203
+ merge:
180
204
 
181
- See §4. A tier ladder is a cost ladder that happens to correlate with capability
182
- inside one vendor. Across vendors and across domains it stops correlating, which
183
- is why `domains.*.preferred` exists and why each entry carries a `confidence`
184
- field. `measured` and `hypothesis` are load-bearing words here the frontend
185
- list is a hypothesis and is labelled as one.
205
+ | | Share |
206
+ |---|---|
207
+ | Review landed *after* the merge too late entirely | 12% |
208
+ | Merged within 5 minutes of the review | **44%** |
209
+ | Merged 5–60 minutes after | 25% |
210
+ | Merged > 1 hour after (saw it, chose to ship) | 18% |
211
+
212
+ Median: **4 minutes**. Only ~18% of unresolved merges look like a considered decision.
213
+
214
+ The elapsed-time hypothesis — that authors bail because loops run too long — is **true
215
+ in the tail and insufficient on its own**. Convergence is flat at 33–41% across every
216
+ span and round bucket, including the 108 single-round PRs that resolved in a median of
217
+ 11 minutes. Long loops do not predict merging unresolved; the behavior is uniform.
218
+
219
+ The uniformity is the tell. If authors were weighing findings, the rate would vary with
220
+ how painful the PR was. It does not vary at all — which is what an **absent gate** looks
221
+ like (§0). The fast majority is not a considered trade-off; it is merging that nothing
222
+ prevents. The tail is a separate problem: those PRs have the *highest* rate of reviews
223
+ landing after merge (20%) and a median span of **51 hours**, which is what makes the
224
+ loop intolerable once a gate forces engagement.
225
+
226
+ The window is genuinely tight. First review lands at a median of 6 minutes; merge at 11
227
+ minutes for single-round PRs and 36 overall. At p25, merge happens at 9 minutes against
228
+ a p25 review at 5. **For a large share of PRs the usable window is single-digit
229
+ minutes** — which makes time-to-first-review a first-class design constraint, not a
230
+ nice-to-have.
231
+
232
+ ### Static prediction is weak
233
+
234
+ **Static complexity does not predict cost.** Spearman vs agent-call count across all 199
235
+ engaged PRs: churn 0.513, files 0.428, churn/files 0.321. The distributions overlap
236
+ severely — the largest one-call PR was **101,118 lines**; the smallest 7+-call PR was
237
+ **103**; and the single most expensive PR in the census (#1998, 26 calls) changed
238
+ **2 files**.
239
+
240
+ Conclusion: static features can answer only *"is this trivially safe to start cheap?"*
241
+ They set a floor. They cannot identify the hard PRs in advance, so they must never set
242
+ the ceiling.
186
243
 
187
- ### 6.3 Why `thorough` moved to Opus 5
244
+ ---
188
245
 
189
- Identical $5/$25 pricing, higher SWE-bench Verified (0.960 vs the 4.8 pin it
190
- replaced). There is no trade here; the old pin was a silent capability loss, and
191
- it persisted only because nothing was watching. §2 exists so the next one does
192
- not persist.
246
+ ## 5. Finding 4 model selection is currently inert
193
247
 
194
- ### 6.4 Models with no effort ladder the OpenCode case
248
+ Three breaks in one path, all of which must be fixed before any tiering policy can have
249
+ an effect.
195
250
 
196
- Most open-weight models expose no effort parameter at all: `kimi-k3`, `glm-5.2`
197
- and `claude-haiku-4-5-20251001` all have `effort_levels: []`, and the deepseek
198
- models expose only `none`/`high`/`max`. An escalation step defined purely as
199
- "raise effort" silently no-ops on every one of them.
251
+ **a. The Claude override bypasses tiers.** `vendors.claude.model: sonnet` wins over the
252
+ tier table, so all 43 logged runs were `claude-sonnet-5` regardless of `quality.tier`.
200
253
 
201
- So the ladder declares `effort_fallback: "promote_tier"`: where effort cannot
202
- rise, the escalation degrades to a model step instead. `escalate()` implements
203
- this by clamping the round's target effort to what the model actually accepts and
204
- promoting a tier only when the clamped value is not an increase over the round
205
- before — otherwise a model whose ladder tops out below `xhigh` comes out of
206
- round 3 *weaker* than it left round 2.
254
+ **b. The Codex model is silently discarded.**
207
255
 
208
- The verifier enforces the invariant directly: if any model has no effort levels,
209
- `ladder.effort_fallback` must be set.
256
+ ```ts
257
+ export function resolveCodexModel(quality, vendor): string {
258
+ if (vendor.auth !== 'api-key') return 'default' // ← drops the configured model
259
+ ...
260
+ }
261
+ ```
210
262
 
211
- ### 6.5 Escalate on measured non-convergence, never on predicted complexity
263
+ The config sets `vendors.codex.model: gpt-5.6-sol` with `auth: subscription`, so it
264
+ never reaches the CLI. **148 of 199 engaged PRs and 482 of 597 calls (81%) ran on
265
+ `model=default`.** A live probe contradicts the guard's rationale: `codex -m/--model`
266
+ is a top-level flag and the CLI accepted it under subscription auth.
212
267
 
213
- The ladder is bounded 3 rounds, 5 blocking findings, 60 wall-clock minutes —
214
- and each round narrows scope rather than widening it. Round 2 rechecks the delta
215
- plus the open findings, not the whole PR again. The model never weakens across
216
- rounds; only its scope shrinks. Round 4 is a handoff to a human with a digest,
217
- not a fourth attempt.
268
+ **c. The installed CLI is too old for the configured model.** The same probe returned
269
+ `The 'gpt-5.6-terra' model requires a newer version of Codex.` Installed
270
+ `codex-cli 0.141.0`; npm latest `0.147.0`. That error text is exactly what
271
+ `isVendorUnavailableError` matches, so smart-switch has been silently absorbing it.
272
+
273
+ Outcomes by first model:
274
+
275
+ | First model | PRs | Calls | Merged without APPROVE |
276
+ |---|---|---|---|
277
+ | `default` | 148 | 482 | **74%** |
278
+ | `claude-opus-5` | 22 | 60 | **5%** |
279
+ | `claude-sonnet-5` | 15 | 19 | 27% |
280
+ | `gpt-5.6-luna` | 6 | 22 | 17% |
281
+ | `gpt-5.6-sol` | 7 | 14 | 86% |
282
+
283
+ > **Confound, stated plainly.** Assignment was not random — it followed origin routing
284
+ > and smart-switch state. `gpt-5.6-sol`'s 86% is the clearest artifact: n=7, used as an
285
+ > *escalation target* on already-stuck PRs. The same bias inflates part of the `default`
286
+ > column. But `default` spans 148 PRs, and 74%-vs-5% is wider than selection alone
287
+ > plausibly explains.
288
+
289
+ ### Price and capability
290
+
291
+ | Model | In | Out | Cost @ 48k out | Effort |
292
+ |---|---|---|---|---|
293
+ | `claude-fable-5` | $10 | $50 | $2.40 | low→max |
294
+ | `claude-opus-5` | $5 | $25 | $1.20 | low→max |
295
+ | `claude-sonnet-5` | $3 | $15 | $0.72 | low→max |
296
+ | `claude-haiku-4-5` | $1 | $5 | $0.24 | **none** |
297
+ | `gpt-5.6-sol` | $5 | $30 | $1.44 | none→max |
298
+ | `gpt-5.6-terra` | $2 | $12 | $0.58 | none→max |
299
+ | `gpt-5.6-luna` | $0.20 | $1.20 | **$0.058** | none→max |
300
+
301
+ SWE-bench Verified (one leaderboard, refreshed 2026-08-06, consistent within the Claude
302
+ family): Opus 5 **96.0%**, Fable 5 95.0%, Sonnet 5 **85.2%**, Haiku 4.5 **73.3%**.
303
+ Cross-family figures (Sol ~96.2%, Luna ~93%) come from a *different* aggregator and are
304
+ directional only.
305
+
306
+ Measured step costs (43 runs): review 643 s / 48.0k output tokens; **recheck 663 s /
307
+ 48.9k — the same as a full review**; fix 409 s; conflict-resolve **37 s**.
308
+
309
+ Anthropic's effort guidance supports varying effort per call, with the constraint that
310
+ it must be constant *within* a cached conversation. Note Haiku 4.5 has no effort
311
+ parameter at all.
218
312
 
219
- ### 6.6 Cross-vendor vs single-vendor
313
+ ---
220
314
 
221
- Cross-vendor buys an independent second perspective: in the census a vendor
222
- switch broke a stalled loop twice (PR #2057, #2060) after same-vendor escalation
223
- had failed for three to six rounds. Its round-3 lever is *switch vendor and raise
224
- effort*, and it caps at 3 rounds. Single-vendor buys one CLI to authenticate,
225
- version and monitor, with no routing logic; its round-3 lever is *raise effort,
226
- then promote one tier*, because no fresh-eyes option exists, and it caps at 2.
315
+ ## 6. Design
227
316
 
228
- ### 6.7 Conflict resolution is not a review
317
+ Everything follows from §0: maximize actioned findings inside the merge window.
229
318
 
230
- `conflict_resolve` runs at `fast`/`low` regardless of class. Resolving conflict
231
- markers is mechanical text surgery bounded by the markers themselves — measured
232
- at 37s against ~643s for a review. Spending review-grade capability on it buys
233
- nothing.
319
+ ### 6.1 Front-load capability into pass 1 against a latency budget
234
320
 
235
- ### 6.8 What classification may and may not conclude
321
+ Pass 1 is the only pass reliably read before merge, and later passes find *more*
322
+ defects, not fewer (§3). So capability spent on pass 1 converts to action; capability
323
+ spent on pass 7 mostly does not.
236
324
 
237
- Two rules constrain the classifier, and they are the reason it stays a small
238
- static function instead of growing into a difficulty predictor:
325
+ But capability costs time, and §4 shows the window is often single-digit minutes. **This
326
+ tension is the actual case for dynamic thoroughness** — and it is the one justification
327
+ that survives the census. It is not cost. A review that is 10 points better on SWE-bench
328
+ and arrives after the merge scores zero.
239
329
 
240
- 1. **It may set a floor, or promote on consequence.** A security path is
241
- reviewed thoroughly because a miss there is expensive. That is a statement
242
- about cost of failure, which the file list genuinely supports.
243
- 2. **It may not predict that a PR will be hard.** Static features — file count,
244
- line count, extension mix — do not support that inference. Difficulty is
245
- discovered by reviewing, not before it. So there is no "this looks tricky,
246
- use the big model" rule, and there never should be.
330
+ So the tier is chosen against a **latency budget derived from expected merge speed**,
331
+ not from predicted difficulty:
247
332
 
248
- Escalation is how difficulty gets handled, and it is driven by *measured*
249
- non-convergence: the review came back BLOCK, the fix did not resolve it, so the
250
- next round escalates. That is evidence. A file-count heuristic is not.
333
+ | PR class | Expected merge | Budget | Tier |
334
+ |---|---|---|---|
335
+ | Trivial (§6.5) | ~9–11 min | **< 3 min** | `fast` — latency-driven, not cost-driven |
336
+ | Standard | ~36 min | < 10 min | `balanced` + `xhigh` |
337
+ | Risky / `risk:T3` | hours | < 20 min | `thorough` |
338
+
339
+ This is why the `fast` tier earns its place: on a PR that merges in nine minutes, Haiku
340
+ at 73.3% delivered in two minutes beats Opus at 96.0% delivered in twelve. The 12% of
341
+ reviews that currently land after merge are pure waste at any capability level.
342
+
343
+ - **Pin the models** (§5). Drop the `auth !== 'api-key'` guard; honor a configured model
344
+ under either auth, as `resolveClaudeModel` already does. Require `codex >= 0.147.0`
345
+ when a `gpt-5.6-*` model is set, and fail loudly at startup.
346
+ - **Refresh `review-model-tiers.json`.** `thorough` currently pins `claude-opus-4-8`,
347
+ now legacy; `claude-opus-5` supersedes it at the identical $5/$25 and scores 96.0% vs
348
+ 88.6%. Free upgrade, ship independently.
349
+ - **Raise the pass-1 floor to `balanced`, and use `xhigh` effort.** Effort is the cheap
350
+ lever: Sonnet 5 at `xhigh` costs the same per token as at `medium` and merely spends
351
+ more of them, where Sonnet→Opus is a flat 1.67× on every token.
352
+
353
+ ```json
354
+ {
355
+ "claude": { "fast": "claude-haiku-4-5-20251001",
356
+ "balanced": "claude-sonnet-5",
357
+ "thorough": "claude-opus-5" },
358
+ "codex_api": { "fast": "gpt-5.6-luna",
359
+ "balanced": "gpt-5.6-terra",
360
+ "thorough": "gpt-5.6-sol" }
361
+ }
362
+ ```
363
+
364
+ No `frontier` tier for `claude-fable-5`: 2× Opus 5's output price for a *lower*
365
+ SWE-bench score.
366
+
367
+ ### 6.2 Make the terminal state visible — the closing contract
368
+
369
+ The exit condition changes from "APPROVE" to "**the blocking set is closed**," and —
370
+ critically — **pass 1 must say so in the comment.**
371
+
372
+ Authors merge past findings because entering the loop is an open-ended commitment (§0).
373
+ Bounding the set internally does not fix that; the author cannot see the bound. The
374
+ first review has to carry the promise explicitly:
375
+
376
+ > **3 blocking issues. This is the complete blocking set — later rounds will not add to
377
+ > it.** 7 further observations are advisory and do not gate this PR.
378
+
379
+ That sentence is the whole intervention. It converts an open-ended commitment into a
380
+ bounded one, which is the difference between a decision an author can make in the merge
381
+ window and one they resolve by merging.
382
+
383
+ It also has to be *true*, which constrains the implementation:
384
+
385
+ - Pass 1 emits at most N blocking findings (default 5), ranked; everything else is
386
+ advisory and explicitly non-gating.
387
+ - **Advisory findings never trigger a fix round**, and later rounds may not promote a
388
+ finding into the blocking set. A new defect discovered in round 3 is reported as
389
+ advisory — otherwise the promise breaks and the learned disengagement returns.
390
+ - **Hard cap: 3 fix/recheck rounds, and a wall-clock budget.** Whichever binds first
391
+ ends the loop with a digest and a human assignment. Given §3 the process has no
392
+ natural terminal state, and given §4 the round count is the wrong unit — 7+ round PRs
393
+ span a median of 51 hours.
394
+
395
+ The one real cost: capping the blocking set means some genuine defects ship as advisory.
396
+ Given ~85% precision that is a real loss, and it is the right trade — a bounded set that
397
+ gets acted on beats an unbounded one that gets merged past. The advisory list preserves
398
+ the finding for the author and for follow-up.
399
+
400
+ ### 6.3 Tighten severity so BLOCK means something
401
+
402
+ Six of 40 sampled findings were true but should not have gated a merge. BLOCK should
403
+ require a concrete failure path in the changed code: data loss, security boundary,
404
+ broken API contract, or a correctness bug with a reachable trigger. Explicitly **not**
405
+ BLOCK-grade:
406
+
407
+ - repository process and hygiene conventions (#20 — screenshot binaries)
408
+ - hardening that requires operator misconfiguration to exploit (#2)
409
+ - requests for validation beyond what the code's stated contract claims (#3)
410
+
411
+ These stay in the review as advisory. This costs nothing and raises the signal on the
412
+ verdict that gates the pipeline.
413
+
414
+ ### 6.4 The ladder — narrow the scope, hold the model, escalate the effort
415
+
416
+ Start strong but not maxed (54% of engaged PRs are one-and-done — maxing every pass 1
417
+ wastes budget on PRs that need one), then escalate on measured non-convergence.
418
+
419
+ | Round | Scope | Model | Effort | Rationale |
420
+ |---|---|---|---|---|
421
+ | **1 · review** | Whole PR | `balanced`, or `thorough` for risk classes (§6.5) | `medium` | Only pass reliably read. Sets the blocking set. |
422
+ | **· fix** | The N blocking items | **same as review** | `medium` | See below — cheap fixes cost a whole round. |
423
+ | **2 · recheck** | Delta + open items only | **same as review** | `high` | Narrower task, not a weaker judge. |
424
+ | **3 · recheck** | Delta + open items | same | `xhigh` | Non-convergence *is* the difficulty signal. |
425
+ | **4+** | — | — | — | Stop. Digest + human (§6.2). |
426
+
427
+ **Should review get cheaper over rounds? Yes — but by narrowing scope, never by
428
+ weakening the model.** The two levers look similar on a cost report and are opposite in
429
+ effect:
430
+
431
+ - *Scope* legitimately shrinks. Round 1 reads the whole PR; round N only has to check N
432
+ known items against the delta. Today recheck costs the same as a full review (663 s
433
+ vs 643 s, 48.9k vs 48.0k tokens) **because it re-reviews everything** — that is
434
+ waste, and scoping it is a real saving that the closing contract (§6.2) already
435
+ implies, since later rounds may not add blocking findings anyway.
436
+ - *Model* must not weaken. CR-Bench shows weak models degrade fastest under iteration —
437
+ GPT-5-mini's SNR fell to 0.91, below 1, more noise than signal. Recheck is the call
438
+ that decides whether to spend another round; a weak judge there is how loops become
439
+ unbounded.
440
+
441
+ So the per-round cost curve falls because the **input shrinks**, while capability holds
442
+ and effort rises. That is the opposite of the intuitive "reviews are expensive, make
443
+ rechecks cheap," which optimizes the one call where cheapness is most damaging.
444
+
445
+ **On making `fix` cheaper:** it is the safest step to cheapen — generation against an
446
+ explicit list is the task models are strongest at (SWE-bench-shaped), unlike detection.
447
+ But the census argues for restraint. Two of 40 sampled findings are regressions
448
+ *introduced by a previous round's fix* (#15→#19, #32→#33), and each cost a full extra
449
+ round at ~10.7 minutes. Against that, a stronger fixer costs ~1.67× on a single call.
450
+ **Hold the model, drop effort one notch** — cheaper tokens without buying new defects.
451
+ A tier drop is defensible only for mechanical fix steps on trivial PRs.
452
+
453
+ ### 6.5 The strategy table
454
+
455
+ Encoded in [`src/config/review-strategy.json`](../src/config/review-strategy.json), which
456
+ follows the pattern `review-model-tiers.json` already established: an `updated` date,
457
+ `sources` with verifiable `checks` strings, and a `review_interval_days` so drift is
458
+ detectable rather than silent (§6.6).
459
+
460
+ **Vendor tiers.** Cost is per 48k-output call, the census median.
461
+
462
+ | Tier | Claude | $/call | SWE-b | Codex | $/call | Effort |
463
+ |---|---|---|---|---|---|---|
464
+ | `fast` | `claude-haiku-4-5` | $0.240 | 73.3% | `gpt-5.6-luna` | **$0.058** | Claude: **none** · Codex: all |
465
+ | `balanced` | `claude-sonnet-5` | $0.720 | 85.2% | `gpt-5.6-terra` | $0.576 | both: all |
466
+ | `thorough` | `claude-opus-5` | $1.200 | 96.0% | `gpt-5.6-sol` | $1.440 | both: all |
467
+
468
+ *Claude strengths:* highest measured capability at `thorough`, most recent knowledge
469
+ cutoff (May 2026). *Weakness:* the `fast` tier has no effort parameter and only a 200k
470
+ context. *Codex strengths:* `fast` is 4× cheaper than Haiku on output, effort available
471
+ at every tier, 1.05M context throughout. *Weakness:* cross-family benchmarks are not
472
+ comparable — validate locally before assuming tier parity.
473
+
474
+ **`claude-fable-5` is banned from code review** (`banned_models` in the strategy file,
475
+ enforced by `verify:strategy`). At $50/MTok output it is 2× `claude-opus-5` for a
476
+ *lower* SWE-bench Verified score — $2.40 per call at the measured 48k output tokens,
477
+ with no capability justification at any tier.
478
+
479
+ ### The second axis: domain
480
+
481
+ Capability is domain-shaped, not scalar, and a single tier ladder cannot express that.
482
+ SWE-bench Verified resolves issues in *Python* repositories; in this census **42% of
483
+ reviewed PRs are frontend-dominant and 55% touch frontend at all**. On the domain that
484
+ carries most of the workload the ordering inverts:
485
+
486
+ | Frontend Code Arena (Elo, blind pairwise) | | SWE-bench Verified | |
487
+ |---|---|---|---|
488
+ | **Kimi K3** *(open)* | **1679** | **Claude Opus 5** | **96.0** |
489
+ | Claude Fable 5 | 1631 | Claude Sonnet 5 | 85.2 |
490
+ | GPT-5.6 Sol | 1618 | DeepSeek V4 Pro *(open)* | 80.6 |
491
+ | GLM-5.2 *(open)* | 1587 | DeepSeek V4 Flash *(open)* | 79.0 |
492
+ | Claude Opus 4.8 | 1562 | GLM-5 *(open)* | 77.8 |
493
+ | Grok-4.5 | 1558 | Claude Haiku 4.5 | 73.3 |
494
+
495
+ The most capable frontend model available is open-weight, and it is a different model
496
+ from the backend leader. Hence `domains` in the strategy file: `frontend` prefers
497
+ `kimi-k3` → `gpt-5.6-sol` → `claude-opus-5` at `thorough`; `backend` prefers
498
+ `claude-opus-5` → `gpt-5.6-sol`. `mixed` and `unknown` fall back to `backend`.
499
+
500
+ > **Confidence: hypothesis, marked as such in the JSON.** Arena measures blind human
501
+ > preference on *generated* frontend output — a generation task, where review is
502
+ > detection. The same asymmetry that limits SWE-bench applies. And **Claude Opus 5 is
503
+ > absent from the retrieved frontend leaderboard** (only Opus 4.8 at 1562) — a missing
504
+ > measurement, not a low score. The backend row is `measured`; the frontend row needs a
505
+ > local A/B before it should be trusted over the default.
506
+
507
+ **PR classes.** First match wins, so ordering is the routing logic.
508
+
509
+ | # | Class | Detection | Tier | Effort | Steps |
510
+ |---|---|---|---|---|---|
511
+ | 1 | Generated / vendored only | every file is a lockfile, build output, or generated stub | — | — | **skip** |
512
+ | 2 | **Security / data-critical** | risky path, `risk:T3`, or hotfix→default branch | `thorough` | high | review, fix, recheck |
513
+ | 3 | Deletion-only | ≤ 5 additions with ≥ 20 deletions | `fast` | medium | review |
514
+ | 4 | Documentation / specification | ≥ 50% `.md`/`.rst`/`.adoc` | `balanced` | high | review |
515
+ | 5 | Test-only | every file is a test or fixture | `fast` | medium | review, fix |
516
+ | 6 | Config / infrastructure | ≥ 50% config, no source files | `balanced` | high | review, fix, recheck |
517
+ | 7 | Trivial | ≤ 3 files and ≤ 150 source churn | `fast` | medium | review, fix |
518
+ | 8 | Standard code | fallthrough | `balanced` | medium | review, fix, recheck |
519
+
520
+ Three classes carry reasoning that is not obvious from the row:
521
+
522
+ - **Deletion-only** is not "small, therefore cheap." Its risk is *reference breakage* —
523
+ dangling imports, call sites, route registrations, docs pointing at removed symbols —
524
+ which is a narrow lookup task, not defect detection. And it gets no fix loop: a wrong
525
+ deletion is reverted, not patched.
526
+ - **Docs is review-only.** Census: docs PRs converge worst (mean 4.1 calls vs 3.0), and
527
+ #1998 — the most expensive PR in the census at 26 calls — was two markdown files. The
528
+ findings were real, but they were *design negotiation*, which no automated loop
529
+ terminates. Removing the fix/recheck loop for this class is the single largest tail
530
+ saving available.
531
+ - **Security is the only promotion rule**, applied because a missed defect there is
532
+ expensive — *not* because those PRs are predicted to be hard. §4 shows static features
533
+ cannot predict difficulty.
534
+
535
+ **Mode.**
536
+
537
+ | | Cross-vendor | Single-vendor |
538
+ |---|---|---|
539
+ | Strength | Independent second perspective | One CLI to auth, version, monitor |
540
+ | Round-3 lever | **switch vendor + raise effort** | raise effort, then promote one tier |
541
+ | Cost | both CLIs must stay current | loses the deadlock-breaker |
542
+ | `max_rounds` | 3 | **2** |
543
+
544
+ Cross-vendor's specific value is empirical: a vendor switch broke a stalled loop twice
545
+ in the census (#2057, #2060) *after* same-vendor escalation had failed for 3–6 rounds.
546
+ Single-vendor has no equivalent move, so it should cap rounds more aggressively rather
547
+ than grind.
548
+
549
+ ### 6.6 Citing the strategy in every review
550
+
551
+ A review is only auditable if it says what policy produced it. Every crosscheck comment
552
+ carries the strategy version, the PR class that was matched, and the tier that class
553
+ selected — in both the machine annotation and the human-readable line.
554
+
555
+ ```
556
+ <!-- crosscheck: origin=claude reviewer=claude model=claude-opus-5 type=review
557
+ round=1 verdict=BLOCK strategy=1.1.0 class=risky tier=thorough
558
+ service=crosscheck sha=abc123 -->
559
+ ```
560
+
561
+ > _Reviewed with [Claude Code](https://claude.ai/code) via [Crosscheck](https://github.com/humanbased-ai/crosscheck)
562
+ > with Opus 5 (high) — thorough tier · touches a security or data-critical path, where a
563
+ > missed defect is expensive · strategy v1.1.0_
564
+
565
+ The rationale text is not written per review — it is the matched class's `reason` field,
566
+ so the explanation and the routing decision cannot drift apart. `strategy`, `class`, and
567
+ `tier` are **additive** annotation fields: per the annotation contract in `CLAUDE.md`,
568
+ parsers already tolerate unknown fields, so this is a minor version bump, not a breaking
569
+ change.
570
+
571
+ This is what makes a past review explicable. A BLOCK from six weeks ago was produced by
572
+ whatever policy was current then; without the version stamp, re-reading it under today's
573
+ policy is a guess.
574
+
575
+ ### 6.7 Keeping the table current
576
+
577
+ The table is a perishable asset — `review-model-tiers.json` already went stale, pinning
578
+ `thorough` to `claude-opus-4-8` after `claude-opus-5` superseded it at the same price
579
+ and +7.4 points. That is a free capability loss, and it happened silently.
580
+
581
+ Staleness is now loud, by machine:
582
+
583
+ 1. **`npm run verify:strategy`** ([`scripts/verify-review-strategy.mjs`](../scripts/verify-review-strategy.mjs))
584
+ checks three things — internal consistency (every routed model exists; no banned
585
+ model is routed; every class carries a citable reason), freshness against
586
+ `review_interval_days`, and source drift (each `sources[].checks` string still
587
+ appears on the live page).
588
+ 2. **The `Review Strategy` workflow** ([`.github/workflows/review-strategy.yml`](../.github/workflows/review-strategy.yml))
589
+ runs it on every PR touching the strategy, and weekly on a schedule. A scheduled
590
+ failure opens a `strategy-drift` issue with the report and the remediation steps —
591
+ it deliberately does *not* auto-edit the file, because model choice is a judgement
592
+ backed by benchmarks a human has to read.
593
+ 3. **Fail loudly on model rejection.** The `gpt-5.6-terra` 400 (`requires a newer
594
+ version of Codex`) was silently absorbed by smart-switch, so the operator never
595
+ learned their configured model was dead. Version and model errors should surface as
596
+ configuration errors, not degrade quietly.
597
+ 4. **`min_cli_version` per vendor**, checked at startup when a model needs it.
598
+
599
+ ### 6.8 What classification may and may not decide
600
+
601
+ Every class in §6.5 is computed from the file list alone — no model call, no cost, and
602
+ available before the first review starts.
603
+
604
+ The hard constraint is what classification is *allowed* to conclude. It may:
605
+
606
+ - set the **floor** (`trivial`, `deletion_only`, `test_only` → `fast`)
607
+ - **promote** on consequence, not on predicted difficulty (`risky` → `thorough`)
608
+ - choose the **step set** (`docs` → review-only; `generated` → skip)
609
+
610
+ It may **not** predict that a PR will be hard and pre-emptively assign `thorough` for
611
+ that reason. §4 shows static features cannot support that inference: churn correlates
612
+ only 0.513 with realized cost, the largest one-call PR was 101,118 lines, and the most
613
+ expensive PR in the census changed 2 files. Difficulty is discovered by reviewing, and
614
+ that is what the ladder in §6.4 is for.
615
+
616
+ This is the discipline that keeps the table honest as it grows: new classes may adjust
617
+ the floor or the step set, but escalation stays evidence-driven.
251
618
 
252
619
  ---
253
620
 
254
- ## 7. Runtime status
621
+ ## 7. Expected effect
622
+
623
+ | Change | Mechanism | Effect on the goal |
624
+ |---|---|---|
625
+ | Bounded set + closing contract (§6.2) | Loop gets a terminal state | Prerequisite for everything else |
626
+ | **Docs → review-only** (§6.5) | Removes the loop from the worst-converging class | Largest single tail saving |
627
+ | Pin models (§5, §6.1) | 148 PRs leave `default` | Unresolved 74% → plausibly ~25% |
628
+ | Cap rounds + wall-clock (§6.2) | 18 PRs truncated to ≤7 calls | **139 of 597 calls removed (-23%)**, verified |
629
+ | Latency budget (§6.1) | Review lands inside the merge window | Recovers the 12% arriving after merge |
630
+ | Severity discipline (§6.3) | ~15% fewer spurious BLOCKs | BLOCK becomes actionable |
631
+ | Skip generated / deletion-only (§6.5) | Removes calls with no defect surface | Pure saving, no quality cost |
632
+ | Publish a check run | Lets a team gate if they choose | Enables adoption; team's decision, not ours |
255
633
 
256
- `runtime_status: "defined_not_wired"`. This file and the JSON are the adopted
257
- policy, but no runtime code reads `resolveReviewStrategy()` yet
258
- `resolveClaudeModel` / `resolveCodexModel` still apply the single configured
259
- `quality.tier`. Verification, documentation and the citation contract are in
260
- place ahead of the resolver so the policy is settled before it is enforced.
634
+ **The gate is what converts every other item here from advisory to load-bearing.** Today
635
+ review latency and review quality are both free variables nothing downstream consumes
636
+ them. Once `crosscheck/review` is a required check, review latency becomes time-to-merge
637
+ directly, and *that* is what makes dynamic thoroughness a velocity lever rather than a
638
+ cost optimization.
261
639
 
262
- Consequently:
640
+ Cost moves in both directions — up on pass 1, down hard on the tail — and nets roughly
641
+ flat to modestly lower. **That is the correct trade for this goal.** The call-count
642
+ reduction is the firm number; it follows from the round cap alone.
263
643
 
264
- - `quality.mode` stays **optional**. Unset means `fixed` the legacy behaviour
265
- and it will stay that way until the resolver lands, so nothing silently changes
266
- tier on upgrade.
267
- - `citation.annotation_fields` (`strategy`, `class`, `tier`) is a declared
268
- contract, not yet emitted by `src/lib/annotation.ts`.
644
+ The measurable success criterion is **actioned-finding rate**, not cost: today 38% of
645
+ engaged PRs reach a closed state. Everything above should be judged on whether it moves
646
+ that number.
269
647
 
270
- Wiring is the next change, and it is a separate one. It needs the PR's changed-file
271
- list threaded to the reviewers, the `strategy` argument passed at each
272
- `resolve*Model` call site, and the annotation fields emitted — none of which
273
- belongs in the PR that settles the policy.
648
+ Throughput: #1998 goes from 4.6 hours of reviewer wall-clock, merged unresolved, to
649
+ ~1.2 hours with a human handoff carrying a bounded list.
650
+
651
+ Two levers not modelled: `gpt-5.6-luna` at $0.058/call is 4× cheaper than Haiku on
652
+ output and reportedly much stronger — but that number is cross-family, so it needs a
653
+ local A/B, not a config change. And prompt caching cuts cached input 90%.
274
654
 
275
655
  ---
276
656
 
277
- ## 8. Cadence
657
+ ## 8. Rollout
658
+
659
+ Scoped to the tool. Whether a team gates on the result, and how they merge, is theirs to
660
+ decide — crosscheck's job is to be correct, bounded, and current by default.
661
+
662
+ **Silent-failure fixes** — pure defects, independent of every open design question:
663
+
664
+ 1. **`resolveCodexModel` discards the configured model** under subscription auth (§5).
665
+ Verified: the CLI accepts `--model` under either auth. 81% of census calls ran on an
666
+ unpinned `default` because of this one guard.
667
+ 2. **Model/version rejection is absorbed by smart-switch.** The `gpt-5.6-terra` 400 was
668
+ swallowed, so the operator never learned their configured model was dead. Surface
669
+ configuration errors as configuration errors; add `min_cli_version` checks at startup.
670
+ 3. **Audit the round cap.** `workflow.yml` sets `max_rounds: 3`; #1998 reached round 12.
671
+ `exceedsMaxRounds` (`runner.ts:184`) exempts a review step coerced to recheck — a
672
+ plausible path, not yet confirmed. No bound holds until this does.
673
+ 4. **Refresh the tier table** — `claude-opus-4-8` → `claude-opus-5`, same price, +7.4pts.
674
+
675
+ **Behavior** — needs the fixes above to be measurable:
676
+
677
+ 5. **Bound and rank the finding set; state the closing contract** (§6.2); **tighten
678
+ BLOCK** (§6.3). Prompt-level only, reversible in one commit.
679
+ 6. **Land the strategy table** (§6.5) in shadow mode — log the class and tier beside the
680
+ realized outcome, change nothing. Start with the three highest-confidence classes:
681
+ `generated` (skip), `deletion_only`, and `docs` (review-only).
682
+ 7. **Measure per-tier latency**, then enable the latency budget (§6.1). The fast-lane
683
+ argument depends on numbers not yet measured.
684
+ 8. **Enable the ladder** (§6.4) last — highest impact, easiest to get wrong.
685
+
686
+ **New capability** — makes crosscheck adoptable where review must be load-bearing:
687
+
688
+ 9. **Publish a `crosscheck/review` check run** — `pending` while reviewing, `success`
689
+ when the blocking set is closed or empty, `failure` when it is not. `client.ts` reads
690
+ check-runs today but creates none, so a team *cannot* gate on crosscheck even if they
691
+ want to. Ship it off by default and let teams opt in.
692
+
693
+ ```yaml
694
+ quality:
695
+ tier: balanced
696
+ mode: smart # already in schema.ts; currently inert
697
+ smart:
698
+ shadow: false
699
+ max_blocking: 5 # above this → advisory, never gating
700
+ max_rounds: 3 # hard stop, then hand to a human
701
+ max_wall_clock_min: 60 # whichever binds first ends the loop
702
+ ```
703
+
704
+ ### Open questions
705
+
706
+ - **What block rate would a required check actually produce?** Today 62% of engaged PRs
707
+ end non-APPROVE. Gating naively would block roughly that share — untenable. Step 3's
708
+ unrequired rollout measures the real number after §6.2–6.3 land, and that number
709
+ decides whether gating is viable at all.
710
+ - **Does the closing contract change behavior on its own?** It is the least-proven part
711
+ of the design, and it only matters once a gate forces engagement. Cheapest test: ship
712
+ the bounded-set wording for two weeks and watch the actioned-finding rate.
713
+ - **Should the gate apply to every PR?** A required check on trivial PRs that merge in
714
+ nine minutes may cost more velocity than the defects it catches. Gating only the
715
+ risky-path and `risk:T3` classes (§6.5) is the conservative first cut.
716
+ - **What is the real time-to-first-review at each tier?** The latency budget in §6.1 is
717
+ built on one measured configuration (Sonnet 5, 643 s median). Haiku and Luna
718
+ latencies on real diffs are unmeasured, and the whole fast-lane argument depends on
719
+ them. Measure before committing thresholds.
720
+ - **Does `fast` hold on trivial PRs?** All one-call PRs ran Sonnet 5. Whether Haiku 4.5
721
+ (73.3%) reaches the same verdicts is untested. Replay those diffs at both tiers.
722
+ - **Is `default` causal or a proxy for routing?** Shadow mode with a pinned model on
723
+ unchanged routing settles it.
724
+ - **Should docs PRs enter the fix/recheck loop at all?** They converge worst (#1998).
725
+ Their findings are real but constitute design negotiation. A review-only workflow may
726
+ fit them better.
727
+ - **Should coverage widen?** 201 of 400 merged PRs saw no review at all — an author-scope
728
+ gate. Out of scope here, but it dominates any projection.
729
+
730
+ ---
278
731
 
279
- `review_interval_days: 60`. The verifier warns past 45 days and fails past 60.
280
- The `Review Strategy` workflow runs Mondays at 09:15 UTC — fifteen minutes after
281
- `model-tier-sources`, so the two do not race the same provider docs — and on any
282
- PR touching the strategy, the verifier or this document.
732
+ ## Sources
283
733
 
284
- To refresh: re-read the `sources` and `benchmarks` pages, update prices, scores
285
- and tier assignments in `src/config/review-strategy.json`, refresh the analysis
286
- here, bump `version` and `updated`, then run `npm run verify:strategy`.
734
+ - [Anthropic model catalog](https://platform.claude.com/docs/en/about-claude/models/overview) pricing, legacy status
735
+ - [Anthropic effort parameter](https://platform.claude.com/docs/en/build-with-claude/effort) levels, guidance, caching constraint
736
+ - [OpenAI model catalog](https://developers.openai.com/api/docs/models) GPT-5.6 family
737
+ - [CR-Bench](https://arxiv.org/html/2603.11078v1) — review precision/recall/SNR
738
+ - [SWE-bench Verified leaderboard](https://benchlm.ai/benchmarks/sweVerified) — Claude-family scores