ci: split Opus review into discovery and validation passes - #2332
Conversation
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Blocked on #2333 — and the block is the design workingFirst CI run failed in 21s, exactly where it should: The extraction step reads the prompts from the base commit so a PR cannot rewrite the prompt that reviews it. This PR introduces those prompts, so on its own base they do not exist — the step refuses, and the fail-closed gate turns the check red rather than reviewing against nothing. Both halves behaved correctly; the lane simply cannot bootstrap itself. I did not add a fallback to the PR-head copy. That would mean any time these files were absent from So this splits in two:
Converting to draft until #2333 is in. Also pushed The latency measurement this PR was opened to get is therefore still outstanding — it needs a run that actually reaches both stages. |
Design Review (Fable 5) — 🟡 CONCERNSAdvisory design-level review of Design-Verdict: CONCERNS Sound, evidence-driven split; the risk left is operational — a doubled worst-case runtime under an unchanged fail-closed 90-minute ceiling. Watch
Suggestions
[DESIGN-REVIEWED] d2691a9 |
UX Review (Fable 5) — ⏭️ skippedRevision |
Dormant on purpose: nothing reads these yet. They land first so that the workflow change in the follow-up PR has something to read. The reformed Opus lane materialises its prompts from the BASE commit, not the PR head, so a pull request can neither weaken the rules that govern it nor rewrite the prompt that reviews it -- the same property the AUTOSDE rule snapshots already have. That makes the PR which INTRODUCES the prompts unable to run its own review: the extraction step looks for them on the base commit, does not find them, and fails the job closed. Verified on the first attempt (#2332): "opus-discovery.md is missing or empty on the base commit ... Refusing to review against an unspecified contract." The alternative -- falling back to the PR head copy when the base has none -- was rejected. It would mean that any time these files were absent from main, a pull request could supply the prompt used to review it, which is exactly the hole the base-ref read exists to close. So the two halves land separately: these files first, the workflow rewrite and its tests second. Prose uses the two-word brand form, which the Brand Name Gate requires on added lines.
fcd8ba5 to
5c4a637
Compare
Dormant on purpose: nothing reads these yet. They land first so that the workflow change in the follow-up PR has something to read. The reformed Opus lane materialises its prompts from the BASE commit, not the PR head, so a pull request can neither weaken the rules that govern it nor rewrite the prompt that reviews it -- the same property the AUTOSDE rule snapshots already have. That makes the PR which INTRODUCES the prompts unable to run its own review: the extraction step looks for them on the base commit, does not find them, and fails the job closed. Verified on the first attempt (#2332): "opus-discovery.md is missing or empty on the base commit ... Refusing to review against an unspecified contract." The alternative -- falling back to the PR head copy when the base has none -- was rejected. It would mean that any time these files were absent from main, a pull request could supply the prompt used to review it, which is exactly the hole the base-ref read exists to close. So the two halves land separately: these files first, the workflow rewrite and its tests second. Prose uses the two-word brand form, which the Brand Name Gate requires on added lines.
5c4a637 to
5582c6c
Compare
|
Rebased onto Out of draft. This run is the first one that should actually reach both stages, so it is also the first real measurement of the end-to-end latency question this PR was opened to answer. |
7a8a740 to
4567118
Compare
|
Prior reviewed SHA: Candidate truncation made the gate fail open: fixed. This was the third fail-open of the same shape in this pipeline, after the empty-candidate list and the symlinked scratch paths. A candidate emitted past 24,000 bytes was dropped with only a Fixed in two parts rather than only failing at the old cap:
Also absorbed from One consequence a reviewer should weigh, because it is not covered by this PR's evidence: every measurement in the description was taken on |
4567118 to
e61f0b8
Compare
|
Prior reviewed SHA: Discovery could poison the validation workspace: fixed. The grant was Reading the prompts from the base ref closes the "a PR rewrites its own reviewer" path through git. This closes the same path through the shell — and it was an asymmetry this PR introduced, since the fork lane has always prefetched its diff and run with no Bash at all. Fixed exactly as suggested, matching the fork lane:
Two existing tests asserted the old posture; they follow it rather than being deleted. For the record, since it is a pattern rather than one bug: this is the fourth finding on this PR and the fourth of the same shape — a way for the two-stage lane to emit a clean verdict without having actually reviewed. The earlier three were an empty candidate list, symlinked scratch paths, and silent candidate truncation. Splitting a reviewer into two stages creates a handoff, and every artifact crossing that handoff is attack surface the single-call design did not have. Worth weighing against the recall gain when reviewing this PR. |
e61f0b8 to
9ea15ea
Compare
Rebased onto
|
| arm | design | named the known defect | labelled it BLOCKING | false-positive BLOCKING | advisory |
|---|---|---|---|---|---|
| A | #2379 as merged (one call, inline contract, PR intent) | 2/6 | 0 | 0/8 | 5 |
| B | this PR (two calls, prompt files from base ref) | 4/5 | 2 | 0/8 | 8 |
| C | hybrid: #2379's contract split into two independent calls | 4/6 | 0 | 0/8 | 19 |
Corpus: 3 PRs carrying a defect the GPT lane caught and the old Opus lane missed
(recall), 4 PRs where both production lanes emitted zero findings at that SHA
(false positives). n=2 per cell, 69/70 cells completed.
Arm B is the only design that produces a blocking verdict. One example, verbatim:
**BLOCKING — src/kiro_crew/dashboard/handlers/terminal.py:309**— a JSON
null/0/""inredact_outputcoerces toFalse, silently turning off live PTY
credential redaction — the falsy-value fail-open classbackend-security-controls
(blocking) names.
Two findings worth stating because they cut against my own prior claims:
- The effective variable is the contract, not the call structure. B and C both run
two independent calls; only B blocks. Splitting the call is not what recovers a
blocking verdict — the validation contract's wording is. - "Demote instead of drop" backfired. Arm C is my own idea (an out-of-scope fix
demotes to advisory rather than being dropped) and it became a pressure valve: 19
advisories, zero blocking. Reporting went up, gating went to nothing.
I also retract something I asserted earlier in this PR's history: I claimed the
opus-5 → opus-4.8 switch in #2339 invalidated this PR's original evidence. Arm B
still blocks, so that claim was unfounded.
What this resolution keeps from #2379, and what it supersedes
Kept — #2379's five contract guarantees are all still enforced, retargeted at the
prompt files where the contract now lives (TestClaudeReviewQualityDimensions):
consequence-chain bar, advisory-only quality dimensions, the authoritative
blocking: true flag, terse output, and the mechanical/semantic division of labour.
Four already existed in the prompts under different wording; the fifth was the
BLOCKING cap, and I adopted #2379's 5 over this branch's 2 — a strict relaxation
that cannot change any measured outcome, since arm B emitted at most 1 per review.
Superseded — the Fetch PR intent step and its two tests. Arm B was measured
without author prose reaching the reviewer, so carrying the intent step here would
ship an untested combination, which is precisely what arm C was. Re-introducing PR
intent (and the sage quality dimensions) on top of arm B is a follow-up that deserves
its own measurement, not a freebie folded into a conflict resolution.
What I am not deciding
Whether the contract belongs in files or inline is a maintainer call, and #2379 is the
later, human-reviewed decision. I have brought data and a working diff; I am not
claiming authority to reverse that. If the intent is that the lane stays inline, say so
and I will close this and re-propose the two prompt-file guarantees separately.
Caveats, so the table is not read as stronger than it is
- n=2 per cell, 5–6 observations per arm. 2 versus 0 is suggestive, not statistically
significant. - The model was not pinned —
spawn runexposes no--modeland no model field is
recorded — so this compares the designs under one model, not production's
opus-4.8specifically. - One cell (
2152arm B rep 1) never completed after three attempts, so arm B is n=5
against the others' n=6. - PR titles/descriptions were fetched today and may have been edited post-merge. That
favours arms A and C, which are the arms that lost.
Harness and all 69 outputs are reproducible from gen_compare.py / grade_compare.py.
Also worth flagging independently of this PR: .github/review-prompts/*.md are
currently dead files on main — nothing references them since #2379 inlined the
contract. Whichever direction wins, someone should either delete them or wire them
back up.
The Opus lane had a 2.7% blocking rate against GPT's 18% across 73 PRs, and 63 of 73 posted comments contained literally "No findings." A controlled experiment on this repo found the cause is not the model and not the architecture -- it is three clauses in the prompt, each independently sufficient to silence a defect the same model reports 3/3 times without them: the closed-list reading of the residual defect classes, the certainty threshold, and "drop the finding if the fix touches untouched code". Splitting the call while keeping those clauses in the first half did NOT help: the discovery pass produced zero candidates, so the filter had nothing to keep. Precision enforcement has to sit downstream of discovery, which is also what Anthropic's own code-review plugin does (parallel discovery agents, then a per-candidate validation agent with a confidence floor). What changes: * Stage 1 (discovery) generates candidates with generous recall and no precision gates. Its output is never posted and gates nothing. * Stage 2 (validation) is an independent call that re-derives input / call path / observable outcome for every candidate from code it opens itself, keeps only those it scores >= 80, and only then applies the closed blocking list. It keeps `id: review`, so the existing transcript capture, comment upsert and fail-closed gate are unchanged. * The prompts move out of the YAML into .github/review-prompts/ and are shared by the same-repo and fork lanes, which previously carried near-duplicate copies. They are materialised from the BASE commit, so a PR can neither weaken the rules that govern it nor rewrite the prompt that reviews it; a missing prompt fails the job rather than degrading into an unspecified review that could look clean. * Candidates cross the stage boundary as a workspace FILE, never string interpolation -- model output must not reach YAML or a shell argument, and a file has no arg-length ceiling. * The fix-scope rule changes behaviour instead of being deleted: a finding whose only remedy lies outside the changed lines is reported as advisory rather than dropped, because the author cannot land the remedy here but the signal is still real. A regression the diff itself introduces still blocks, since reverting the hunk is an in-diff fix. Job name, required check, tool surface and the marker contract are unchanged. The fork lane keeps its no-shell posture (its diff is pre-fetched from GitHub's compare endpoint). Measured locally on the same corpus, running these exact prompt files: * 8 negative-control runs (PRs where BOTH production lanes reviewed the same SHA and both emitted zero findings): 33 candidates in, 0 BLOCKING out, 7 advisory out. The validator removed 79%. No run would have turned the required check red on a PR two reviewers passed. * Positive controls (a GPT blocking the old Opus lane missed): recovered on #2109 (blocking, same guard GPT named, 2/2) and reported on #2169 (advisory, 4/4, where the old lane said nothing). #2152's GPT finding was generated by discovery but dropped by the validator. Three tests that asserted on prompt text follow it to its new home rather than being deleted, and TestOpusTwoStageArchitecture locks the split in place -- including a cross-check that the marker the gate greps equals the marker the validation prompt is told to emit, because a typo either side fails every PR closed and silently. Known and deliberately not addressed here: the two stages are sequential, so end-to-end latency roughly doubles under the same 90-minute runaway backstop. That number can only be measured on a real CI run. Stacked on the prompt-file bootstrap PR. Because the extraction reads the base commit, the prompts have to exist on main before this can run -- and for the same reason, a future PR that EDITS a prompt is still reviewed by the OLD one. The workflow header documents that.
9ea15ea to
d2691a9
Compare
Round on
|
Dormant on purpose: nothing reads these yet. They land first so that the workflow change in the follow-up PR has something to read. The reformed Opus lane materialises its prompts from the BASE commit, not the PR head, so a pull request can neither weaken the rules that govern it nor rewrite the prompt that reviews it -- the same property the AUTOSDE rule snapshots already have. That makes the PR which INTRODUCES the prompts unable to run its own review: the extraction step looks for them on the base commit, does not find them, and fails the job closed. Verified on the first attempt (kirodotdev#2332): "opus-discovery.md is missing or empty on the base commit ... Refusing to review against an unspecified contract." The alternative -- falling back to the PR head copy when the base has none -- was rejected. It would mean that any time these files were absent from main, a pull request could supply the prompt used to review it, which is exactly the hole the base-ref read exists to close. So the two halves land separately: these files first, the workflow rewrite and its tests second. Prose uses the two-word brand form, which the Brand Name Gate requires on added lines.
Summary
The Opus review lane almost never says anything: 2.7% blocking rate vs GPT's 18% across 73 PRs, and 63 of 73 posted comments contained literally
**No findings.**.A controlled experiment on this repo found the cause is neither the model nor the call architecture — it is three clauses in the prompt. Each one, added on its own to an otherwise bar-free prompt, silenced a real defect that the same model reports 3/3 times without it.
This PR moves precision enforcement downstream of discovery, which is what Anthropic's own
code-reviewplugin does (parallel discovery agents → a per-candidate validation agent with a confidence floor).What the experiment found
Ground truth: PR #2169 deleted three
except BaseException:temp-file cleanup handlers and replaced them with a helper that only unwinds onexcept Exception, so a Ctrl-C duringfsyncorphans a full copy of the deploy store. GPT blocked on it. Opus said nothing.Single-clause ablation, n=3 each, isolated the three suppressors: the closed-list reading of the residual defect classes, the certainty threshold, and "drop the finding if the fix touches untouched code". No single clause is necessary — removing any one from the full prompt left it silent, so the suppression is over-determined.
Arm B is the important negative result: splitting the call is not the fix. Keeping those clauses in the discovery half produced nothing for the filter to keep.
Industry evidence points the same way — arXiv:2603.18740 measures a 16–93pp recall loss from "bug-free" framing with only a 0.8pp FP increase, Greptile could not reduce nits by prompt without also losing critical comments, and Cursor moved Bugbot to aggressive discovery prompts with category filtering and FP validation as post-generation steps.
What changes
id: review, so the existing transcript capture, comment upsert and fail-closed gate are unchanged..github/review-prompts/and are now shared by the same-repo and fork lanes, which previously carried near-duplicate copies (that duplication is why a single-file fix would have left fork PRs on the suppressive prompt). They are materialised from the BASE commit, so a PR can neither weaken the rules that govern it nor rewrite the prompt that reviews it. A missing prompt fails the job rather than degrading into an unspecified review that could look clean.Job name (
Opus 5 Review), required check, tool surface and the[OPUS-REVIEWED]/[BLOCK-MERGE]marker contract are unchanged. The fork lane keeps its no-shell posture.Verification
Ran these exact prompt files, both stages, against the same corpus.
Negative controls — PRs where both production lanes reviewed the same SHA and both emitted zero findings. This is the question that decides whether the lane can ship: does loosening discovery make the gate noisy?
No run would have turned the required check red on a PR two reviewers already passed.
Positive controls — a GPT blocking the old Opus lane missed:
tailnet_serve.py:396), 2/2One post-fix #2152 run blocked instead on a different, rule-backed defect that neither production lane caught at that SHA: the new PTY integration tests spawn the operator's real login shell in their real
$HOMEand leaveprintf 'AKIAIOSFODNN7EXAMPLE\n'in their real shell history —no-test-side-effects(blocking: true,test/**/*.py).Tests
Three tests asserted on prompt text that now lives in the prompt files. They follow the prompt to its new home rather than being deleted.
TestOpusTwoStageArchitecture(7 tests) locks the split in place — most usefully a cross-check that the marker the gate greps equals the marker the validation prompt is told to emit, because a typo on either side fails every PR closed and silently. Prose assertions run through a whitespace-collapsing helper so re-wrapping a paragraph cannot break them.40 passed; flake8 / isort / mypy clean.Known limits — please read before approving
sessionWatch.ts:256,DevFleetPage.tsx:415), which argues against hallucination but does not settle it.#2169lands advisory, not blocking, in 4/4 runs. The validator's reading is defensible — the guard was removed but a narrower compensating replacement exists, so it is not the "removed guard with no compensating replacement" class — but blocking parity with GPT is not achieved for that case.Experiment artifacts (23 ablation variants, 14 pipeline runs, prompts, per-run outputs) are on the dev desk under
opus-lane-exp/, not in this repo.