Mahal Systems

Overview › Reference1.0.1

The aot-evals method

This is the reference for what aot-evals does and why: the principles, the measurement frame, the files it keeps in your repository, the commands C0–C19 and their exit gates, the measurements (M), experiments (E), report panels (P) and refusal gates (G) that the skills and the CLI name. The skills read the section they need; you can too.

Who it is for. The method is designed for production, user-facing agents on any stack: their work is specific to the product, so no public benchmark measures it; their volume makes a point of correctness, cost or latency worth measuring; and their output reaches users without an expert checking it first. Any agent that can be run from a command can use it. The agent is invoked as a command, so framework, language and model provider do not matter.

What it is not. Not trace collection (it reads exports of what your observability already records), not a model gateway, not a hosted dashboard and not a leaderboard. There is no service and no account. Everything it writes is text, diffable and safe to commit; raw traces and PII never enter git. Every reported number carries n, k and an interval, or states why it cannot have one. When a number cannot be defended, aot-evals refuses to print it and names the gate.

1. Principles

  1. Cases come from the agent’s real traffic and real failures, not from generic benchmarks. Public benchmarks are a source of grading designs, not of cases. Assess matches the agent to a local catalog of them (aot-evals benchmarks, references/benchmarks.md) without searching; Build may search for newer ones. What the catalog does contribute is the situations (references/situations.yaml): the conditions a request can put an agent in (unanswerable, ambiguous, forbidden, confidential, multi-turn, tool error, …) that benchmarks were built to test and that an agent’s own task types never name. The discovery report lays the agent’s task types (rows, from its traffic) against the situations (columns) as a grid of cases that exist against targets; in Build each situation is a taxonomy entry, so coverage (C14) reports the grid’s empty cells.
  2. Understand the agent before measuring it: inventory, infrastructure, volume and usage first.
  3. Grade outcomes, with code first. Check what changed in the world. Use a judge only where code cannot decide, and only after calibrating it.
  4. Cheap sources first, raw transcripts second.
  5. Keep sensitive data out of version control. Raw traces stay local and ignored; only reviewed, scrubbed cases are committed.
  6. Record every claim as confirmed or inferred, with its source. When config and runtime disagree, trust the traces.
  7. Three planes, reported separately, never combined into one score (§2.1): correctness, operational cost, and trust in the instruments.
  8. Two case sets and two regimes (§2.2, §2.3). A set built to find errors and a set built to estimate quality are different sets. A test that can be re-run and a signal that can only be observed in production are different instruments. Every reported number states which set and regime it came from.
  9. Measure the suite itself (§2.4): coverage against a list you did not derive from your own cases, and redundancy by whether cases distinguish versions of the agent.

The step-by-step process behind principles 1–6 is in references/process/.

2. Measurement frame

2.1 Three planes

PlaneQuestionInstrumentReported as
A. CorrectnessDid the agent do the right thing?code state-diff checks; a calibrated binary judge where code cannot decidepass rate per category with CI; pass^k
B. OperationalWhat did it cost?counters, not judgements: $ per run and per successful task, p50/p90 latency, input/output/cached tokens, cache-hit rate, tool calls, retries, errors and timeouts by outcome code, runs killed at a limitvalues against per-category budgets, across runs
C. Instrument trustCan plane A be believed?judge TPR/TNR against human labels; judge self-consistency and cross-family agreement; case-validity screensits own report panel, with reliability and validity in separate column groups

Plane B is deterministic. It is recorded on every trial, pass or fail, and needs no ground truth, judge or labels, so it is the first thing to instrument. Operational failures (timeouts, rate limits, crashes) are plane-B data: never scored as correctness failures and never dropped.

  • An operational regression is a finding even when correctness is unchanged. Cost per successful task doubling at the same pass rate is a failed comparison (verdict worse). A common cause is a prompt-prefix edit that invalidates the prompt cache.
  • A correctness gain that comes with a cost or latency regression beyond budget is a trade, reported with both numbers, not as an improvement (G11).

2.2 Two regimes: offline and online

Offline (benchmark)Online (operational)
Subjectfrozen cases, in a sandbox or by replaylive traffic
Re-runnableyes: same inputs, k timesno
Ground truththe case’s expected end statenone; needs a referent such as human labels or user behaviour
Gradingstate diff, then a narrow judgea judge on a sample; behavioural signals; fingerprints on a control chart
AnswersDid this change make the agent better?Is it getting worse, and where?
Cannot answerwhether the frozen set still resembles productionwhich of two candidates is better

Neither substitutes for the other. Offline cases may come from your observability stack, but are always imported as a local export (C0); the suite runs with that stack switched off. aot-evals automates the offline regime today; the online regime (C16, C17) is guidance only.

2.3 Two offline case sets

  • The error-finding set is enriched with hard, ambiguous and rare-but-costly cases. It finds bugs and verifies fixes. Its pass rate says nothing about performance on real traffic.
  • The quality-estimate set is sampled in proportion to each category’s observed share of traffic. Its weighted pass rate is the only number the report may call “quality”.

The two are disjoint, sized by different arithmetic (C5), and every reported number is labelled with its set.

2.4 Coverage and redundancy

