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 can answer 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. Each of those calls is four messages in a fixed order: the instructions as a system message, the evidence as a user message, a constant one-line assistant acknowledgement, and the criterion as a user message. Only the first is a system message, and the roles alternate after it, because some self-hosted chat templates refuse anything else with a 400: templates that accept a system message only in first place (the reported error reads System message must be at the beginning.), and Mistral-family templates on vLLM, which require strict alternation (conversation roles must alternate user/assistant). Either refusal leaves the judge unavailable. A reply that does not parse is retried once, in the same alternating order. A server with some other rule can still refuse the call; xorcise config test sends the same order of roles, so it shows you that before a run does. What the judge is shown is two things from the agent: 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. When the harness’s adapter cannot capture some evidence class, the message also opens with a trusted harness telemetry disclosure ahead of the fence, naming the harness as the agent registered it and the classes that are missing, so the judge does not penalize the agent for them. 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 reproduce the real markers. What the judge does with the text inside the fence is not enforced: the instructions tell it to treat that text as data and to ignore any instructions there, but agent text can still contain brackets that resemble the markers, or instructions aimed at the judge, and whether the judge ignores them is up to the model. The criterion rides the same user role as the evidence, so its place after the closing fence, not its role, is what sets it apart. 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 divides what the scored criteria earned by the total weight of the whole rubric:
A criterion the judge leaves unscored counts as 0 in the score, and its weight stays in the divisor. An agent can never raise its score by making its evidence impossible to assess. Beside the score, the result carries an upper bound that grants all the unscored weight in full, so the gap between the two shows how much of the rubric was never assessed:
The judge’s unknown is accepted as a verdict only when the run has no trace, discloses harness telemetry gaps, or had span bodies truncated. In any other run the criterion scores 0 with a note that it was not demonstrated. A criterion whose reply is invalid after one retry is also unscored. Rubric weights are used exactly as written and need not sum to 1.0, because the division by the total weight rescales them. A criterion that omits weight counts as 1.0, so a rubric whose criteria all omit it splits the judge half equally. Unlike checks, a rubric may mix the two, and nothing warns you: beside a criterion of weight 0.2, one that omits its weight carries five times as much. 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. One of scout’s spans is longer than the judge’s per-span cap, judge_span_max_tokens, so its body is truncated in the evidence the judge reads, and that is one of the conditions under which the judge may answer unknown. All three checks declare weights, so XORCISE uses them as declared:
The judge grades each criterion in its own call:
hygiene is unscored, so it counts as 0 and its weight stays in the divisor. The result records the reason on judge_detail: 1 of 3 criteria were unobservable, and judge_status is partial.
The result also carries the upper bound, which grants hygiene’s 0.3 in full:
So scout scored 75.5%, and the most the unassessed criterion could have added is 90.5%. Had no span been truncated, the judge’s unknown on hygiene would have been rejected: the criterion scores 0 with a note that it was not demonstrated, the judge half is still 0.66, judge_status reads ok, and there is no upper bound above the score. 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 33 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 with grading_failed set, and a judge_detail beginning 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. That zero is a stand-in, not a score: nothing graded the run, so the performance views count it as Grading failed and leave it out of every median. A result recorded before grading_failed existed is recognised by its judge_detail. xorcise mission pull <id> leaves an installed mission as it is, so refresh the installed copy with xorcise mission update <id> for a newer release, or xorcise mission pull <id> --refresh to reinstall it, 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. When the judge did not run, which is model-not-configured or unavailable, the CLI says so beside the score. xorcise run status prints a JUDGE DEGRADED line under the scores with the status and any judge_detail, as do run terminate and run regrade when they wait for the result. xorcise run list marks the score with *. None of them changes the run’s own score, which stays at most 0.50, exactly as the arithmetic above says. What changes is how it counts: half of the score was never measured, so a run with no judge is left out of every median and counted on its own as No judge, in xorcise leaderboard and on every web UI view that shows a median. Track an agent over time sets out the rule. xorcise agent history does not mark these runs, so a 0.00 in its Judge column can mean either a judge that scored zero or one that never ran. In the web UI, a run’s own result page says its judge did not run, the All Runs table and an agent’s run history read No judge in the judge column, and the run strip and run ledger draw the judge’s half as a dashed outline rather than a zero.
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 the configured judge model whenever one is configured, even if the judge call failed, and null only when none is configured, so read it together with judge_status — a degraded judge is the most common reason a comparison is not valid. Intel is provenance for you; grading never reads it. The declared model is only what you typed at agent register --model. Beside it, the result carries models_reported: the model or models the agent’s harness named in its own telemetry, in the order it first named them. Claude Code and OpenHands name the model on their token-usage events, and Codex names it once when its session starts. The generic adapter has no mapping for it: it reads an attribute named exactly model, and only on a span it classifies as a message or an error. Both names are self-reported and XORCISE verifies neither, so it keeps them apart. xorcise run status, the report’s Agent model row and the Conditions card on the web result page show them on one line: Names are compared exactly, so a declared family name beside the full model id the harness reports shows as both. At most eight reported names are kept, and a longer list ends (+N more). A run graded by an earlier release picks up its reported model the first time its result or report is read. The web UI’s result page shows the same line in its Conditions card, and leaves the row out where the report says not disclosed. The leaderboard does not show the reported model, and xorcise agent history shows only the declared one.

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; the models the harness named; and timing, including elapsed seconds, when the first event started and when the last one ended, and the longest single tool call. Cost estimation is not shipped, so it reports nothing. Except elapsed_seconds, which the server takes from the run’s own record, 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. The report sets the elapsed time, as Duration, beside the stretch the telemetry covers, as Telemetry window, and the web UI’s result page shows the two side by side as well; Budgets explains why the two can differ by most of a budget. XORCISE also renders the same result as a single self-contained Markdown or HTML file, which is the shareable form of a run.

