Skip to content

RFC: give the skeptic executable verbs — 84% of one review's noise was settleable by running a command #401

Description

@zaebee

The observation

A guardian review of #399 produced 19 findings, of which 1 had substance. The other 18 were not near-misses — six were the same false claim repeated, and two recommended changes that would have removed deliberate guards.

The single most common failure was a claim about a rule that the repository does not have and a checker that does not look at the file:

"The use of Any is prohibited in strict mypy mode" — ×6

$ uv run mypy --strict /tmp/anytest.py     # def f(x: dict[str, Any]) -> Any
Success: no issues found in 1 source file

pyproject.toml:230  exclude = [..., "scripts"]      # the file under review
Makefile:14         uv run mypy src                 # scripts/ is never checked

One command settles it. The finder did not run it, and the skeptic — gemini-2.5-flash, same vendor as the finder — confirmed all six.

The measurement

Classifying all 19 by "would executing something have settled this?":

tier what it needs findings share
static (mypy, ruff, grep) — does not execute PR code 9 47%
running code (pytest) 7 37%
cumulative settleable by execution 16 84%
beyond any tool 3 16%

Of the 16, execution kills 15 and confirms 1 — the one real finding (git diff with check=True raised CalledProcessError where the module promised MissingFixtureError; fixed in #399).

Where execution does not reach, and why that matters

Three findings survive any tool, and two of them share a shape worth naming: they proposed relaxing a >= 72 ratchet floor and an ambiguous_hits invariant "to reflect the actual number". The code is correct and the tests pass — the model was not wrong about behaviour, it was wrong about intent. It saw a rigid assertion and offered to soften it, not knowing that rigidity was the entire point.

No sandbox fixes that. It is an argument for keeping the ratchets' reasoning in the docstring where a reviewer reads it, not for a better tool.

The runtime question is already answered

The guardian review job checks out refs/pull/N/head at fetch-depth: 0 and runs uv sync --frozen --extra guardian. The repository and the toolchain are already there — the guardian process is running inside them. The model needs no sandbox of its own; the guardian executes an allowlisted verb on its behalf and feeds the output back.

The security crux is not "can it run", it is what running costs:

verb executes PR code? risk
mypy --strict <path> no low — static
ruff check <path> no low — static
grep/read within the repo no low
pytest <node-id> yes the job holds pull-requests: write and issues: write

The job is gated on a commenter with write access, so the threat model is "a maintainer runs the guardian on a hostile PR" — which is precisely the review scenario, not an edge case.

Proposal, tiered so the cheap half ships first

v1 — static verbs only. A fixed allowlist, not a shell: typecheck(path), lint(path), grep(pattern, path). Arguments constrained to repo-relative paths; output truncated before it re-enters the prompt. Buys 47% of today's noise at essentially no new risk, because none of these executes the code under review.

v2 — pytest behind a boundary. Reaches 84%, and needs its own answer to the token question: a separate job without pull-requests: write, or network-off, or both. Not in v1.

Where it belongs: the skeptic, not the finder. The skeptic already runs once per finding and its job is refutation. "Show the command whose output supports this claim, or withdraw it" is a natural strengthening of a prompt that today asks only for an opinion. A finder with tools would be slower and would still emit whatever it emits.

This is measurable now, and cheaply

#399 rebuilt 72 frozen finder passes from the corpus at zero model cost. A tool-using skeptic is therefore a replay arm: the same finder output, judged by two skeptics, no finder tokens. The comparison is recall and noise against the current skeptic — the same shape #246 registers.

The one existing matched data point already says the skeptic is the right place to intervene: on pr-143 with the finder frozen at 28 findings, a same-vendor skeptic refuted 0 and a cross-vendor one refuted 7 while recall stayed at 1.00.

Cost not yet counted

No provider here supports tool calling. BaseProvider exposes generate_content and generate_structured; there are zero occurrences of tools=, function_declarations or tool_choice across gemini.py, mistral.py and base.py. A tool loop is new surface on every provider, and it lands in providers/, which is inside the review-fingerprint closure — so it re-mints every reviewer identity (by design, #375).

Open questions

  1. Does a tool-using skeptic actually refute more, or does it confirm with a citation? Today's run confirmed six false claims without checking any of them; the failure may be disposition rather than capability.
  2. Call budget. The skeptic is per-finding; a tool loop multiplies that. On a 28-finding review at 2 calls each, that is 56 round trips.
  3. Is grep enough for the 9 static findings, or is mypy doing the work? Six of the nine are the mypy claim alone.

Related: #246 (cross-model skeptic — same intervention point), #155 (ensemble dedup — six of today's 19 were one finding repeated, so dedup alone cuts ~26% with no tools at all), #248 (finder robustness).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions