Adding Benchmarks: The Playbook
The mechanical checklist for adding or changing a scenario, workflow, or lane.
Written so an agent can follow it without judgment calls; the judgment is
already encoded in CONTEXT.md and docs/adr/.
Before you start
- Read
CONTEXT.md(vocabulary) anddocs/adr/(decisions you must not re-litigate). Use the glossary terms exactly. - Shared measurement policy lives in
scripts/oakbench/. If your change touches how anything is measured (clocks, tokens, environment, row fields, commands), it goes there β never copied into a lane.
Adding a scenario or workflow to an existing lane
- Name it forever. Scenario and operation names are immutable identity (ADR-0005). Pick names that won't need to change; never reuse a retired name.
- Fixture: deterministic bytes only. Generation is untimed. Use
oakbench.fixtures.write_pattern_filefor bulk content. Bump the lane's fixture version (FIXTURE_VERSION/fixture_version) if you change any existing fixture's bytes. - Commands through the contract. VCS commands come from
oakbench.command_semantics(backed byconfig/command_semantics.json). If the scenario needs a command the contract lacks, add it to the JSON (both tracks, both subject kinds, acompatibility_notewhere one subject lacks the capability) and bump the contract version. Capability gaps run the closest available command and are recorded, never hidden. - Snapshot accounting: git pays stage+commit as two recorded calls, oak
one. Use
snapshot_steps/semantics().snapshot_commands; never hand-roll a combinedadd && commitcall. - Tolerated failures: steps that are SUPPOSED to fail use
expected_returncodes; the row keepsprocess_returncodeas the truth. - Oracle: deterministic only β validators, changed-file shape
(
expected_change_okfor the agent lane), clean final VCS state. The agent's final message is never an oracle. Read-only scenarios must require an untouched tree. - Null honesty (ADR-0002): any signal your scenario cannot observe is
nullwith ameasurement_source, never a fabricated zero. - Spec entry: add the scenario to the lane's YAML under
scenarios/(match the existing anchor structure) and a row todocs/benchmark-coverage.mdwith an honest status badge. - Tests: add or extend a test in
tests/. Minimum: the scenario appears in the catalog, its oracle shape accepts/rejects correctly, and (for new step recipes) the operations list matches expectations. If it parses new external output, add a golden fixture undertests/fixtures/.
Changing measurement behavior
-
If the commands, fixture bytes, or semantics of an existing scenario/operation change materially, that is a NEW name or a version bump β not an edit in place (ADR-0005).
-
If only harness code changed, prove measurement identity:
# before your change python3 scripts/workflow_ab.py --workflows all --results /tmp/parity-old # after your change python3 scripts/workflow_ab.py --workflows all --results /tmp/parity-new python3 scripts/row_parity.py /tmp/parity-old/latest.jsonl /tmp/parity-new/latest.jsonlZero invariant drift required. Repeat for any other lane you touched (
bench.py --profile microis the fastest core-lane parity source). -
Run the instrument tests:
python3 -m unittest discover -s tests.
Adding a new lane
- Define the lane's row shape in
oakbench.rows.LANE_REQUIRED_KEYSand write results throughoakbench.results.ResultsStore(results_dir, lane=...)so the contract is enforced at write time. - Build on the oakbench primitives:
subjects.load_subjects,environment.base_env,execution.run_timed,tokens.interaction_token_fields,reportingmath. - Standard CLI flags:
--subjects,--config,--git-bin,--oak-installed-bin,--oak-local-bin,--results,--runs,--keep-workdirs,--randomize-subject-orderwhere ordering matters. - Skip rows (
returncode: 77,skip_reason) for anything the environment cannot run β recorded, never hidden. - Add: a glossary entry in
CONTEXT.md, a coverage row indocs/benchmark-coverage.md, a README section, an integration test intests/(tiny scale, contract-validated), and devloop wiring if the lane should gate Oak development.
Adding an agent CLI adapter
- New entry in
config/agents.tomlplus a command builder inagent_workflow.build_agent_command. - New
StreamAdaptersubclass inoakbench/stream_adapters.pyfor the CLI's stream shape β never widen the shared heuristics for one CLI's quirk. - Capture a real transcript, sanitize it (paths, ids), and add it as a golden
fixture under
tests/fixtures/with assertions on turns, usage, tool outcomes, and truncation semantics.
Before pushing
python3 -m unittest discover -s tests # instruments
python3 scripts/devloop.py --lanes core ... # the loop your change serves
oak status # nothing generated is staged
Raw results, transcripts, and workdirs never get committed (ADR-0003).
Adding a new subject (VCS)
Adding a VCS subject is data, not lane code: one plugin JSON under
config/subject_kinds/, one [subjects.<name>] entry in
config/subjects.toml, and golden output samples under tests/fixtures/.
Capability gaps are null commands that become skip rows
(capability_gap:<kind>:<operation>), never substituted commands. The git
and oak plugin JSONs are a parallel read of config/command_semantics.json
(which lanes still load unchanged), proven measurement-identical by
oakbench.subject_kinds.parity_report β an empty report is the proof, and
tests/test_subject_kinds.py enforces it. Full recipe with the jujutsu
pilot as the worked example: docs/adding-a-subject.md.