Skip to main content
XORCISE scores every run out of two equal halves: deterministic checks it computes itself, and a judge — a model that reads the mission’s rubric and the record of what your agent did. The split is fixed at 50/50 and nothing you can configure changes it.

What you’ll learn

  • How XORCISE turns one run into one number, and why that number caps at 50% without a judge.
  • What each half reads, what it cannot see, and how weights are applied inside it.
  • How to read judge_status and recover a score that was lost to configuration, without running the agent again.

The two halves

Grading runs over the sealed evidence of a finished run: the artifacts your agent submitted, the frozen OpenTelemetry trace, and the facts XORCISE recorded about the run itself. Each half produces a sub-score between 0 and 1, and the overall score is their average:
The two coefficients are fixed in XORCISE’s grading code. A mission author controls the weights inside each half — per check, per rubric criterion — never the ratio between them. With no judge configured, the highest score any run can reach is 50%. The judge half scores 0.0, that zero is still multiplied by 0.5 and added, and the deterministic half is not rescaled to compensate. A run that passes every single check reads as 50%. A reader who sees 42.5% on an unjudged run is not looking at a failed agent — they are looking at half a score. The same ceiling applies in the other direction: a mission that ships only checks and no rubric, or only a rubric and no checks, caps at 50% no matter how well the agent performs.

Why the flag alone is not enough

A flag check answers one question: did the agent end up holding the right string. It cannot tell a lucky guess from a methodical compromise, it cannot see whether the agent enumerated the target before attacking it or trampled the box on the way through, and it has nothing to say about a mission with no single correct answer. That is the half of agent evaluation people actually argue about, and it is why XORCISE spends half the score on a rubric.

When the judge is the wrong tool

The judge is not objective. It is a model reading your mission’s rubric, and it inherits every weakness that implies: it grades only what the evidence shows, it answers unknown when the transcript is thin, and two providers will not agree to the decimal. Anything you can assert exactly — a flag, an artifact’s format, a turn count — belongs in checks, where it is free, repeatable, and immune to prompt length. Put in the rubric only what needs judgement.

The deterministic half

A check is a declaration in the mission that XORCISE evaluates itself. Every check is two decisions kept deliberately apart: a source and a ref say where to fetch a value from, and an op and its args say what must be true of it. XORCISE resolves the value, applies the operation, and records a verdict. There are three sources — the agent’s submitted artifacts, statistics projected from the sealed trace, and the observed facts XORCISE recorded about the run — and four operations, covering exact equality, whole-string regex match, presence, and a numeric less-than. ref is the lookup key into the source, not the expected value; the value a check compares against lives in args. The complete vocabulary, with the exact strings and the pass and fail semantics of each operation, is on Checks and ops. A missing ref resolves to nothing rather than raising, so the operation decides the verdict. That makes an unsubmitted artifact a clean failure instead of a crash. Weights follow one rule with no middle ground:
  • Every check declares a weight, each greater than 0 and at most 1, and they sum to 1.0. XORCISE uses them as declared.
  • Or no check declares a weight, and each gets 1/n.
  • Mixing the two is a manifest error, rejected when the mission manifest is validated.
  • A mission with no checks at all scores 0.0 on this half, which caps the run at 50%.
The deterministic sub-score is the sum of the weights of the checks that passed. The verdict for each check carries its effective weight, so an equal split shows as a real number rather than a blank. A check can never crash grading. If the operation is unknown, or the resolver or operation raises, the verdict is recorded as failed with an error explaining why. Those checks consume their weight as failures, not as skips — so a check with a non-empty error is a mission-authoring bug, and it is the first column to read when a score looks unfair.

The judge half

The judge is bring-your-own-model: XORCISE ships no model and calls the OpenAI-compatible endpoint and key you configure. Each criterion in the mission’s rubric — an id, the text of the behaviour to assess, and a weight — gets its own isolated model call, so one criterion’s assessment cannot bleed into another’s. The instructions and the evidence are byte-identical across those calls, so a provider that supports prompt caching reuses the prefix. What the judge is shown is exactly two things: every artifact the agent submitted, as name and value, and a distilled transcript of the agent’s tool actions and their outputs, merged in time order from the trace and log signals your harness exported. When no trace was sealed, the prompt says so explicitly. What the judge is not shown matters as much: it never sees the deterministic check results, the mission objective, the observed facts, the terrain map, or the run’s statistics. All agent-controlled text is fenced as untrusted and stripped of the markers that delimit the fence, so agent output cannot forge a boundary or issue instructions to the judge. For each criterion the judge returns either a score between 0 and 1 with a one-line reason, or the verdict unknown with an explanation of why the evidence was insufficient. The sub-score is the weighted average over the criteria it could grade:
Criteria that come back unknown are renormalised out, not scored zero. Thin evidence must not be charged to the agent as a failure. The consequence is worth knowing: if every criterion comes back unknown, the divisor is 0 and the judge half is 0.0 while judge_status still reads ok. Rubric weights are used exactly as written and need not sum to 1.0, because the division renormalises them — but there is no equal-split fallback the way there is for checks, so a rubric whose criteria all omit weight also produces a judge score of exactly 0.0 reported as ok. XORCISE preserves judge_prompt on the result: the shared instructions and evidence block every criterion call received. When a judge score surprises you, that field is the ground truth for what the judge actually read.

A worked example

One run of sqli-login by the agent scout. The mission declares three checks and three rubric criteria:
During the run scout submits flag and notes, never submits creds, and completes the run. A judge model is configured. All three checks declare weights, so XORCISE uses them as declared:
The judge grades each criterion in its own call:
hygiene dropped out of the average rather than scoring zero. Had it counted as a zero, the judge half would have been 0.66 instead of 0.9429 — a swing of 0.14 on the overall. The result records the reason on judge_detail: 1 of 3 criteria had insufficient evidence (unknown).
Now take the identical run with no judge model configured. The judge half never runs, so it contributes 0.0:
scout did exactly the same work in both runs. The 47 points between them are the judge’s absence, nothing else — which is why conditions.judge_model and judge_status are the fields to read before comparing any two scores.

When the judge half is missing

Every result carries judge_status, and judge_detail alongside it. Read those two before you read breakdown.judge. Two more shapes are worth recognising. An overall score of 0.0 whose judge_detail begins grading failed: is XORCISE’s defensive fallback: grading itself raised, usually against a mission installed before a newer validation rule, so the run records a zero rather than wedging forever. Refresh the installed copy with xorcise mission pull <id>, then re-evaluate. And spans_truncated above 0 means the judge read abridged span bodies, which is worth knowing before you trust a low judge score on a run with enormous tool output. In every one of these cases the fix is to re-evaluate, never to run the agent again. The evidence is already sealed and re-grading replays it against your current settings.
A XORCISE run result showing the overall score ring, the deterministic and judge halves as separate bars, and the check and criteria counts

A graded result — a different run from the worked example above. The scorecard always separates the two halves, so you can see which one carried the score.

What a result contains

Each judge criterion is a self-contained snapshot: its text and weight are copied from the rubric as it was graded, so a later version of the mission does not rewrite an old result.

Conditions

The result’s conditions record the context a score was produced under: the agent’s disclosed model, the judge_model actually used, the run’s budget_seconds, the sandbox_ref of the mission image, the agent_version and mission_version snapshotted when the run was created, and how much intel the run received. Two scores are comparable only when these agree — and judge_model is null whenever the judge was unconfigured or degraded, which is the most common reason a comparison is not valid. Intel is provenance for you; grading never reads it.

Statistics

Token and timing statistics live on their own endpoint rather than on the grade: input, output, cache and reasoning tokens with a computed total; counts of model calls, tool calls, findings and errors; and timing, including elapsed seconds and the longest single tool call. Cost estimation is not shipped, so it reports nothing. These numbers are reported by the agent’s own harness, so they are display and comparison data only — never an observed fact, and never a grading input. XORCISE also renders the same result as a single self-contained Markdown or HTML file, which is the shareable form of a run.

How a run ends and what gets graded

A run persists exactly two states: created from the moment it exists, and terminal once it is over. There is no separate running state on the wire. What distinguishes one ending from another is terminal_trigger: A partial result is still graded normally over whatever evidence exists — the scores are real and reflect only the work that was finished, and only the agent’s own completion counts as a genuine result against it. Note the asymmetry in the last row: a deploy_failed run records a normal-looking, near-zero result that is not flagged partial, so check the trigger before concluding the agent performed badly. Sealing and grading are deliberately split. Completion returns immediately; XORCISE then waits a short drain window for the agent’s last spans to arrive, freezes the trace, grades both halves, folds the statistics, and tears the environment down.
While that runs, asking for the result returns 202 with the status grading — normally for a few seconds, longer when the judge is working. That is not a hang. Polling the result also re-drives grading if it was lost to a restart, and XORCISE sweeps any terminal-but-ungraded run once on each start.

What grading costs

The deterministic half is free and local. The judge half calls your model provider once per rubric criterion, and the evidence prefix those calls share is identical, so a provider with prompt caching charges the bulk of it once. Cost scales with transcript size, not run count. Two settings bound that size. A per-span cap, judge_span_max_tokens, defaults to 2000 and trims each span to a head-and-tail window with a marker in between — every span survives, so no criterion loses an action. A pre-flight ceiling, judge_transcript_max_tokens, defaults to disabled: the local token count is an estimate, so XORCISE prefers to let the call go through and surface the provider’s real error verbatim rather than reject a prompt that would have fit.
A long lab run can genuinely exceed a judge model’s context window — a measured 10-minute run produced a 369,000-token prompt against a 272,000-token ceiling. When that happens the judge half is unreachable and the score degrades to deterministic-only, capped at 50%. Lower judge_span_max_tokens, or move to a larger-context judge model, then re-evaluate.
That is the cost of grading one run. A playbook benchmark grades every cell of a model × mission × runs matrix, so a 150-run playbook means 150 gradings — each costing what this section describes — on top of 150 agent sessions.

Read a result

Both interfaces show the same graded result. Replace <run_id> with the id XORCISE printed when the run was created.
Add --verbose for the per-check and per-criterion breakdown, or --json for the whole result as a document you can script against. Both flags are documented in the CLI reference.
Either way you end at the same numbers. If the run is still being graded you get the grading status instead of a score — wait a few seconds and ask again.

Re-evaluate a run

Re-grading replays the run’s already-sealed evidence — the same artifacts, the same frozen trace, the same observed facts — against your current settings. It does not re-run the agent and it cannot change what the agent did. This is the fix for a judge that was unconfigured, rejected, or out of context when the run first ended.
From the terminal, run:
It polls until the fresh grade lands unless you pass --no-wait.
One caveat: re-grading re-reads the currently installed mission, so if the installed copy has changed — you pulled a newer version of the mission whose rubric was edited — old runs grade against the new rubric. Compare mission_version in the conditions before treating two scores as the same measurement.

Next steps

Configuration

Set the judge model that produces half of every score.