Coverage. Coverage measured against categories derived from the suite’s own cases is always 100%, so it is measured against a list written for some other purpose: the product spec, the API or tool surface, a help-centre topic list, a job-task taxonomy, a regulator’s list of duties. The report counts cases per entry on that list, crossed with failure modes, and lists the empty cells first. A taxonomy entry may declare failure modes not_applicable, with a reason: those cells leave the empty count but stay visible.

Redundancy. A case that every version of the agent passes, or every version fails, cannot tell you which version is better. Over two or more versions, each case is always-pass, always-fail or discriminating; the report gives the discriminating fraction. Always-pass cases still work as regression guards; they just contribute nothing to a comparison. The hardest cases change after a prompt rewrite, so the discriminating set is re-derived after every prompt change.

3. Repository contract

Each agent’s suite lives in evals/agents/<agent>/, beside its discovery report. A repository with one agent may keep the suite in evals/ itself. Pass --agent NAME to the CLI when there are several. Suites made before that layout live in evals/<agent>/; they are read as they are, and aot-evals migrate moves them.

evals/agents/<agent>/
  discovery.md               # Assess: the discovery report
  agent.yaml                 # identity, triggers, tools, model per call site, stable window,
                             #   and how to invoke it (references/agent-contract.md)
  taxonomy.yaml              # the coverage list (§2.4): entries, the source document, the date read
  categories.yaml            # trace categories: definition, worth, simple flag, cost matrix, observed share
  failure_modes.yaml         # per failure mode: >=1 check, >=1 category, a cost-matrix cell
  budgets.yaml               # plane B: cost / latency / token / error-rate budgets per category
  allocation.yaml            # C5's plan: sizes, and each unfilled cell scheduled or out of scope
  cases/<category>/<case-id>.json   # frozen inputs, start state, trigger, expected end state, set, provenance,
                                    #   requirements (what a correct run must do, each with its checks)
  labels/standard.md         # the labelling standard: rules a person applies, with the categories each applies to
  labels/runs.jsonl          # C19: whole-run human labels, one line per (run, labeller); no agent output
  labels/rulings.jsonl       # §4.2: the verdict on a run its labellers disagreed on
  labels/proposals.yaml      # §4.2: rules proposed from rulings, accepted or declined by the owner
  labels/drafts/             # §4.2: cases a grader drafted, waiting for an engineer
  labels/triage.yaml         # C19: a cause for each disagreement between the suite and a person
  labels/floors.yaml         # optional: C19's floors (min_catch, min_clean, min_runs, min_each)
  checks/
    code/<check-id>.py              # binary state diff: check(start, end, case) -> (bool, detail)
    judge/<check-id>.md             # one failure mode, binary, typed; backend-agnostic (§5)
    judge/<check-id>.labels.jsonl   # human labels, each with the exact judge input labelled
    judge/<check-id>.judgements.jsonl   # cached judge calls, so re-measuring costs nothing
    judge/<check-id>.calibration.json   # C10's measurements
  env/                       # fakes, cassettes, seeds, frozen-clock config
  data/                      # ignored: exports, labelling pages; only export.manifest.json is committed
  runs/<run-id>/manifest.json       # the condition record
  runs/<run-id>/outcomes.jsonl      # one line per attempt and per check verdict
  report/report.json                # §8
  report/agreement.json             # C19: catch and clean pass rates on whole runs, dev and holdout, per category
  report/report-<date>.html         # §9
  report/checkpoints/               # §9: one page per owner checkpoint
  journal/                          # §9: the activity log; ignores itself
  .gitignore

Rules:

  • Paths start at the repository root, the folder that holds evals/: invoke.cwd in agent.yaml and a command judge’s cwd.
  • outcomes.jsonl is never aggregated at write time. It has two kinds of line: kind: attempt, one per (case, trial, attempt), with the outcome code and plane-B counters; and kind: check, one per (case, check, trial, attempt), with the verdict. Every attempt is written, including retries and operational failures. An attempt’s check lines are written before its attempt line, so a final attempt line on disk means the attempt is complete. pass^k, flakiness, discrimination and paired tests are computed from these lines.
  • manifest.json is the condition record: the agent’s commit (computed with evals/ excluded, so committing a run or a label is never an agent change) and this suite’s commit, every model id, harness, fakes, k, total cost, and whether the run was isolated or saw the eval files (§4, C12). Counts, cost and observed models are recomputed from outcomes.jsonl when the run ends. A resumed run lists each resume under resumed (at, pairs_done_before). A run started with --model <id> records it as invoke.model; the agent receives it as model, a candidate without --variable declares model=<id>, and a resume keeps it. Two runs whose manifests differ on any field other than the declared independent variable are not compared (G7).
  • Every case has a set: error_finding or quality_estimate. There is no default.
  • Every case has requirements: what a correct run of it must do, each citing a rule of labels/standard.md and naming the checks that decide it, or why it is unchecked. Every rule that applies to the case’s category is cited or excused in rules_not_applicable (C6, C8).
  • taxonomy.yaml names the document it came from, and is committed before the first case. A model may draft it (C2), but only from a source outside cases/.
  • Unknown values are null, never 0.

4. Commands

Each command has an exit gate. A gate is a trust upgrade, not a lock: no command refuses to run because an earlier gate is unmet. The run manifest records the gates that were unmet when a run started (unmet_gates), and the report gates (§10) grey what each one covers rather than hide it. aot-evals status says which phase the work is in, how far each category can be trusted, what changed since the baseline, and the one next step; aot-evals status --gates shows every gate and the first unmet one.

#CommandDoesExit gate
C0importImport your data as an export (trace dump, dataset download, CSV/JSONL from your observability stack) into data/, with an export.manifest.json entry: source, query, window, row count, date.Every downstream number traces to an export manifest entry; no command calls a live vendor API.
C1inventoryWrite agent.yaml: surface, triggers, instructions, tools (credential names only), harness, the model actually used per call site, version history, the stable window, and the invocation command.Stable window stated with evidence; model per call site observable; invoke.cwd exists; the agent runs once end to end from its command.
C2taxonomyBuild the coverage list from a source outside the suite: the product spec, API/tool surface, help-centre topics or job description. The owner edits it; the source document is recorded.taxonomy.yaml names a source outside cases/ and a date, and its commit precedes the first case.
C3categorizeSort traces into categories, one per trace. Give each a worth and a per-failure-kind cost matrix. Count observed share.Every category has a definition, an example, worth, a simple flag, a cost row and a measured share. “Other” is under 10%.
C4decomposeWithin each category: feature → scenario → failure mode. Mark each failure mode code-checkable, judge-needed or monitor-only, and set plane-B budgets.Every failure mode names its category, check type and cost cell; budgets.yaml covers every category.
C5allocateSize both case sets and print the arithmetic. Error-finding: ≥10 per category, 3 for simple ones. Quality-estimate: proportional to observed share, sized by the interval the owner needs (M14).Both budgets printed with target intervals and the minimum detectable effect (M7); every cell filled, scheduled, or declared out of scope with a reason.
C6authorWrite the labelling standard, then cases: frozen inputs, start state, trigger, scripted follow-ups, expected end state, and requirements walked rule by rule from the standard. Prefer self-verifying pairs where state allows. Scrub before writing.The standard exists; every case has a set, a category, failure modes, provenance, requirements covering every rule that applies to it, and no secrets.
C7screenPer case: a human read; the checks must pass on start → expected end (reference) and fail on start → start (do nothing); a third pass, do-nothing plus a success claim, catches checks that trust the agent’s report (G10). With a reference answer: other correct wordings must pass, the reference answer with nothing changed must fail, and an answer with every number wrong is reported if it passes. Drop cases that fail, and record the drop.Screens recorded per case; drop rate reported.
C8checksImplement plane-A checks: code state diffs first, graded against the goal rather than one path; binary judges only where code cannot decide; side-effect checks on every agent; a grounding check for every fact a prose answer states. A check never uses the agent’s own report of success.Every failure mode has at least one binary check; side-effect checks present; every requirement names a check that runs on its case, or why it is unchecked.
C9labelCollect human labels per judge, stratified across categories: one item per case first, then other trials where the answers differ. Target 100–200; below 60 the interval is too wide to act on.Label count and stratification printed per judge.
C10calibrateMeasure each judge against its labels: TPR and TNR separately (never raw agreement), κ, confusion matrix, cost per judgement. Choose a probabilistic backend’s threshold on the labels. Version the criteria.Every judge has TPR/TNR on ≥60 labels at or above its floors, a named backend and model family, and a version. Uncalibrated judges may run but never count toward headline numbers.
C11reliabilityRun m fresh sessions per item on two backends from different model families; report agreement at three levels (all / majority / none) next to C10’s TPR/TNR.Self-consistency and cross-family agreement reported next to TPR/TNR, never in place of it.
C12baselineRun the suite k = 3–5 times on an unchanged, committed build. Write per-trial outcomes and plane-B counters. Compute the variance band.Run complete and finished, manifest complete, no eval-file exposure, variance band per category, plane B recorded against budgets.yaml.
C19agreementRuns after C12 (numbered last so earlier numbers stay stable). A person labels whole baseline runs pass or fail with a note, seeing the request, the answer and what changed, never the checks. Every labelled run is re-graded from its raw record with the suite as it is now, so labels outlive suite versions. Report the catch rate and clean pass rate (M29), dev and holdout; give every dev disagreement a cause.≥60 labelled runs with ≥15 of each kind; a measurement current for the suite and the labels; dev catch and clean pass rates at or above their floors (85% by default); every dev disagreement triaged. Until then G14 fails and comparisons are provisional.
C13compareRun a candidate on the same cases with one declared independent variable. Report gains, losses and a paired test, never a net delta alone, with plane-B deltas alongside.Paired test present; gains and losses listed; manifests differ only on the declared variable.
C14coverageCount cases per (taxonomy entry × failure mode), zeros included, with the source document named.Coverage reported against an external list, or refused (G5).
C15redundancyOver ≥2 agent versions, classify each case; report the discriminating fraction and a per-case index; propose prunes and cells to extend.Discriminating fraction published; below the threshold (default 0.3), a prune/extend proposal.
C16driftRe-run the frozen set on a schedule and on every prompt, skill, tool, model or harness change, against the baseline’s band.Guidance only; not yet automated.
C17onlineSet up the online regime: shadow → canary → online judging; behavioural signals; fingerprints.Guidance only; not yet automated.
C18reportWrite report.json (§8), then the HTML report (§9).Every gate’s status is in the report.

C3–C18 repeat as a loop; §4.1 says what each kind of change re-opens. A prompt rewrite re-derives the hard-case set, and a shift in category mix re-opens C3 and C5.

How runs work.

  • The agent is a command: the case goes in as JSON on stdin and one JSON result comes back on stdout, with optional setup, seed, observe and reset hooks (references/agent-contract.md). Checks grade what observe returns, never what the agent says it did.
  • Outcome codes (ok, timeout, rate_limited, provider_error, no_output, unparseable, model_disabled, auth_error, budget_exceeded, cli_missing) are recorded per attempt. Only ok is scored; retryable codes are retried up to max_attempts.
  • A run stops after --stop-after (default 5) cases in a row end in an operational failure; a stopped run is never finished. --pause-every N --pause-seconds S paces a rate-limited runtime.
  • aot-evals run --resume RUN_ID carries on a stopped or interrupted run in the same directory. It skips every (case, trial) with a final attempt, including one that ended in an operational failure, and runs the rest, appending to the same outcomes.jsonl. A pair whose last attempt isn’t final is run again, with attempt numbers after the ones already used, so a crashed attempt’s lines never join a new one’s. A truncated last line is dropped first, and the setup and reset hooks run before anything else. Resume is refused when the run is finished, or when the agent commit, the suite’s commit (changes under runs/ aside), invoke.env, a judge’s sha, the case set, the code checks or the fakes differ from the manifest, or when the agent has uncommitted changes: one run never mixes two versions. The run keeps the purpose, k, case set and judges setting it started with. Once finished, it counts as a complete run (C12).
  • The agent must not see the cases. invoke.isolate: true runs the agent and its hooks from a per-run copy of the repository without evals/ (this suite’s env/ is kept). Every run, isolated or not, records eval files changed during the run and agent output that names a case file; either keeps the run from being a baseline.

How comparisons work (C13). Each side’s verdict on a case is its majority over k trials. Gains and losses are majority flips, tested with McNemar’s exact test; a bootstrap CI covers the mean per-case delta. A change is better or worse only when it is significant and outside the baseline’s variance band; otherwise indistinguishable. The declared variable is a kind (model, prompt, config, tools, harness, …) that maps to the manifest fields allowed to differ. Runs of one build split across several runs (for example category by category, to pace a rate-limited runtime) count as one side: their case sets, checks and judges are merged per item.

How redundancy works (C15). Versions are runs grouped by the fields that define the build, so repeats of one build never look like two versions. The prune proposal never prunes quality-estimate cases, sends always-fail cases to review, keeps regression guards for coverage cells and categories, and re-runs the pairwise tests to show the detectable effect is unchanged.

4.1 Phases, trust and iteration

The work has three phases, each ending in something handed to the next group of people:

PhaseWhoEnds with
Assessthe owner and tech lead, with Claudethe discovery report: why the agent is being tested, how it is built, where it can run safely, and the golden test case grid
Buildengineeringthe loop running: the agent wired to the contract, cases seeded from real runs with a check on each, a baseline, and the grader application started
Judgegraders (product owners, experts, end users)each category validated: a person’s verdicts on whole runs, the suite held to them, judges calibrated, rulings folded into the standard

The lightest Build (aot-evals build, then aot-evals build --run) is the one that gets the loop running: an agent that runs from a command (or recorded runs replayed, C1), 10 to 20 cases from real runs in the largest categories (C6), a side-effect check and one code check per category (C8), and a baseline (C12). Every other command is an upgrade that raises trust in a number: provenance (C0), the coverage list (C2), shares and the cost matrix (C3), budgets (C4), allocation (C5), screens (C7), judge labels, calibration and reliability (C9 to C11), whole-run agreement (C19), coverage (C14) and redundancy (C15). The report shows which upgrades are missing and greys what they cover. aot-evals build drafts what it can from the discovery report: categories from its task types, the coverage list from its situations (to be committed before the first case), and one failure mode on every case checked by a generic code check, end-state-matches (the world ends as the case expects), so a first pass needs no check code. Each draft is marked for the owner to confirm.

Trust per category. A category is untested until a finished baseline covers it, provisional once it has numbers, and validated when the suite agrees with a person on its runs: C19’s floors met suite-wide, and the category’s own dev catch rate and clean pass rate at or above the floors (85% by default). report/agreement.json carries the per-category rates. Provisional numbers print, labelled provisional (G14).

The baseline is, for each current case, the newest finished baseline run that covers it: a re-run of one category replaces that category’s baseline and leaves the others’. The agent has changed only when its files outside evals/ differ from the baseline’s, not merely its commit id.

What a change invalidates. status compares the suite and the agent with the baseline and names each change with its short path back:

Change since the baselineInvalidatesThe short path
The agent (commit, model, prompt, tools, harness)the baseline; after a prompt rewrite, the hard-case seta candidate run with one declared variable, then compare; a person labels only the runs whose verdict changed, and the provisional categories
A new category (a new use case)nothing else; the new category is untestedchecks for its failure modes, a baseline that includes it, labels until it clears the floors
Cases added or changednothing until they runscreen them; include them in the next baseline (unscreened cases stay out of gated numbers, G8)
A check changed or addedthe measured agreementre-grade the labelled runs offline (agreement); no agent runs
A judge changed (its version)that judge’s calibrationrecalibrate from cached judgements, then re-grade
The labelling standard changed (a ruling became a rule)the labels the rule could fliprecheck those labels, then re-grade
Labels addedthe measured agreementmeasure it again

An uncommitted agent is named too: a run of it can’t be compared (G7). When the agent’s current version has already run as a candidate and been compared, the step is to decide: have a person label only the runs whose verdict moved, then run it as the new baseline if it ships.

4.2 The bench of graders and the grader application

aot-evals serve starts the grader application: a local web page, served from the suite, for the Judge phase. It binds to 127.0.0.1 and needs the session token in the link it prints; &labeller=NAME on that link gives each grader their own. Everything it does writes the suite’s own files, which are reviewed and committed like any other change:

What a grader doesWhere it is written
Labels whole runs pass or fail, with a note, blind to the checks, the expected state and other people’s labelslabels/runs.jsonl, one line per (run, labeller)
Settles a run two labellers disagree onlabels/rulings.jsonl
Gives a cause to a disagreement between the suite and a personlabels/triage.yaml
Accepts an answer the suite failed as another correct wordingthe case’s expected_end_state.output_variants; the case is screened again
Proposes a rule from a pattern of rulings; the owner accepts or declines itlabels/proposals.yaml; an accepted rule joins labels/standard.md
Labels items for a judge, and recalibrates it (confirming the cost of any new calls)checks/judge/<id>.labels.jsonl, <id>.calibration.json
Drafts a new case in the shape of an existing onelabels/drafts/<id>.json, until an engineer finishes it

Rules:

  • Several people label the same run, blind. Each labeller’s latest label is kept. Where all of a run’s labellers agree, that is its verdict; where they differ, it has no verdict until someone rules, and C19 stays open while any such run is unruled. Agreement between each pair of labellers (share agreed and Cohen’s κ) is reported beside the suite’s agreement with people.
  • Queues are per labeller. A new labeller starts with up to five runs whose answer is settled (a ruling, or two people agreeing) and sees the settled answer after each label. After that, one run in four is one someone else labelled, so agreement between people can be measured, and the rest are runs nobody has labelled, one per case first. After a change, the queue can be limited to the cases whose verdict moved (aot-evals agreement label queue --changed).
  • Re-grading is offline. The application re-grades the labelled runs from their recorded raw records with the suite as it is now: no agent runs and no judge calls.
  • An accepted wording is screened. Every accepted wording must pass every check (C7). If a check still rejects it, the disagreement is triaged as a check bug for an engineer.
  • The engineering inbox lists what only engineering can do: triaged disagreements whose cause is a fix to a case or check, drafted cases, and changes that need the agent run.

4.3 The components

An optimization loop around an agent has the same parts whatever the agent does. aot-evals components lists them, each with the phase that builds it and its state read from the suite (done, partly, not yet, not automated yet, or doesn’t apply). The discovery report may record a component’s state before a suite exists (components: in its front matter); the suite’s own reading wins once it says more.

AreaComponents
Agent eval infrastructureharness and sandbox runtime; mock services, staging accounts, a safe database; record and replay; seeded, realistic, anonymized data
Golden test casescases by category; deterministic checks first; private holdouts
Grader UIeach problem graded like a test, with the reason (aot-evals serve)
Bench of gradersa calibration set first; two blind labels per run; graders from customer and expert pools
LLM judgescalibrated on human labels; qualitative dimensions
Synthetic test casesgenerated from golden cases, validated by graders
Evaluation enginegolden test and judge evals; change scoping against the test sets; A/B tests; trace monitoring
Objective monitoringvalue and traffic weighting; business alignment; distribution change detection
Agent testing and modelingsignificance testing; noise floor and k; misclassification analysis; test runner and isolated compute

5. Judges

A judge is a check for one failure mode that asks one binary, typed question: a noul (a true/false claim, with detects saying whether yes means the failure is present or the requirement is met), a choice over declared options, or a score on a rubric level. The judge layer is backend-agnostic (references/judge-backends.md):

BackendUse
Codeanything decidable from state; always first
System One (systemone), such as Jevtyped questions answered with a probability; cheap and fast enough to judge every attempt
Generative (anthropic), a different model family from the agentreference-guided grading, pairwise comparison, anything needing a written critique
commandany judge you run yourself: another provider, a gateway, a rules engine

Rules:

  1. Every backend is calibrated in C10. A typed answer can still be wrong.
  2. A backend’s confidence is not trusted until measured on your labels. A probabilistic backend’s threshold is chosen by maximising TPR + TNR on the dev labels; the reported rates come from 5-fold out-of-fold predictions, and a hash-assigned 20% holdout is reported separately. The floors are min_tpr / min_tnr (default 0.75).
  3. A judge counts toward a headline only when it is calibrated on its current version and backend. Stored scores are re-thresholded with the calibrated threshold, so calibrating after a run changes the report without re-running anything.
  4. Labels carry the judge’s input. Each label includes the exact input that was labelled (after a secret scan), so calibration can be re-run on any backend without the raw run data.
  5. Judgements are cached per backend (the question, the inputs and that backend’s own config), so adding or changing a backend does not re-pay for the others.
  6. No silent fallback. A refusal or an error is an instrument error, never a verdict from another model. A rate-limit error stops that backend’s calls at once.
  7. Different family for pairwise grading (G4).

E5 compares backends on the same labels, by TPR/TNR and cost per judgement, and is how each judge’s backend is chosen.

6. Measurements

N = cases, k = trials per case, L = human labels. Plane refers to §2.1. Measurements marked † are guidance for the online regime and are not yet computed by the CLI.

idPlaneMeasurementDefinitionUse
M1APer-check outcomebinary, per (case, check, trial)the unit every other plane-A number is computed from
M2APass ratemean over trials and cases, per category, with a cluster-robust CI over cases (Wilson when that collapses)never reported suite-wide without the per-category table
M3Apass^kfraction of cases passing all their trialsreliability across repeats; 75% per trial ≈ 42% at k = 3
M4AFlakinessper case: 0 < passes < kreported as a finding; sets the k budget
M5AVariance bandsame build, ≥3 repeats, spread per categoryno change inside the band is called drift or improvement
M6APaired differenceper-case paired delta, McNemar and bootstrap CI, gains and losses listed separatelya net delta alone is not reported
M7AMinimum detectable effectfrom N, k and per-case variance at 80% powerprinted before an experiment runs
M8CJudge TPR / TNRagainst that judge’s labels, separately, with CIsraw agreement is not reported: an always-pass judge agrees 95% of the time at a 95% base rate
M9CJudge reliabilityself-consistency across m sessions; agreement across two families; three-level agreementreported next to M8, never in place of it
M10CLabel statusL per judge and the resulting interval widthunder 60: uncalibrated; 100–200: usable
M11ACheck trigger ratehow often each check fires over the report’s runs (the baseline by default), and how often it is the only failing checka check that never fires across several versions, or always fires with another, is a prune candidate. On a baseline that passes everything, every rate is 0 and says nothing about the check
M12ACoverage per cellcases per (taxonomy entry × failure mode), zeros included, source namedG5
M13ADiscriminating fractionshare of cases not always-pass or always-fail across ≥2 versions§2.4
M14AQuality estimateCI on the share-weighted pass rate of the quality-estimate set, weights publishedthe only number reported as “quality”
M15AError-finding yielderrors found per case reviewed, vs random selectionjudges a case-selection method
M16ADisagreement yieldsize of the set where judges or models disagree, and its share of all errorsjudges a case-selection method
M17 †AHard-set churnoverlap of the hardest-x% set before and after a prompt changere-derive the hard set when it churns
M18BCost per successful taskrun cost ÷ successescost per run is the wrong denominator
M19BLatencyp50/p90; runs killed at a limitbudgeted per category
M20BTokens and cacheinput, output and cached tokens; cache-hit ratea cost jump at an unchanged pass rate usually means a prompt-prefix edit broke the cache. Compare dollars, not tokens
M21BErrors and retriescount and rate (Wilson interval) per outcome code, over every attempt including retriesneeds no judge; the cheapest regression signal
M22CCase validityreference pass, do-nothing fail, human read; drop rate at C7unscreened cases do not count toward gated numbers (G8)
M23 †CFake fidelitycontract test of each fake against the real APIa drifted fake looks the same as a regression
M24CSimulated-user sensitivitythe same suite under ≥2 user simulators, reported per simulatorG9
M25CCompounded validityproduct of stated stage validities (task × simulator × judge)printed in the report header; 70% × 70% × 70% ≈ 34%
M26ALeakage screenoverlap of case provenance with likely training or benchmark sourcesrequired for cases built from a public source
M27 †BOnline fingerprintsper agent: unanswered requests, time to first response, tokens and cost per turn, tool calls per turn, category mix, model id per calldrift in the part of the system that cannot be sandboxed
M29CWhole-run agreementcatch rate (share of runs a person fails that the suite fails) and clean pass rate (share a person passes that the suite passes), each with a Wilson interval, dev and holdout; a run with no verdict counts as wrong either wayC19, G14. Per-check validity (M8, M22) does not imply it: a suite can clear every screen and miss what a person fails runs for
M28 †ABehavioural signalspost-output edits, acceptance, follow-up corrections, re-asksfree labels for C9

7. Experiments

No comparison runs before E1. Experiments marked † are guidance only.

idExperimentDesignProduces
E1Noise floorsame build, same cases, k ≥ 3, ideally on two different daysM5, M4, the k budget
E2Candidate comparisonpaired on identical cases; one declared independent variableM6 with gains and losses, M3, plane B
E3Judge calibrationstratified labels per judge; iterate; hold out a sliceM8, M10, judge versions
E4Judge reliabilitym sessions × two families, plus a human-labelled subsampleM9 next to M8
E5Judge backend comparisonsame judge and labels: System One vs generative vs your owncost per judgement, TPR/TNR; the backend for each judge
E6Downgrade / routing probea cheaper model or routing policy against a near-100%-pass regression suite, with the prompt re-optimised firstM6, M18–M20
E7 †Selector comparisonrandom vs uncertainty vs disagreement selection at a fixed label budgetM15, M16
E8Coverage auditproject the suite onto the taxonomy; triage every empty cellM12, an ordered authoring queue
E9Redundancy / pruneclassify every case over ≥2 versions; propose prunesM13, M11, a smaller suite with the same detectable effect
E10Weight sensitivityfor composite scores only: vary the weights and check whether the ranking holdswhether the composite is stable
E11 †Shadow / canarythe candidate states what it would do; compare with what happened; then a fraction of live trafficthe gate before write access, for agents with no sandbox
E12 †Fake contract testreplay each fake against the live API on a schedule; diffM23
E13 †Drift watchthe frozen set on a schedule, plus M27 on a control chart with M5’s limitsthe standing check for provider-side and self-inflicted changes
E15Suite validitywhole-run human labels on baseline runs, the suite re-graded on them; iterate on dev disagreements by cause; check holdout once; label a fresh batch before a new claimM29, a triaged list of what the suite misses and why
E14Operational sweepplane B only, no labels or judges: budgets vs actuals per categoryM18–M21; can run in week one, from an export

8. report.json

Written by C18 (aot-evals report) to report/report.json and read by the HTML report. The schema is versioned (aot-evals/report@1); keys may be added within a version, never removed or renamed.

{
  "schema": "aot-evals/report@1",
  "generated": "2026-01-01T00:00:00Z",
  "agent":   { "id": "...", "stable_window_since": "...", "model_ids": {} },
  "conditions": { "agent_commit": "...", "evals_commit": "...", "harness": "...", "k": 5,
                  "judge_backends": [ { "check": "...", "backend": "...", "model": "...",
                                        "family": "...", "version": "..." } ],
                  "simulator": null, "cost_usd": 0.0,
                  "data_exports": [ { "source": "...", "window": "...", "rows": 0 } ] },
  "regime":  "offline",
  "taxonomy": { "name": "...", "source": "...|MISSING", "read_on": "...", "entries": [] },
  "sets":    { "error_finding": { "n": 0, "mde_at_80_power": 0.0 },
               "quality_estimate": { "n": 0, "weights_source": "...", "estimate": {} } },
  "categories": [ { "id": "...", "worth": "high", "simple": false,
                    "observed_share": 0.0, "n_cases": 0,
                    "pass_rate": { "point": 0.0, "ci": [0, 0], "n": 0, "n_cases": 0, "k": 5,
                                   "set": "...", "ci_method": "..." },
                    "pass_pow_k": 0.0, "pass_pow_k_reason": null, "flaky": 0, "variance_band": [0, 0],
                    "operational": { "cost_per_success": 0.0, "cost_per_run": 0.0,
                                     "latency_p50": 0, "latency_p90": 0,
                                     "tokens_input": 0, "tokens_output": 0, "cache_hit_rate": 0.0,
                                     "tool_calls": 0.0, "outcomes": { "ok": 0, "timeout": 0 },
                                     "retries": 0, "killed": 0,
                                     "budget": {}, "budget_status": "within|over" },
                    "excluded": null } ],
  "checks":  [ { "id": "...", "type": "code|judge", "failure_mode": "...",
                 "trigger_rate": 0.0, "sole_failure_rate": 0.0,
                 "judge": { "backend": "...", "model_family": "...", "agent_model_family": "...",
                            "tpr": 0.0, "tnr": 0.0, "labels": 0, "kappa": 0.0, "threshold": null,
                            "self_consistency": 0.0, "cross_family": 0.0,
                            "confidence_calibration": { "ece": null, "brier": null },
                            "cost_per_judgement": 0.0,
                            "status": "calibrated|uncalibrated", "status_reasons": [] } } ],
  "coverage": { "list": "external|MISSING", "cells": [ { "taxonomy_entry": "...", "failure_mode": "...", "n": 0 } ],
                "empty_cells": 0, "authoring_queue": [] },
  "redundancy": { "candidates": [], "always_pass": 0, "always_fail": 0, "discriminating": 0,
                  "discriminating_fraction": 0.0, "per_case": [], "proposal": {} },
  "comparison": { "baseline": "...", "candidate": "...", "paired": true, "refused": null,
                  "gains": [], "losses": [], "net": 0.0,
                  "test": { "name": "mcnemar_exact", "p": 0.0, "ci": [0, 0] },
                  "mde_at_80_power": 0.0, "per_set": {},
                  "operational_delta": { "cost_per_success": 0.0, "latency_p90": 0, "error_rate": 0.0 },
                  "verdict": "better|worse|indistinguishable|trade", "verdict_reasons": [] },
  "validity": { "stages": { "task": null, "simulator": null, "judge": null }, "compounded": null },
  "gates":   [ { "id": "G5", "status": "pass|fail|not_applicable", "detail": "...", "blocks": [] } ],
  "next_actions": [],
  "cases":   [ { "id": "...", "set": "...", "category": "...", "provenance": "...",
                 "screen": { "human_read": true, "reference_passes": true, "donothing_fails": true },
                 "trials": [ { "run": "...", "trial": 0, "attempt": 0, "outcome": "ok", "passed": false,
                               "checks": [ { "id": "...", "type": "code", "passed": false, "detail": "...",
                                             "counted": true } ],
                               "operational": { "cost_usd": 0.0, "latency_ms": 0,
                                                "tokens_input": 0, "tokens_output": 0,
                                                "tool_calls": 0, "retries": 0 },
                               "state_diff": "...", "trace_ref": "..." } ] } ]
}

Rules: unknown values are null, never 0. A missing coverage list is the string "MISSING", not an omitted key. Nothing is pre-rounded. There is one category row per (category, set), labelled by pass_rate.set. Per-trial outcomes and plane-B counters are kept so the renderer can recompute anything. trace_ref is an id or a local path, never the trace.

9. The HTML report, checkpoints and the activity log

aot-evals ships the schema (§8), the panel list and the rules below, not a dashboard. On request, Claude writes one self-contained local HTML file: data inlined as JSON, no network requests, no CDN, no build step. It opens from the filesystem and can be attached to a pull request or an email. Each file is written for a question: a report on “why did the candidate regress” leads with the comparison and the losing cases; one on “is this suite any good” leads with gates, coverage and discrimination. aot-evals report --html writes the branded shell, and Claude writes the panels into it (the report skill).

idPanelShows
P0Conditionsagent and commits, every model id, harness, k, N per set, regime, data exports, date, total cost
P1Gatesevery gate in §10, pass/fail, and what each failure blocks; above the results
P2Validitytask × simulator × judge and the compounded product (M25)
P3Correctness by categorypass rate with CI and pass^k, ordered by worth; variance band shaded; n, k and set on every row
P4Operational by categoryplane B against budgets.yaml; over-budget cells flagged. Next to P3, never merged with it
P5Coveragetaxonomy entry × failure mode counts; empty cells outlined; source in the caption
P6Discriminationalways-pass / always-fail / discriminating, with counts and the discriminating fraction
P7Comparisongains and losses as separate bars, each changed case linked; paired test and MDE in the caption; plane-B deltas beside it. No net-only view
P8Judgesper judge: backend and family vs the agent’s, confusion matrix, TPR/TNR with CIs, labels, κ, threshold, cost; reliability in a separate column group. Uncalibrated judges greyed
P9Flakinessper case, k marks in run order: pass, fail, operational failure
P10 †Driftthe frozen set over time against M5’s band
P11 †Onlinefingerprints on control charts, sampling rate, referent
P12Case detailinputs, start state, expected end state, actual state diff, each check’s verdict, plane-B counters, trace reference, provenance, screen status
P13Next actionsempty coverage cells, judges under 60 labels, categories under their k budget, prune candidates, over-budget categories; each with the command that clears it

Rules:

  1. Every number shows n, k and an interval, or says why no interval can be computed.
  2. “Quality” labels only a quality-estimate-set number with published weights. Error-finding numbers are labelled “error-finding set — not a quality estimate”.
  3. Correctness and operational numbers are never combined, and an operational regression is shown even when correctness is unchanged.
  4. Anything below its gate is shown greyed and not used as a headline; it is never hidden.
  5. Reliability and validity never share a column group.
  6. The headline is the per-category table, not a single aggregate pass rate.
  7. Every claim in prose links to the panel it comes from.

Checkpoints and the activity log. The work is paced by pages, so the owner can follow it in a browser and steer at every gate.

  • Activity log. journal/activity.jsonl is append-only: every CLI command writes an entry (command, gate, exit code, duration, refusal message), and the skills add their steps, findings, decisions and questions with aot-evals log. Each append re-renders activity.html, which reloads itself and says when Claude is waiting. The journal ignores itself in git: it may quote unreviewed trace text, and the committed record of a decision is the file it changed.
  • Checkpoints. aot-evals checkpoint writes a page for one gate: the C0–C19 roadmap in plain words, Claude’s summary, the log since the last checkpoint, every design file rendered as tables, the owner’s questions, what still blocks the gate, and the next step. It makes no model calls.
  • Pacing. Every skill works until a gate clears or the work needs the owner, publishes a checkpoint, and stops until the person replies.
  • Telemetry. start, discovery-page, checkpoint, report and demo each send one pseudonymous milestone event when they succeed (install id, event, gate id, version, OS, time; nothing about the agent or its data). It is off whenever Claude Code’s own telemetry is off, on cloud providers and in CI; aot-evals telemetry shows, shows the payload of, and switches it. Details: SECURITY.md.

Visual style follows brand/README.md.

10. Gates

idConditionRefuses
G1No condition record (model ids, commits, harness, k)the whole report
G2k = 1any claim of improvement, regression or drift
G3Judge uncalibrated (under 60 labels, below its floors), or reported as raw agreementthat judge’s contribution to any headline; the judge is greyed
G4Generative judge from the same model family as the agent, for pairwise or preference gradingthe pairwise result. A System One judge satisfies this
G5Coverage list with no external source, derived from the suite’s own cases, or committed after themthe word “coverage”; prints MISSING and the reason
G6Headline pass rate from the error-finding setthe word “quality”; offers the weighted estimate instead
G7Comparison without a paired test, or with undeclared manifest differencesthe comparison
G8Cases that have not passed the reference / do-nothing screentheir inclusion in any gated number
G9Simulated user present, results not reported per simulatorthe aggregate score
G10A check that relies on the agent’s own report of successthe check
G11A “better” verdict while plane B regressed beyond budgetthe verdict; it becomes “trade”, with both numbers
G12A number whose data does not trace to an export manifest entry or a committed casethat number
G13Retried attempts or operational failures missing from outcomes.jsonl, or scored as correctnessthe run’s plane-A and plane-B numbers
G14The suite’s verdict on whole runs not measured against a person’s, stale, below its floors, or with untriaged dev disagreements (C19)the word “validated”; every comparison is marked provisional

From docs/method.md in aot-evals 1.0.1.