Evidence seal

When a run is sealed, XORCISE records a SHA-256 digest of the evidence it grades: every raw trace and log batch, everything recorded over run control (including the artifacts your agent submitted), and the observed facts, plus the run id so a digest cannot be moved to another run. Checking the seal later hashes that evidence again and compares the two. The time XORCISE received each batch is left out on purpose, so a report line built from receipt times can change while the seal still verifies. The result carries the digest as evidence_digest and the outcome as evidence_status. Read the status rather than evidence_verified, which is null in five different situations:
  • verified: the evidence was hashed again on this read, and it still matches.
  • mismatch: it was hashed again, and it does not match. This is the only status that says the evidence changed after sealing. It does not say what changed, or who changed it.
  • recorded: a digest is on file, and this read did not check it.
  • none, unverifiable, unavailable and unreadable give no verdict, and none of them means the evidence changed. The REST reference says what each one means.
Three surfaces check the seal and show you the answer: the run report, every time it is fetched; xorcise run status; and GET /api/runs/<run_id>/result?verify=true. A plain read of the result does not, and neither do run list or the leaderboard, because checking re-reads all of the run’s recorded evidence and costs more as the run grows. The web UI’s results page does not show the seal. The digest covers the evidence, not the result: an edited score, judge reason or condition still reads verified. A run sealed by an earlier release, before XORCISE recorded digests, has none and reads none. Security and isolation sets out what the seal protects against and what it does not.

How a run ends and what gets graded

A run is created from the moment it exists, xorcising once its agent has started, and terminal once it is over. 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. A run that timed out is in the agent’s median at the score it earned, because not finishing within the budget is the agent’s failure. A run an operator stopped is not: its score says nothing about the agent. A cancelled or never_started run is never graded either: the agent did nothing, so there is nothing to score. It is left out of every performance view, not counted as an attempt, and not in the dashboard’s Evaluated count. The deploy_failed and crashed rows are environment failures, and those runs are never graded. The failure is XORCISE’s, not the agent’s, so there is no score to record, not even a 0.00, and no judge call is made. Such a run counts in no median and in no completion or partial rate; the leaderboard and the dashboard show it separately as Infra failed. Its result answers not graded with the reason recorded when it ended, and it cannot be re-evaluated. A score is withheld even when the failure came mid-run, after the agent had made progress. 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. A cancelled, never_started, deploy_failed or crashed run is the exception: it never reaches grading, and its result is a final 200 with the status not_graded, so a poll loop stops on it.

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. If it ended cancelled, never_started, deploy_failed or crashed you get not graded with the reason instead, and that is final: xorcise run status exits 0, and the web UI shows a Not graded panel in place of the scorecard.

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. Before it grades, XORCISE checks the run’s evidence seal. If the evidence no longer matches, it logs a warning and grades the run anyway. A mismatch never stops a re-grade, so read the seal yourself before you trust a re-graded score.
From the terminal, run:
It polls until the fresh grade lands unless you pass --no-wait.
A run that ended cancelled, never_started, deploy_failed or crashed cannot be re-evaluated. It was never graded, and the server refuses with 409 rather than record the score it declined to record the first time. 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.