File layout
Where things live on disk, both the inputs you write and the outputs runs produce.
Inputs
agent-exam finds the project root by walking up from the current directory to
the nearest pyproject.toml, and reads its suites from evals/
next to it. Point it somewhere else with evals_dir:
[tool.agent-exam]
evals_dir = "qa/evals"
evals/
config.yaml # default harness, judge model, concurrency
# groups, default timeout, skills_dirs, …
config.local.yaml # personal overrides — gitignored
suites/<suite>/ # typically one suite per skill
suite.yml # optional suite-level config
tasks/
<task>.yaml # one task per file
fixtures/<fixture>/ # starting working directory for tasks that
... # opt in via setup.fixture
config.local.yaml is deep-merged over config.yaml at load time.
Dict keys merge recursively, so you only need to write what you want to
override; lists are replaced outright rather than appended. Typical uses are
changing the harness for your own runs:
# evals/config.local.yaml
default_harness: opencode
or overriding a single key inside a provider block without repeating the whole block:
providers:
claude_code:
default_model: claude-opus-4-7
Suites and fixtures are committed. All fixtures live in version control — evals have to be reproducible across machines and team members, so fixtures cannot be gitignored or regenerated locally. Prefer small plain-text fixtures where that is realistic, but do not reject a large one that is genuinely needed (a 5 MB HTML page to test how a skill handles big input, say). If repository size becomes a problem, Git LFS is a reasonable escape hatch for the largest files.
Fixtures are shared across suites, so give them descriptive, scoped names to avoid collisions.
suite.yml
A suite may carry an optional suite.yml declaring suite-level metadata.
Today the only supported key is evaluated_skills:
# evals/suites/<suite>/suite.yml
evaluated_skills:
- github-release
evaluated_skillsThe skills this suite evaluates.
--without-skillexcludes them from the bundle handed to the harness. Defaults to the suite name alone.
Using suite.yml is worthwhile even for a single-skill suite: it makes
the skill-to-suite mapping explicit. Use it in earnest when a suite covers
several related skills — a primary skill plus internal helpers — so the reality
check removes the full set. --no-skills ignores the field entirely and
removes every skill under skills_dirs.
Outputs
Each run creates one directory under evals/runs/<run-id>/, with
timestamp-based run ids:
evals/runs/<run-id>/
run.json # run-level metadata: timestamps, config.
# written once
artifacts/ # what the skill produced. frozen once the
# run completes; rescoring does not touch it
<suite>/<task>/attempt-1/
trajectory.json # normalized per-turn record
cwd/ # archived working tree the skill wrote
attempt.json # harness, model, timestamps, metrics, and
# the raw transcript path
reports/ # how the run was graded. one file per report
2026-04-23-1423.json # initial report — every attempt
2026-04-23-1500.json # rescore — only the attempts in scope
The raw harness-specific transcript is not copied, since it is too
format-dependent; attempt.json stores its path as
raw_transcript_path. For Claude Code, run claude-measure-usage <path>
on it.
evals/runs/ is gitignored: runs are your local lab notebook, not a
shared artifact. They are mostly reproducible from committed inputs and usually
fine to share, but be a little careful with transcripts — skills are written to
keep secrets out of the agent's context, and mistakes happen. Skim before
posting one externally.
Nothing prunes evals/runs/ automatically. Delete old runs when your
disk complains. They feed the per-task history view, so they stay useful
for trend analysis until then.