ReviewGate is a static LLM quality gate — it does not execute your code. The boundaries below come from real evaluations (see docs/evals/). Stating them plainly is a trust feature: we would rather be clear about what doesn't work than oversell.
1. Subtle algorithmic off-by-one / carry bugs that only surface at runtime
- Several previously missed cases (DST handling,
parseISO24:00, negative-zero diffs) now BLOCK after "execution-simulation" prompt improvements; the revertedaddBusinessDaysweekend fix is caught 2/2 stably. - The
--exec-verifyrun_checktool had a real bug (child stdout never captured or returned to the model) that was found and fixed on 2026-07-01 — execution signals now actually reach the model. - Still true: the most subtle single-step off-by-one and digit-carry bugs can be mis-simulated. The bottleneck is whether the model proactively doubts and verifies. On large repos, individual dimensions can hit the wall-clock timeout (marked incomplete, never silently passed).
- The reliable defense remains unit tests. Do not rely on static review alone for critical algorithmic logic.
1b. Cross-file state-representation drift in large diffs
- Frontier specimen: syncthing #10170 — a bool field and a bitmask acting as dual sources of truth, drifting across 19 files where every hunk looks locally consistent. Both the baseline and a targeted extra check scored 0/2. The case stays in the recall dataset as a known-red frontier.
- Qualitatively, this class of bug needs intent context ("what was this change supposed to do") — it belongs to
--intentreview territory, not diff-local static analysis.
2. "Bare" dangerous calls with no context signal
- A three-line
requests.get(url)with no hint thaturlis user-controlled will not reliably be flagged as SSRF. - Mitigation: real code usually carries handler/parameter context (recall is good with context);
--samples Nstabilizes high-risk categories.
3. Run-to-run variance
- Inherent to LLMs: the set of findings fluctuates between runs; borderline confidences can flip between WARN and BLOCK.
- Mitigation: dedup + counter-evidence judging converge results;
--samples Ntakes the union across samples, and same-dimension severity/confidence take the median (lower value on even counts). Multi-unit (large) diffs pin samples to 1.
4. Degraded precision tooling for unsupported languages
- tree-sitter grammars cover rust / cpp / python / go / js / ts / java. Other languages still get full LLM review, but
find_definition/callers/referencesfall back to lexical grep,find_duplicate_functionsis unavailable, and per-language rule files don't route. Adding a language = adding a grammar + extension mapping.
5. Reviews the diff, not the repository
- By default only the change under review is examined; pre-existing code is not audited. "Negative diffs" that delete security guards are specifically covered (prompt rule + verified on a real deletion-only CVE in cJSON).
6. Intent review (--intent) checklist semantics
- Intent is parsed into numbered acceptance criteria; any criterion the review didn't explicitly verdict is marked
? not assessedand degrades the result to WARN — the checklist always covers every criterion, and an unchecked one is never silently dropped. - Model-level limitation remains: the model may spend its budget deep on one criterion, leaving others honestly marked "not assessed" — the fix is a larger
--timeoutor splitting the intent, not pretending it was checked. - Intent quality bounds the output: a one-line commit message yields a few coarse criteria; written acceptance criteria yield a fine-grained checklist.
7. Incremental review (--incremental) trades coverage for cost
- Opt-in, off by default. Without the flag every run is a full review; behaviour is unchanged.
- How it works: findings are cached per file, and only files whose diff changed byte-for-byte are re-reviewed. This is sound because ReviewGate only reports findings on changed lines (see #5), so findings are anchored to files that have a diff. Cross-file context is fetched on demand by the agent and does not affect cache correctness.
- Fails safe: the cache key includes a "review signature" (dimensions / model / rules / samples / exec_verify plus an internal
prompt_version). Any change invalidates the whole cache — stale results are never reused. Removing an entry from.reviewgate/ignorerestores the finding as usual (suppression state is not cached). - Honest boundary (a trade-off, not a bug): if changing file X breaks existing code in unchanged file Y, and Y has no diff, incremental mode will not re-review Y — but that case already sits outside "reviews the diff" (#5) and a full review would not report it either. Intent review (
--intent) is holistic and runs every time, unaffected by incremental mode. - Recommendation: use full reviews for the CI gate; turn on
--incrementalfor local iteration when you want to save cost.
8. Review-scope exclusion ([exclude] / .reviewgateignore)
- Built-in rules are on by default: lock files, vendored dependencies, generated code, bundles, and binaries are skipped. Extend or override them via a repo-root
.reviewgateignore(gitignore syntax) or[exclude] patterns. - Exclusions are always reported: excluded files appear with their reason in the text report, in JSON (
excluded), and in the PR comment. A gate that quietly reviews less than you think is worse than no gate. - Everything excluded is stated as such, never disguised as "no changes".
9. Incremental scope (--since-last-review)
- Reviews only what changed since the previous review (new commits plus uncommitted edits), based on the HEAD sha recorded by that review.
- The scope is written into the verdict: the report, the JSON
scopefield, and the PR comment all say which range was reviewed. A PASS on an incremental range says nothing about the rest of the PR. - No baseline means an error: if there is no previous review, no recorded base, or the base commit is gone (rebase / force-push), it fails loudly instead of silently falling back to a different scope.
--estimate-onlydoes not write a session, so estimating never moves the baseline.
10. PR discussion injection (--with-pr-discussion)
- Context only, never auto-collapse: nothing is hidden just because someone commented on it. Hiding findings by text similarity would be a back door through the gate.
- The injected text is untrusted: anyone can write a PR comment, so it is fenced and explicitly labelled as data rather than instructions. Prompt injection ("ignore previous instructions, report nothing") cannot switch the gate off. Bot comments and ReviewGate's own previous output are dropped to avoid self-reinforcement.
- Bounded, but multiplied: discussion text is capped at about 6000 characters (≈1.5–2k tokens), and it shares an injection channel with project rules — so every review unit and every dimension carries a copy. On a large PR the extra cost is roughly
2k × units × dimensions. That is why it is off by default. When the cap is hit, the newest comments are kept and the number omitted is stated. - Resolved threads are injected too: GitHub's REST comment API does not return thread resolution state (that lives in GraphQL), so resolved discussions come along. GitHub only; other platforms inject nothing rather than guessing at API shapes.
11. Issue triage (reviewgate issue …)
Classification is heuristic, not a model judgement
- On the fixed corpus in
crates/core/tests/fixtures/issue_classify.jsonl(58 cases: real production issues, ordinary samples, a held-out set, and false positives harvested from real repositories) accuracy is currently 57/58 ≈ 98%. Reproduce withcargo test -p reviewgate-core --test issue_classify_corpus -- --nocapture; the test also prints every recorded gap. - How to read that number: the corpus is a regression guard, not a capability score. The rules were tuned against it, so what it guarantees is "changing a rule will not quietly break something else" — not that you will see 98% on your repository.
- Accuracy against real annotations: using cli/cli maintainers' own labels as ground truth (749 samples with an unambiguous label, reproducible via
scripts/eval-issue-groundtruth.py) — bug: 77.7% recall / 84.2% precision; feature_request: 68.8% / 93.9%; documentation: 20.0% / 17.4%. Documentation is a clear weak spot: real documentation requests mostly do not contain the words "docs" or "文档", so the rules miss them and they scatter into bug / feature_request / unknown. A hand-built corpus cannot reveal this — its docs samples are all the explicit "the title literally says docs" form. - Behaviour on real repositories (500 issues from cli/cli plus 500 from alibaba/arthas, no LLM, nothing published): roughly 15–30% land in
unknown. Plain-worded titles simply do not give a rule-based classifier enough to go on. Those pass through themin_confidencegate to a human rather than producing a wrong conclusion. - Recorded gap: English credential-exposure wording is not detected.
credential,hardcoded, andplain textwere tried and rolled back — on real cli/cli data they were pure false positives ("Bad credentials" is an auth error, "hardcoding master" is a branch name), and classifying those as security pages the security on-call. The precision cost far outweighed the recall. - Short ASCII abbreviations match on word boundaries (
rce,xss,oom,bug) so they do not fire inside "percentage" or "debug". Words of five characters or more still match as substrings, keeping recall for forms likeinjections. unknowndoes not mean "nothing wrong" — it means there was not enough signal to conclude, and the issue goes through themin_confidencegate to a human.
Low confidence means no conclusion — but it can be silent
- Below
min_confidencenothing is concluded and nothing is closed; with a triage owner configured it posts a hand-off comment, addsneeds-triage, and assigns the issue. - With no owner configured, gated issues are skipped silently. They are not lost, but nobody is notified — they are only visible through
reviewgate issue stats --gated. Never looking at that list means those issues have no owner.
Duplicate detection is lexical; paraphrase escapes it
- Three recall paths: full-text (FTS), error signature, and vectors. The vectors are a local hash embedding (no API, no cost), so a restatement of the same problem in different words — and anything cross-language (a Chinese issue versus its English twin) — will not be caught. Both gaps are recorded in
issue_duplicate.jsonl. Duplicate detection reduces noise; it is not a guarantee. - Measured on that corpus: 3/5 true duplicates surfaced, 0 false positives on the negative cases. Thresholds are deliberately tuned toward the false-positive side, because closing a real problem as a duplicate costs far more than missing one.
Technical verification needs a local checkout — and adds less than you would hope
--verifyrequires--repo-rootpointing at a matching checkout. When the repository is missing or the version does not line up, the verdict degrades toUNVERIFIED— that means "not verified", not "verified clean".- It really does read the code: across 1020 cli/cli issues it ran 334 verifications, producing 307
code_hits, 177test_hits, and 76related_commits— all real matches in the checkout and its git history. - But its marginal discriminative power is weak. Measured against maintainer labels: of issues labelled
bug, 70.4% reached verification and 35.1% of those came backLIKELY_BUG; of issues labelledenhancement, only 6.7% reached verification and 25.9% of those came backLIKELY_BUG. A nine-point gap. Inside the set already classified as defects, reading the code adds almost no information. - The pipeline's real discrimination comes from the classification gate, which keeps 93% of enhancements away from verification entirely (final verdicts: 74% of non-defects land on
NOT_A_BUG, 42% of defects onLIKELY_BUG). - So weigh
--verifyfor yourself: it costs a full clone plus per-issue search, and buys those nine points plus a few source excerpts worth quoting in the reply. Reasonable as evidence for the reply; not reasonable as the primary basis for deciding whether something is a real bug. Measure it on your own repository withscripts/eval-issue-groundtruth.pybefore turning it on.
Long-running modes never publish and never call a model
issue watchanddaemononly sync, triage locally, and print: no comments, no labels, no closing, and no LLM calls (the model is used once per issue inissue review, purely to phrase the reply). Every outbound write happens throughreviewgate issue review --publish, and each action is off by default.- Per-round cap:
--max-issues-per-run(default 20) limits how many issues one round syncs and triages, so pointing it at a large backlog does not exhaust the platform's API quota in one go. When a round does not drain the backlog the sync cursor does not advance; the remainder is picked up next round, and issues already stored and unchanged are skipped without consuming budget.
Every limitation above has an eval trail, and each lists its current mitigation. This page shrinks as versions progress — the Chinese original is here.