# Honest checks and an LLM judge to trust

By [Anubhav Choubey](https://anubhavchoubey.com/) · AI & Agents · 2026-11-04 · 4 min read

> A check approved a page it never loaded. I wrote a contract for what verification tools may claim, then built an eval with an LLM judge behind a gate.

```stats
honesty contract for tools · evidence from raw traces · LLM judge from another model family · agreement gate
```

## The problem

An AI builder edits a store and then checks its own work with a visual check: render the page, look at it, give a verdict. The problem fit in one sentence: the check said everything looked good while the page was broken.

Replaying the session, the agent had asked to check the checkout page. With an empty cart, the store redirects checkout to the cart page. The tool rendered the cart, then reported the path it was **asked for**, a 200 status and a verdict of "production-quality". It approved a page it never opened.

That is not one bug. It is a class: a verification tool describing itself inaccurately. The agent trusts the tool, the user trusts the agent, and nobody sees the gap.

## Tools may only report what they measured

I treated the fix as a design rule, not a patch. A tool's output is a claim about the world, and a tool must not make a claim it cannot support. The visual check may report the URL it landed on, the status it got and the verdict of a model that saw the actual page. Nothing else.

Applying that rule turned up more of the same class:

- **The judge saw a crop.** The screenshot was one viewport, but the model was asked about the whole page. Now it sees the full page.
- **200 is not success.** A server error halfway through rendering still returns 200 and a page that just stops. Checks now assert what the page type needs, like an add-to-cart button on a product page, at more than one width.
- **The eyes looked at the wrong tree.** Some tools rendered a shared base copy instead of the user's own working tree, so the agent was checking a page that could not contain its own work.
- **Logged out is not "no access".** The renderer browsed anonymously, hit a login wall on admin pages, and the agent told the user it had no access to their own store.
- **One lucky sample is not a verdict.** The vision model could not be made deterministic, so the tool reports the worst verdict across renders, not the latest.

## A written contract

The first fix overcorrected. It started attaching "this check did not run" to results where the check clearly had run. A new false statement, in the opposite direction.

Both mistakes had passed green CI and adversarial code review. A human driving one real turn by hand on a test store caught them. Reviews check whether code does what the PR says. Nothing was checking whether what the PR says is true.

So I wrote down what each verification tool may say about itself, as a contract with tests that hold it, run as a smoke gate. The same spirit applies to failures: when the agent's tool access failed to authenticate, it used to silently carry on with half its tools. Now it fails loudly.

```callout warn
A tool that quietly reports success is worse than one that crashes. Make every check able to say "I don't know", and make that answer cheap to give.
```

## From one turn to the whole builder

Honest tools answer "is this one turn right?" The next question was "is the builder getting better?" I built an evaluation programme in five phases:

1. **A flat fact table of turns.** Outcomes, tool calls and errors were buried in a JSON blob. Now they can be grouped and counted. It stores lengths, never prompt or response text.
2. **An API for an outside runner.** The eval runs in CI, outside the cluster. Task rubrics live outside every repo, so the agent can never see what it is graded against.
3. **Evidence from raw traces.** The stored transcript was lossy: missing calls, empty bodies, no timings. Grading reads the raw execution trace instead. A call whose transport says success but whose body says `ok: false` counts as a failure.
4. **An LLM judge with an agreement gate.** The judge comes from a different model family than the builder, to avoid self-preference. A judge is itself an unverified verification tool, so its verdicts only count after it agrees with human labels on enough tasks. Below that, the gate fails closed and the judge stays opt-in.
5. **A results dashboard.** Runs and tasks are stored and comparable, and a run that bypassed the gate is marked as such.

```text
task suite ──▶ builder runs ──▶ raw traces ──▶ checks + LLM judge ──▶ results
                                                    ▲
                              human labels ──▶ agreement gate
```

One small rule mattered a lot: a judging failure is `infra_error`, never `fail`. An expired API key should not look like the agent got worse overnight.

## What I'd do differently

- **Write the contract before the tools.** I wrote it after the bugs. It would have been cheaper as a spec.
- **Collect human labels from day one.** The judge could only be trusted once enough tasks had human labels to compare against. Starting that labelling earlier would have opened the gate sooner.
- **Give suppressions an owner.** A known-issue entry with an expiry date kept hiding a fixed bug until it turned the smoke gate red.

## Takeaway

Verification is only useful if it is honest about what it saw. Make tools report only what they measured, put that in a contract with tests, and treat an LLM judge like any other unproven tool: measure it against humans before you trust it. The measuring side of this started with [Figma to code by measuring](/blog/figma-to-code-by-measuring).
