Rubric review is a detached, agentic quality review of finished rollouts. A
reviewer agent reads a rollout's records — trajectory, result, verifier
output, and the task definition — inside its own sandbox and grades the run
against a rubric, one pass / fail / not_applicable verdict plus an
explanation per criterion.
Review is report-only. It runs after a job is over, from the host-side
rollout directories, and writes review_report.json. It never modifies a
reviewed rollout's rewards or result.json, and there is no code path
through which it could: the deterministic verifier is the only owner of
reward.
This is distinct from the llm-judge verifier strategy:
an llm-judge is part of a task's verifier and produces the reward, while
rubric review is downstream quality assurance about finished runs — is the
task well specified, did the agent game the grader, was the method sound.
The rubric (contract v0.1)
A rubric is a JSON file with one list:
{
"criteria": [
{
"name": "reward_hacking",
"description": "Author-facing note about why this criterion exists.",
"guidance": "Study the recorded run under trial/ for signs the agent manipulated grading instead of doing the work. PASS when ... FAIL when ... NOT_APPLICABLE when ..."
}
]
}
Each criterion is exactly three strings:
| Field | Purpose |
|---|---|
name | Stable identifier. Becomes a field in the reviewer's structured-output schema, so it must be a valid Python identifier. |
description | Documentation for humans reading the rubric. Never included in the reviewer prompt — grading must not depend on it. |
guidance | The grading contract the reviewer follows. Put the full pass/fail/not-applicable conditions here. |
There are no weights, gates, thresholds, or aggregate scores. Consumers read per-criterion outcomes from the report and apply their own policy.
The contract is named v0.1; the document itself carries no version key
— a rubric is exactly its criteria list.
A rubric must contain at least one criterion, names must be unique, and
unknown fields are rejected. (Validation is stricter than the shape alone
requires: rubrics that would produce vacuous or ambiguous reviews are
refused. Every rubric that passes is exactly the v0.1 shape.) rubric.json is an overloaded filename —
llm-judge verifier rubrics use {id, match_criteria} entries. Discovery is
fail-closed: a rubric.json is treated as a review rubric — and validated
loudly — unless every entry carries the full judge shape (both id
and match_criteria). Unreadable files, invalid JSON, empty or missing
criteria, and misspelled keys are all claimed and rejected with an
explicit error rather than silently replaced by the default rubric.
Rubric resolution order, per reviewed rollout:
- an explicit
--rubric/-rfile, - the reviewed task's own
verifier/rubric.json(ortests/rubric.json) when it is shaped like a review rubric, - the built-in default rubric (
reward_hacking,task_specification).
Running a review
# review one rollout locally with a Codex subscription
bench review jobs/<job>/<rollout> --sandbox docker \
--agent codex --model gpt-5.5 --tasks-root ./tasks
# review a small job locally, two rollouts at a time
bench review jobs/<job> --sandbox docker -n 2 \
--agent codex --model gpt-5.5 --tasks-root ./tasks
# audit the winners for grader manipulation
bench review jobs/<job> --passing \
--agent codex --model gpt-5.5
# analyze the losers for specification gaps
bench review jobs/<job> --failing -r spec-rubric.json \
--agent codex --model gpt-5.5
--passing selects rollouts with reward 1.0 and no recorded error;
--failing selects everything else, including rollouts whose result.json
is unreadable. The reviewer agent (--agent, default opencode) and model
(--model; agents without a registry default require one) are independent of
whatever ran the original job. Docker is the default and is appropriate for
one review or a small local job; choose a remote sandbox only when you need
more isolated parallel reviewers.
How a review executes
Each review is an ordinary rollout of a throwaway wrapper task assembled on
the host, which is why every sandbox backend (docker, daytona,
agentcore, ...) works unchanged:
-
Prebuilt image, pinned by digest. The wrapper declares a digest-pinned
pythonimage and ships no Dockerfile, so Docker and Daytona never build one. AgentCore is the exception: it must wrap any image with its runtime-contract shim, so it still builds and pushes a derived ECR image once per distinct image, then reuses it. -
Evidence by upload, outside the workdir. A copy of the rollout directory is uploaded to
/evidence/trial. A task copy is uploaded to/evidence/taskonly when it is admitted through the trusted-root and digest checks below./evidencesits outside the agent workdir; after all uploads, a pre-agent hook fails closed unless the whole tree can be made root-owned, readable, and non-writable by the reviewer. Prior review outputs are excluded from the copy, so a re-review can never read an earlier verdict; symlinks anywhere in the evidence are dropped, never dereferenced; task skills and any shippedrubric.jsonare excluded from the task copy. The canonical ACP trajectory is retained. When an ACP implementation drops a completed tool observation or reduces a command title to the generic tool name, BenchFlow reconciles the missing detail from the matching exact-ID event in its trusted provider capture before the canonical record is finalized. The cumulative provider-historyllm_trajectory.jsonlremains omitted: it repeats the growing conversation on every request and can exhaust a reviewer model's context. The reviewed rollout itself is never touched. -
Post-initialization egress restriction, fail closed. The wrapper declares
allow_internet: false, which disables web tools, forces the model proxy sandbox-local, and arms the agent-UID egress firewall scoped to that loopback gateway. Backends that cannot enforce isolation (for exampleagentcore, whose runtime only offers PUBLIC/VPC networking) refuse the review at launch;--allow-open-networkis the explicit, report-recorded override for them.Be precise about what this guarantees: the container needs network during image setup and agent installation, so the firewall is armed after the reviewer harness starts and completes ACP initialization. The guarantee is restricted egress for the graded portion of the run, not network isolation for the container's whole lifetime. Evidence is uploaded during sandbox setup, before the firewall is enforced, so a reviewer harness that is itself malicious could egress during startup after evidence is present. Treat the reviewer harness as trusted code; the untrusted input is the evidence it reads, and this guarantee constrains the graded portion of the run, not a hostile harness.
-
Task evidence requires an explicitly trusted root. A rollout's recorded
task_pathis rollout-authored data, so it is never read directly — pass--tasks-root <dir>and the task is looked up by name beneath that root. Without it, the review proceeds from run records alone and says so in the trial'snotes. When the rollout recorded atask_digestinresult.jsonorconfig.json, the values must be valid and mutually consistent. A missing digest, mismatch against the on-disk task, conflict, or any verification failure excludes the task from evidence and says so innotes; an old or unverifiable rollout is never reviewed against current task content. -
The rubric never enters the sandbox. It is decomposed host-side:
guidancelines render into the instruction, criterion names become the output schema andtests/criteria.json.descriptiongoes nowhere. -
Validity-only reward. The wrapper's verifier is a stdlib-only structural check of the reviewer's
review-result.json(every criterion answered, outcomes in vocabulary, non-empty explanations). Reward 1.0 means "a well-formed review exists" — never "the reviewed run was good". -
Failure isolation. A review that crashes or produces malformed output becomes an error entry for that rollout; the rest of the job continues.
Output
The review job directory contains review_report.json:
{
"path": "…/jobs/2026-08-03__12-00-00",
"rubric": {"path": "…", "criteria": ["…"]},
"reviewer": {"agent": "opencode", "model": "gemini/gemini-2.5-flash", "environment": "docker", "network": "no-internet"},
"job_summary": "Deterministic aggregation over VALID reviews only.",
"trials": [
{
"trial_name": "hello-world-task__829cddb8",
"source_rollout": "…",
"review_valid": true,
"summary": "Three-to-five sentence account of the run.",
"checks": {
"reward_hacking": {"explanation": "…", "outcome": "pass"},
"task_specification": {"explanation": "…", "outcome": "fail"}
},
"error": null,
"reviewer_rollout": "…/runtime/hello-world-task__829cddb8/<run-id>/…",
"rubric_path": "…/verifier/rubric.json",
"criteria": ["reward_hacking", "task_specification"],
"notes": ["task evidence skipped: no --tasks-root was given"]
}
]
}
Each reviewer rollout's own records (trajectory, verifier output, raw
review-result.json) sit under the report's runtime/ directory for
audit; every invocation uses a fresh unique runtime leaf. When a leaf is
successfully identified, reviewer_rollout points at that exact leaf;
otherwise it is null rather than an ambiguous parent directory. Reusing
--out-dir can therefore never resurface a stale review. The job summary is a deterministic
aggregation, not a model call — a host-side LLM call would bypass the
sandbox backend, egress policy, and telemetry.
Writing good criteria
- Put the entire decision rule in
guidance, including when to answernot_applicable(for example: infrastructure failure before the agent ever attempted the task). - One judgment per criterion. A criterion that bundles several claims makes
failambiguous. - The reviewer reads evidence produced by the solver. Guidance should direct
it to concrete records (
trial/result.json,trial/trajectory/,trial/verifier/) rather than to intent. descriptionis the right place for authorship context you do not want influencing the judge — provenance, rationale, links.