Minimal harness for evaluating coding agents with real hidden-check separation: the agent works on a seed repo with only the public tests; grading happens in a clean directory with both public and hidden tests intact, taken from the fixture. Runtime is 100% stdlib.
Evaluating an agent on the tests the agent itself can see (or modify) measures nothing. This harness makes the separation a structural invariant, not a convention:
- The agent's workspace never contains the hidden tests (checked at the end of preparation and re-checked at grading).
- Grading does not trust the workspace: a fresh directory holds the intact seed plus ONLY the agent's entry files plus public and hidden tests taken from the fixture. An agent that rewrites the tests changes its own grade by zero.
- Every run records the SHA-256 of the hidden set: cryptographic proof of which checks graded the run.
fx-001-csv-dedup/
├── task.json id, title, prompt, entry_files, timeout_seconds, taxonomy
├── seed/ the buggy/incomplete repo the agent starts from
├── tests_public/ test_public*.py — visible to the agent (happy path)
├── tests_hidden/ test_hidden*.py — NEVER seen by the agent (edge cases)
└── reference/ correct solution of the entry files
Design contract (checked by aeh validate): the seed passes the public tests but fails ≥1 hidden test (the bug lives in the edge cases); the reference passes everything. That way "passes the public tests" and "solved the task" stay distinct measures.
# install (dev)
uv venv .venv && uv pip install -e ".[dev]" --python .venv/bin/python
# 1. check the fixture contract
aeh validate fixtures/fx-001-csv-dedup fixtures/fx-002-slugify
# 2. baseline: what happens with no agent (seed as-is)
aeh run fixtures/fx-001-csv-dedup --noop # exit 2, PARTIAL
# 3. sanity: the reference solves it
aeh run fixtures/fx-001-csv-dedup --ref # exit 0, SOLVED
# 4. a real agent: any shell command, cwd = workspace
aeh run fixtures/fx-001-csv-dedup \
--solver 'claude -p "$(cat PROMPT.md)" --permission-mode acceptEdits' \
--label claude-sonnet --price-in 2.5 --price-out 12.0
# 5. cross-solver matrix from N runs (solve rate, hidden gap, EUR/solved task)
aeh matrix runs/* --out runs/matrixEach run produces runs/{id}-{ts}/ with workspace/ (what the agent saw and touched), grading/ (the clean evaluation), transcript.txt, results.json and run_card.md (verdict, public/hidden table, hash of the hidden set, reproduction steps).
Exit codes: 0 solved · 1 harness/contract error · 2 valid run but not solved.
- Measures: the agent's ability to solve the REAL contract of the task, not just the tests it sees. Solve rate is the signal. The public pass rate is not: a valid fixture is green on the public suite before the agent starts (
aeh validateenforces it), so a do-nothing solver scores full marks there — it is a regression guard, never evidence of a solution. The public-vs-hidden gap is informative only when read against the floor a--nooprun produces on the same fixtures. Full rules: docs/METRICS.md. tamper_suspect: files named like hidden tests, created by the agent in the workspace, are flagged in the run card (grading stays valid either way).- Does not (yet): filesystem confinement of the solver (it runs as your user, so use an agent CLI you trust), multi-language support (fixture tests are Python
unittest), run parallelism.
The code written by the agent IS EXECUTED at grading (the tests import the entry
files), so grading runs with the network denied whenever a backend exists:
sandbox-exec on macOS (seatbelt: deprecated in the headers but present and
maintained; a refused connect becomes EPERM), unshare --net on Linux. Every run
records in results.json what actually happened (grading_sandbox: backend,
unavailable, disabled, or fallback-off:* if the backend would not start).
Escape hatch: --no-grading-sandbox. The solver, on the other hand, has network by
default (agent CLIs need it for the API); --sandbox-solver denies it explicitly to
local/offline solvers.
The solver can report its own consumption by writing JSON to the file named by
$AEH_USAGE_FILE ({"tokens_in": …, "tokens_out": …, "cost_eur": …}); with
--price-in/--price-out (EUR per Mtok) the cost is computed from the tokens. The
provenance is always marked solver-reported: it is data from the process under
evaluation, not a proof. aeh matrix aggregates N runs per label (--label) and
reports, for each solver, solve rate, hidden gap, median latency and
EUR/solved-task (only when ALL of the solver's runs carry a cost: no invented
averages on partial data). Include a --noop and a --ref run over the same
fixtures in the same matrix: they are tagged as floor and ceiling, every row gets
its distance from that floor, and without a floor run the matrix warns instead of
printing a hidden gap with no scale (docs/METRICS.md).
aeh is the runner for the next matrices of the Agent Failure Eval Bench: two separate repos with a one-way bridge (ADR-001 in docs/). AFB stays the bench artifact (specs, taxonomy, policy, evidence), aeh executes. The importer converts AFB's executable tasks into aeh fixtures:
aeh import-afb path/to/afb/repo --out fixtures # holdout inside the working repo
aeh import-afb path/to/public/clone --holdout-dir DIR # external private materialsagent_visible/→seed/(withouttests/and TASK.md, which becomes the prompt); public tests renamedtest_public_*; the non-.py test assets stay visible in the workspace but grading ignores them.entry_filesdetected by diffing reference against visible; reference-only artifacts the agent must produce (e.g.attempt_log.json) get a stub in the seed, recorded inprovenance.seed_stubstogether with the holdout origin and timestamp.- The public AFB bundle excludes hidden tests and reference (disclosure policy): without the private materials the import is
PARTIAL(exit 2) with aMISSING.mdlisting the published hidden checks as author guidance. Complete imported fixtures contain the bench's private material, so do NOT commit them to a public repo (here:.gitignore fixtures/afb-*). - AFB's process/rubric checks (trace, communication) do not become aeh tests: only the mechanically executable part carries over. Matrix results go back to AFB as evidence, citing the version of aeh used.
On the first real import (v0.3.0) aeh validate caught a real drift in the bench: the hidden test of afb-v0-006 expected a trailing newline the published note does not have, confirmed by AFB's internal validator and fixed on the private holdout side (the public file is frozen by the SHA-256 manifest).
- 70 pytest tests, 94% coverage (CI gate: 80%), ruff clean.
- The suite explicitly checks: separation (no hidden test in the workspace), tamper-proofing (edits to the tests leave grading unchanged), fixture contract (4 fixtures), solver timeout, CLI exit codes, REAL network denial (loopback probe: refused without the sandbox, EPERM/unreachable under it), honest backend fallback, usage ingestion, matrix, and AFB import (mapping, rename, stub, external holdout, partial, full contract of the imported fixture).