Repository navigation
pr-review has no false-negative metric and no production feedback loop — accepted defects found by advisory bots after approval are never counted or learned from #1596
Description
The gap
We measure pr-review's operational health and its false positives. We do not measure its false negatives, and nothing feeds them back into the improvement loop.
What exists today:
| surface | measures | does not measure |
|---|---|---|
scripts/pr_review_health.sh (daily-pr-review-health.yml) |
run outcomes, cancellation rate, sweep hit-rate | review quality at all |
scripts/reviewer_report.sh / #1220 scorecard |
third-party bots' own activity: review counts, thread resolution, reactions | any comparison against pr-review's verdict on the same PR |
evals/deep-review/ + #1088 / #1094 / #1107 |
held-out synthetic cases, llm-judge, and explicitly the false-positive rate | defects pr-review approved past in production |
So the one signal that is both free and perfectly labelled — a third-party bot found a real defect on a PR pr-review had already approved — evaporates. It is never counted, never reported, and never becomes an eval case.
Evidence from a single day (2026-08-30/31)
Every one of these is a real, accepted defect that pr-review did not raise:
| PR | found by | defect | pr-review's state at the time |
|---|---|---|---|
.github#995 |
graphite-app (08-21) and CodeRabbit (08-30), independently, 9 days apart | ADR asserted a "rolling" 5-hour window while also asserting utilization is monotonic and only resets_at resumes — a self-contradiction that would have shipped as the acceptance criterion for #641/#994 |
had reviewed; never flagged it |
.github#1017 |
codeant-ai | contract test compared jq's rendered text, so "50" passed as an integer and "true" as a boolean — in the very test whose stated job is enforcing types |
approved |
.github#1017 |
CodeRabbit | config's own _exempt_note claims the exempt set is "the SAME set as standards/pr-limits.json", but nothing enforced it — silent drift between two gate policies |
APPROVED at 22:32, defect raised at 22:40 |
#1556 |
codeant-ai | missing/placeholder Copilot token returns status 1, indistinguishable from a rate limit → deterministic auth failure classified as retryable (now #1591) |
approved |
#1558 |
codeant-ai | orphan-rescue sets force_review=true, which bypasses all CI/advisory gates, not just the idempotency marker (now #1590) |
approved |
#1559 |
codeant-ai | metadata-digest re-review path takes no atomic claim; two triggers can both launch the cascade (now #1589) |
approved |
Six real findings, five distinct PRs, three third-party bots — in one day. Three of them were substantive enough that they are now tracked issues.
Is pr-review approving defective PRs a bug?
Partly, and the honest answer matters for what to fix.
Not a bug: the architecture deliberately delegates depth to advisory bots (there is an advisory-review gate that waits for them) and required_review_thread_resolution: true means late findings still block the merge. On #1017 that backstop worked — the defect was raised after approval and the ruleset held the PR. So the system caught it even though the agent did not.
Genuinely a bug, in two specific places:
-
The advisory gate proceeds on partial bot coverage. Observed verbatim on
.github#995:[advisory-gate] Only 2/3 required bots submitted; head is 1966s old — timeout fallback, proceedingpr-review approved having seen 2 of 3 advisory opinions. The timeout is a liveness protection, but it silently converts "all reviewers agreed" into "the ones that answered agreed", with no record that the verdict was taken on partial evidence.
-
Approval is never re-evaluated when a later finding lands. pr-review's approval on
#1017remained attached while CodeRabbit was raising a real defect. Only the ruleset stopped the merge. If thread-resolution enforcement were ever relaxed — or the finding arrived as a comment rather than a review thread — an approved-and-defective PR merges.
The deeper issue is that an approval from pr-review is currently read as evidence of defect-freedom, and it is not — it is an orchestration signal. Nothing quantifies how much weight it actually deserves.
Proposal
1. Measure the miss rate
Extend the existing deterministic pipeline (no LLM needed for the metric itself — reviewer_report.sh already reads reviews, threads and resolution state in one GraphQL round-trip per repo). For each PR, emit:
missed_findings— threads opened by a trusted advisory bot after pr-review's approving review on the same head SHA, that were subsequently resolved as accepted (i.e. a fix landed or the disposition accepted it) rather than refuted.caught_findings— findings pr-review raised itself that were accepted.partial_evidence_approvals— approvals issued via the advisory-gate timeout fallback, i.e. before all registered bots reported.
Report per week alongside the #1220 scorecard, and per reviewer, so we can see which bot is catching what pr-review misses. That is directly actionable: a class of defect one bot repeatedly finds first is a prompt gap.
Distinguishing accepted-from-refuted is the crux. Roughly 3 of 4 codeant/gemini findings dispositioned this same day were false positives correctly refuted (yq claimed missing when the dedicated test workflow installs it; a registry assertion claimed unguarded when the adjacent test covers it). A naive "bot opened a thread → pr-review missed it" counter would be dominated by noise and would push the agent toward more false positives. Use the accepted/refuted disposition already recorded in the resolving reply, and treat anything ambiguous as not a miss.
2. Close the loop into the evals — dev split only
Harvest accepted misses into evals/deep-review/dev/cases.jsonl as regression cases (de-identified per evals/README.md).
Hard invariant, do not violate: harvested cases go to dev/ only. holdout/cases.jsonl is CODEOWNER-gated and holdout-guard.yml hard-fails proposer-authored PRs touching evals/; the whole point of the split is that the proposer must never see the cases it is scored against. Auto-harvesting into holdout/ would silently destroy the one control that makes #1088's success metric meaningful. Harvesting into dev/ is exactly the "merged-PR few-shot" gap #1088 already names — this issue supplies the source of truth for those cases instead of hand-authored synthetic ones.
3. Fix the two concrete holes
- Record on the PR when an approval was issued under the advisory-gate timeout, and count it (
partial_evidence_approvals). Consider requiring a re-check rather than a bare approval when coverage was partial. - When a trusted advisory bot opens an accepted finding after an approving review at the same head, dismiss or re-run that approval rather than leaving it standing on
required_review_thread_resolutionalone.
Acceptance criteria
-
missed_findings,caught_findings,partial_evidence_approvalscomputed deterministically from GitHub review data — no LLM in the metric path, matchingreviewer_report.sh's stated design. - Accepted-vs-refuted classification is explicit, and ambiguous dispositions count as not a miss; the rule is documented and unit-tested with both a real miss and a refuted false positive.
- Weekly report surfaces miss rate overall and per third-party reviewer.
- Accepted misses are harvested into
evals/deep-review/dev/cases.jsonl, de-identified, never intoholdout/; a test asserts the harvester cannot write aholdout/path. - An approval issued under the advisory-gate timeout is recorded as such on the PR and counted.
- A post-approval accepted finding from a trusted bot invalidates the standing approval rather than relying solely on thread-resolution enforcement.
- Baseline the miss rate over the trailing 30 days before any prompt change, so
#1088's improvements can be measured against it.
Relationship to existing work — extends, does not duplicate
#1088(world-class bug hunter) targets bug-finding depth and names "merged-PR few-shot — no learning from this repo's past review→merge histories" as gap 4, with a success metric of held-out eval score plus no false-positive regression. It has no false-negative metric and no production feedback path. This issue supplies both, and gives gap 4 a real case source. Treat as a companion/sub-issue.#1220measures third-party reviewers' activity, never pr-review's verdict against theirs.#1094/#1107gate on the held-out synthetic split; this is about production signal.#1552(merged) surfaced pr-review'sdecision/reason— the plumbing this metric can build on.
⚠️ CI steering for PR #1604 (added 2026-08-31 after two failed auto-fix passes)
Two distinct failures on #1604. They need different fixes; the first is a design decision, not a test to relax.
1. not ok 29 Advisory gate: script is minimal — [ "$lines" -lt 500 ] (tests/dev-lead/unit/test_advisory_review_gate.bats:172)
scripts/lib/advisory-review-gate.sh has a deliberate size guard: "non-blocking means fewer lines". Adding partial-evidence recording pushed it past 500.
Do NOT raise the threshold to make this pass. The guard encodes a real property — the advisory gate runs on every review pass and is deliberately kept small and non-blocking. Growing it is the thing the test is there to catch.
Instead, extract the partial-evidence recording into its own library — e.g. scripts/lib/review-miss-evidence.sh — and have the gate call it. That matches the pattern #1567 established with scripts/lib/review-change-evidence.sh (pure, unit-tested helpers in their own lib; the caller keeps only the plumbing). It also fixes failure 2 below for free, because the new tests can source the new lib directly.
2. not ok 702-705 Partial-evidence: … — source "$SCRIPT_DIR/lib/advisory-review-gate.sh" failed (tests/dev-lead/unit/test_partial_evidence_marker.bats:16)
The setup() sources a path that does not exist, so all four tests error before asserting anything. Point it at the real location of whichever lib ends up owning these helpers (see 1 — after the extraction it should source scripts/lib/review-miss-evidence.sh). Check SCRIPT_DIR resolution against a sibling unit test that already sources a scripts/lib/*.sh correctly rather than assuming the prefix.
Reminder on scope
Everything else on #1604 is green (49 checks). Keep the change to these two items; do not touch the metric semantics already agreed in this issue — in particular the accepted-vs-refuted classification and the dev/ split only, never holdout/ invariant both still stand.
Activity
github-actions commentedon Aug 31, 2026
Thanks for this thorough and well-evidenced writeup — the day-of examples and the careful distinction between system-level catches and agent-level misses make the gap very concrete. Before diving into implementation, the most important question is around the accepted/refuted classification rule: do you have a specific heuristic in mind for determining when a resolved thread counts as "accepted" (e.g. a follow-up commit landed on the same files, or the resolver explicitly acknowledged the finding), versus "refuted" — and is there an existing example in the current
reviewer_report.shoutput or GraphQL data that already exposes that signal, or would it need a new field?don-petry commentedon Aug 31, 2026
Dev-Lead Implementation Plan
Issue: #1596 — pr-review has no false-negative metric and no production feedback loop
Scope
Add a deterministic (no-LLM) false-negative pipeline: measure
missed_findings/caught_findings/partial_evidence_approvalsfrom the review datareviewer_report.shalready collects, surface the miss rate weekly (overall + per third-party reviewer), harvest accepted misses intoevals/deep-review/dev/only, record advisory-gate timeout approvals on the PR, and provide a mechanism to invalidate a standing approval when a trusted bot lands an accepted post-approval finding.Implementation Checklist
scripts/lib/pr-review-miss-rate.sh(new, pure-logic): accepted/refuted/ambiguous disposition classifier; per-PR miss metrics jq program (findings a trusted bot opened after pr-review's approving review, resolved-as-accepted); org-level aggregation (overall + per reviewer); markdown renderer; standing-approval-invalidation detector; partial-evidence + approval marker regexes.scripts/lib/miss-harvest.sh(new, pure-logic): dev-path assertion that refuses anyholdout/path; de-identification (strip URLs/@mentions/token-like strings); accepted-miss →evals/deep-review/dev/cases.jsonlcase builder + append.scripts/lib/advisory-review-gate.sh: addadvisory_partial_evidence_marker()+maybe_post_partial_evidence_marker(); setADVISORY_PROCEED_REASONand stamp the marker on the PR when approval proceeds under the head-age/quiescence timeout fallback.scripts/reviewer_report.sh: source the miss-rate lib; add an additivemiss_prcollection pass (mirrors the [Phase 4] Capture the pre-rollout baseline for the three success metrics, including a new no-action-comment noise metric #1411 agent_comment pass); render a "Review miss rate" section inrender_reviewer_report.scripts/invalidate-standing-approval.sh(new): DRY_RUN-default wrapper over the pure detector (mirrors the repo's staged-write convention).scripts/lib/README.md: document the metric, the accepted/refuted/ambiguous rule, the dev-only harvest invariant, the partial-evidence marker, approval invalidation, and the 30-day baseline runbook..github/workflows/lint.yml: add the new bats files to thebatsjob list.Tests to Write
tests/test_pr_review_miss_rate.bats: a real accepted miss counts; a refuted false positive does NOT; an ambiguous disposition does NOT;caught_findingsandpartial_evidence_approvalscounting; per-reviewer attribution; invalidation detector.tests/test_miss_harvest.bats: harvester cannot write aholdout/path (asserts refusal); de-identification strips a URL/mention; appended line is valid JSON with anid.tests/test_advisory_review_gate.bats: extend with partial-evidence marker string + reason.Test Command
scripts/run-bats.sh tests/test_pr_review_miss_rate.bats tests/test_miss_harvest.bats tests/test_advisory_review_gate.bats tests/reviewer_report.batsandbash .dev-lead/scripts/dev-lead-lint.shdon-petry commentedon Aug 31, 2026
Dev-Lead: Implementation Complete — PR #1604
The implementation for issue #1596 is durable: commits are pushed and a pull request is open. This record is backed by verifiable artifacts you can confirm in one click.
79e5a98825c5e5c69cf0e51b9907390bcc565218Review happens on the PR. Acceptance criteria and test results are evidenced by the PR's diff and CI checks — not asserted here. If a later stage fails, this claim is retracted in place.
feat: implement issue #1596 — pr-review has no false-negative metric …
fix(advisory-gate): keep the gate under its size guard instead of rai…
feat: implement issue #1596 — pr-review has no false-negative metric …
fix(pr-review): re-evaluate at the same head after its own gate dismi…