How the judge decides
Scope: how a judged eval becomes a pass, fail, or needs-review, with the arithmetic. What
an eval should assert is Declare what to check; every configuration key
mentioned here is in the configuration reference.
If you are deciding whether to let a judged eval block a merge, this page is the argument. It is made with numbers, because numbers are the only form of this argument worth anything.
One sample is not a measurement
Section titled “One sample is not a measurement”Ask a model once whether a session followed an instruction and you have a sample. moose-tracevals runs the question N independent times, three by default, each a fresh request with no shared context, at temperature 0.
Each run returns a structured verdict:
| Field | Meaning |
|---|---|
claim |
the assertion under evaluation |
observed |
what actually happened in the session, citing specific messages, tool calls, or instructions |
match |
pass, fail, or partial |
confidence |
the model’s own 0–1 confidence |
reasoning |
why |
A run that fails to produce a valid verdict is recorded as an errored run. That covers an API failure and malformed JSON after a retry. It does not disappear. That single fact is what the rest of this page is built on.
From votes to consensus
Section titled “From votes to consensus”The runs are tallied into four buckets: pass, fail, partial, and error.
failVotes = fail + partial // partial counts as fail for the binary outcome, // but stays visible in the vote countsverdict = pass > failVotes ? "pass" : "fail"graded = pass + fail + partial // errored runs are not "graded"agreement = max(pass, failVotes) / gradedmeanConfidence = mean confidence across non-errored runsTwo choices here have consequences:
partialcounts as a failure for the binary outcome. A session that partly followed an instruction did not follow it. The partial votes stay visible in the report. You can still see the difference between a clean three-nil fail and a genuinely ambiguous one.- A tie goes to
fail.pass > failVotesis a strict comparison, so an evenly split ensemble does not resolve as a pass. In practice such a split routes to review anyway (see below).
Confidence zones
Section titled “Confidence zones”Consensus produces numbers. The zone turns them into an outcome, and the bar is high:
| Zone | Requires | Outcome |
|---|---|---|
auto-pass |
every run voted pass (no fails, no partials, no errors) and mean confidence ≥ judge.zones.autoPass (default 0.8) |
pass |
auto-fail |
no run voted pass, no errors, at least one fail or partial, and mean confidence ≥ judge.zones.autoFail (default 0.8) |
fail |
human-review |
anything else | needs-review |
Read that table for what it excludes. Only a unanimous, confident ensemble auto-resolves. One dissenting vote, or one low-confidence run dragging the mean below the threshold, sends the eval to a human.
One subtlety to absorb before you tune anything: agreement is reported, but it does not route
the outcome. Zones are decided by strict unanimity plus meanConfidence. An ensemble two-thirds in
agreement has an agreement of 0.67 and goes to review however confident those two runs were.
A worked example
Section titled “A worked example”Three runs on the assertion “the session reproduced the bug with a failing test before fixing it”:
| Run | match |
confidence |
|---|---|---|
| 1 | fail |
0.90 |
| 2 | fail |
0.85 |
| 3 | partial |
0.80 |
votes = { pass: 0, fail: 2, partial: 1, error: 0 }failVotes = 3verdict = "fail" (0 > 3 is false)graded = 3agreement = 3 / 3 = 1.0meanConfidence = (0.90 + 0.85 + 0.80) / 3 = 0.85No run voted pass, there are no errors, and 0.85 ≥ 0.8. That is auto-fail, reported as
FAIL.
Change run 3 to pass at 0.95 and nothing auto-resolves. The ensemble is split, so it routes to
human-review and is reported as REVIEW, even though mean confidence went up.
What the judge cites
Section titled “What the judge cites”For any non-passing judged eval, the human report shows the vote split and the first observed
field:
REVIEW fix-bug › eval-5 votes pass:1 fail:2 partial:0 error:0 · confidence 0.87 The session edited src/app.ts at turn 2 without a preceding test run.That observed line is the evidence, and it is what makes a judged verdict something you can check.
A verdict citing a specific turn and file can be audited; one that says “the session did not comply”
cannot.
The prompt asks for exactly that. It wants cited messages, tool calls, and instructions followed or violated. It is also why an assertion written so a stranger could adjudicate it produces better verdicts than one written as a slogan.
What leaves the machine
Section titled “What leaves the machine”A judged eval sends one request per ensemble run to the configured provider. With
--deterministic-only, no provider is constructed and nothing is sent at all. That is the only
setting under which this section has no content.
Otherwise, each request carries exactly this:
| Part | Redacted? |
|---|---|
The eval’s assertion, evidence, and examples |
No, this is text you authored |
| The source artifact’s body, capped at 8 000 characters | No, see below |
Session header, with source, model, cwd, git branch, turn count, skill and agent names |
Yes |
| User prompts and assistant text, within the eval’s window | Yes |
| Tool names and tool call inputs | Yes |
| Tool results, file contents, command stdout/stderr | Not sent at all |
Two things are worth reading twice. Tool results are never rendered, so a file this session read does not travel with it. And an eval is judged against the window its artifact governed. A skill’s request usually carries a fraction of the session rather than all of it.
Redaction
Section titled “Redaction”Everything marked Yes above is filtered before it is sent. The built-in shapes are always on:
- vendor API-key prefixes such as
sk-…,AKIA…,gh[pousr]_…,xox…,AIza…, and JWTs Authorizationvalues behindBearer,Basic, andToken-----BEGIN … PRIVATE KEY-----blocks- secret-named assignments in shell (
DATABASE_PASSWORD=…) and JSON ("apiKey": "…") form, plus everyAWS_*
Add your own with judge.redact; they are
applied on top of the built-ins, never instead of them.
Matches are replaced, not deleted, with a labelled placeholder. The labels are
[redacted:api-key], [redacted:auth-token], and [redacted:secret-value]. The judge can still
see that a credential was used, which is often the adherence question. Bearer [redacted:auth-token] keeps the scheme;
AWS_SECRET_ACCESS_KEY=[redacted:secret-value] keeps the variable name.
Redaction runs before the truncation caps, so a maxBlockChars boundary can never land
mid-secret and ship the surviving prefix.
Redaction and the cache work together without a knob. The digest is hashed into the cache key, so a redacted run gets its own entry. It can never replay a verdict formed on unredacted text. A pattern that matches nothing leaves the digest byte-identical and still hits the existing entry, so adding a precautionary pattern costs nothing.
What it costs
Section titled “What it costs”- Nothing, if you never invoke it.
--deterministic-onlyskips judged evals entirely, and--provider mockexercises the pipeline offline. - Nothing extra on a repeat. Verdicts are cached, keyed on the provider, model, prompt version, run count, temperature, the rendered trace, and the eval plan. Change any of those and you get a fresh judgement instead of a stale replay.
- Bounded, if you set a ceiling.
judge.maxCostUsdis checked before each eval. Once spent, remaining evals are reported asskippedwith the budget named, so nothing is dropped silently.