benchspec 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. benchspec-0.0.1/.gitignore +16 -0
  2. benchspec-0.0.1/LICENSE +21 -0
  3. benchspec-0.0.1/Makefile +41 -0
  4. benchspec-0.0.1/PKG-INFO +210 -0
  5. benchspec-0.0.1/README.md +178 -0
  6. benchspec-0.0.1/docs/concepts.md +174 -0
  7. benchspec-0.0.1/docs/configuration.md +237 -0
  8. benchspec-0.0.1/docs/harnesses.md +139 -0
  9. benchspec-0.0.1/docs/quickstart.md +242 -0
  10. benchspec-0.0.1/docs/results.md +184 -0
  11. benchspec-0.0.1/docs/sandbox.md +167 -0
  12. benchspec-0.0.1/docs/style/development.md +243 -0
  13. benchspec-0.0.1/docs/writing-evals.md +264 -0
  14. benchspec-0.0.1/evals/binder/conftest.py +205 -0
  15. benchspec-0.0.1/evals/binder/corpus.yaml +309 -0
  16. benchspec-0.0.1/evals/binder/test_corpus.py +147 -0
  17. benchspec-0.0.1/evals/binder/test_corpus_integrity.py +378 -0
  18. benchspec-0.0.1/evals/e2e/hello/SKILL.md +23 -0
  19. benchspec-0.0.1/evals/e2e/hello/evals/hello/greets-by-name.eval.md +12 -0
  20. benchspec-0.0.1/evals/e2e/hello/evals/hello/setup.sh +7 -0
  21. benchspec-0.0.1/evals/e2e/hello/evals/hello-file/setup.sh +7 -0
  22. benchspec-0.0.1/evals/e2e/hello/evals/hello-file/workspace/request.md +1 -0
  23. benchspec-0.0.1/evals/e2e/hello/evals/hello-file/writes-greeting-file.eval.md +17 -0
  24. benchspec-0.0.1/pyproject.toml +134 -0
  25. benchspec-0.0.1/src/benchspec/__init__.py +17 -0
  26. benchspec-0.0.1/src/benchspec/__main__.py +178 -0
  27. benchspec-0.0.1/src/benchspec/agents/__init__.py +106 -0
  28. benchspec-0.0.1/src/benchspec/agents/base.py +305 -0
  29. benchspec-0.0.1/src/benchspec/agents/claude.py +334 -0
  30. benchspec-0.0.1/src/benchspec/agents/codex.py +607 -0
  31. benchspec-0.0.1/src/benchspec/agents/opencode.py +653 -0
  32. benchspec-0.0.1/src/benchspec/config/__init__.py +3 -0
  33. benchspec-0.0.1/src/benchspec/config/arms.py +281 -0
  34. benchspec-0.0.1/src/benchspec/config/sets.py +207 -0
  35. benchspec-0.0.1/src/benchspec/exit_codes.py +53 -0
  36. benchspec-0.0.1/src/benchspec/grading/__init__.py +6 -0
  37. benchspec-0.0.1/src/benchspec/grading/binder.py +388 -0
  38. benchspec-0.0.1/src/benchspec/grading/checkers.py +272 -0
  39. benchspec-0.0.1/src/benchspec/grading/judge.py +219 -0
  40. benchspec-0.0.1/src/benchspec/grading/judges/__init__.py +27 -0
  41. benchspec-0.0.1/src/benchspec/grading/judges/config.py +164 -0
  42. benchspec-0.0.1/src/benchspec/grading/judges/registry.py +79 -0
  43. benchspec-0.0.1/src/benchspec/grading/trajectory.py +230 -0
  44. benchspec-0.0.1/src/benchspec/grading/trigger.py +118 -0
  45. benchspec-0.0.1/src/benchspec/orchestration/__init__.py +6 -0
  46. benchspec-0.0.1/src/benchspec/orchestration/cases.py +225 -0
  47. benchspec-0.0.1/src/benchspec/orchestration/environments.py +117 -0
  48. benchspec-0.0.1/src/benchspec/orchestration/execution.py +548 -0
  49. benchspec-0.0.1/src/benchspec/orchestration/results.py +165 -0
  50. benchspec-0.0.1/src/benchspec/orchestration/room.py +191 -0
  51. benchspec-0.0.1/src/benchspec/orchestration/workspace.py +87 -0
  52. benchspec-0.0.1/src/benchspec/py.typed +0 -0
  53. benchspec-0.0.1/src/benchspec/reporting/__init__.py +3 -0
  54. benchspec-0.0.1/src/benchspec/reporting/analyze.py +102 -0
  55. benchspec-0.0.1/src/benchspec/reporting/manifest.py +180 -0
  56. benchspec-0.0.1/src/benchspec/reporting/report.py +750 -0
  57. benchspec-0.0.1/src/benchspec/runners/__init__.py +3 -0
  58. benchspec-0.0.1/src/benchspec/runners/pytest.py +456 -0
  59. benchspec-0.0.1/src/benchspec/runners/run.py +111 -0
  60. benchspec-0.0.1/src/benchspec/sandbox/__init__.py +3 -0
  61. benchspec-0.0.1/src/benchspec/sandbox/backend.py +386 -0
  62. benchspec-0.0.1/src/benchspec/sandbox/project.py +138 -0
  63. benchspec-0.0.1/src/benchspec/sandbox/provenance.py +309 -0
  64. benchspec-0.0.1/src/benchspec/sandbox/sandbox.py +639 -0
  65. benchspec-0.0.1/src/benchspec/specs/__init__.py +3 -0
  66. benchspec-0.0.1/src/benchspec/specs/discovery.py +263 -0
  67. benchspec-0.0.1/src/benchspec/specs/lint.py +90 -0
  68. benchspec-0.0.1/src/benchspec/specs/mdformat.py +220 -0
  69. benchspec-0.0.1/src/benchspec/specs/schema.py +245 -0
  70. benchspec-0.0.1/src/benchspec/testing.py +57 -0
  71. benchspec-0.0.1/tests/__init__.py +1 -0
  72. benchspec-0.0.1/tests/agents/fixtures/codex_parse_error.jsonl +3 -0
  73. benchspec-0.0.1/tests/agents/fixtures/codex_parse_success.jsonl +8 -0
  74. benchspec-0.0.1/tests/agents/fixtures/codex_parse_tool_only.jsonl +4 -0
  75. benchspec-0.0.1/tests/agents/fixtures/codex_route_fired.jsonl +4 -0
  76. benchspec-0.0.1/tests/agents/fixtures/codex_route_nofire.jsonl +4 -0
  77. benchspec-0.0.1/tests/agents/fixtures/opencode_route_fired.jsonl +4 -0
  78. benchspec-0.0.1/tests/agents/fixtures/opencode_route_nofire.jsonl +4 -0
  79. benchspec-0.0.1/tests/agents/test_base.py +132 -0
  80. benchspec-0.0.1/tests/agents/test_claude.py +618 -0
  81. benchspec-0.0.1/tests/agents/test_codex.py +732 -0
  82. benchspec-0.0.1/tests/agents/test_factory.py +47 -0
  83. benchspec-0.0.1/tests/agents/test_opencode.py +1150 -0
  84. benchspec-0.0.1/tests/agents/test_registry.py +52 -0
  85. benchspec-0.0.1/tests/config/__init__.py +3 -0
  86. benchspec-0.0.1/tests/config/test_arms.py +629 -0
  87. benchspec-0.0.1/tests/config/test_sets.py +231 -0
  88. benchspec-0.0.1/tests/conftest.py +66 -0
  89. benchspec-0.0.1/tests/fixtures/activation/evals/activation-demo/eval.md +14 -0
  90. benchspec-0.0.1/tests/fixtures/judge/unset-judge-env.toml +14 -0
  91. benchspec-0.0.1/tests/fixtures/judge/unsupported-judge-harness.toml +10 -0
  92. benchspec-0.0.1/tests/fixtures/sandbox/microsandbox.toml +15 -0
  93. benchspec-0.0.1/tests/fixtures/sandbox/two-backends.toml +24 -0
  94. benchspec-0.0.1/tests/grading/__init__.py +3 -0
  95. benchspec-0.0.1/tests/grading/judges/__init__.py +1 -0
  96. benchspec-0.0.1/tests/grading/judges/test_claude_code.py +114 -0
  97. benchspec-0.0.1/tests/grading/judges/test_codex.py +149 -0
  98. benchspec-0.0.1/tests/grading/judges/test_config.py +140 -0
  99. benchspec-0.0.1/tests/grading/judges/test_judge_registry.py +118 -0
  100. benchspec-0.0.1/tests/grading/judges/test_opencode.py +155 -0
  101. benchspec-0.0.1/tests/grading/test_binder.py +440 -0
  102. benchspec-0.0.1/tests/grading/test_checkers.py +503 -0
  103. benchspec-0.0.1/tests/grading/test_judge.py +280 -0
  104. benchspec-0.0.1/tests/grading/test_trajectory.py +303 -0
  105. benchspec-0.0.1/tests/grading/test_trigger.py +201 -0
  106. benchspec-0.0.1/tests/orchestration/__init__.py +3 -0
  107. benchspec-0.0.1/tests/orchestration/test_cases_messages.py +37 -0
  108. benchspec-0.0.1/tests/orchestration/test_environments.py +71 -0
  109. benchspec-0.0.1/tests/orchestration/test_execution.py +2198 -0
  110. benchspec-0.0.1/tests/orchestration/test_results.py +432 -0
  111. benchspec-0.0.1/tests/orchestration/test_room.py +254 -0
  112. benchspec-0.0.1/tests/orchestration/test_room_history.py +60 -0
  113. benchspec-0.0.1/tests/orchestration/test_workspace.py +96 -0
  114. benchspec-0.0.1/tests/reporting/__init__.py +3 -0
  115. benchspec-0.0.1/tests/reporting/test_analyze.py +256 -0
  116. benchspec-0.0.1/tests/reporting/test_manifest.py +208 -0
  117. benchspec-0.0.1/tests/reporting/test_report.py +937 -0
  118. benchspec-0.0.1/tests/runners/__init__.py +3 -0
  119. benchspec-0.0.1/tests/runners/test_pytest.py +1462 -0
  120. benchspec-0.0.1/tests/runners/test_run.py +295 -0
  121. benchspec-0.0.1/tests/sandbox/__init__.py +3 -0
  122. benchspec-0.0.1/tests/sandbox/test_backend.py +293 -0
  123. benchspec-0.0.1/tests/sandbox/test_project.py +317 -0
  124. benchspec-0.0.1/tests/sandbox/test_provenance.py +354 -0
  125. benchspec-0.0.1/tests/sandbox/test_sandbox.py +1605 -0
  126. benchspec-0.0.1/tests/specs/__init__.py +3 -0
  127. benchspec-0.0.1/tests/specs/test_discovery.py +476 -0
  128. benchspec-0.0.1/tests/specs/test_lint.py +82 -0
  129. benchspec-0.0.1/tests/specs/test_mdformat.py +636 -0
  130. benchspec-0.0.1/tests/specs/test_schema.py +231 -0
  131. benchspec-0.0.1/tests/support.py +7 -0
  132. benchspec-0.0.1/tests/test_e2e_suite.py +267 -0
  133. benchspec-0.0.1/tests/test_exit_codes.py +43 -0
  134. benchspec-0.0.1/tests/test_main.py +308 -0
  135. benchspec-0.0.1/tests/test_readme_examples.py +38 -0
@@ -0,0 +1,16 @@
1
+ .venv/
2
+ .worktrees/
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ __pycache__/
6
+ *.py[cod]
7
+ *.swp
8
+ .env
9
+ tmp/
10
+ dist/
11
+ build/
12
+ *.egg-info/
13
+ .superpowers/
14
+ .claude/scheduled_tasks.lock
15
+ .claude/worktrees/
16
+ .claude/settings.local.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Swift
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,41 @@
1
+ .PHONY: help install test e2e evals assets lint lint\:ruff lint\:houserules clean
2
+ .DEFAULT_GOAL := help
3
+
4
+ help: ## Show this help
5
+ @awk '/^[a-zA-Z0-9_:\\-]+:.*## / {t=$$0; sub(/:[ \t]*##.*/,"",t); gsub(/\\/,"",t); d=$$0; sub(/^.*## /,"",d); printf " \033[36m%-11s\033[0m %s\n", t, d}' $(MAKEFILE_LIST)
6
+
7
+ install: ## Create the venv and install dev dependencies
8
+ uv sync
9
+
10
+ test: ## Run the unit test suite
11
+ PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 uv run pytest -p pytester
12
+
13
+ e2e: ## Run benchspec's own end-to-end suite (real microVMs; needs claude+codex CLIs and provider credentials)
14
+ uv run benchspec run --set e2e
15
+
16
+ # Keep modest: high fan-out trips the Gemini call's ~60s timeout (12-way -> throttling).
17
+ BINDER_WORKERS ?= 6
18
+ evals: ## Run the binder corpus (binder quality, not framework function). Pass EVAL_ARGS="--collect-only -q" to dry-run collection.
19
+ uv run pytest -m binder_corpus -n $(BINDER_WORKERS) evals/binder $(EVAL_ARGS)
20
+
21
+ assets: ## Re-render the raster brand assets in docs/assets (terminal mock PNG/GIF, social card)
22
+ uv run scripts/render_assets.py
23
+
24
+ lint: ## Lint with Ruff and houserules
25
+ $(MAKE) lint:ruff
26
+ $(MAKE) lint:houserules
27
+
28
+ lint\:ruff: ## Lint with Ruff
29
+ uv run ruff check .
30
+
31
+ LINT_BASE ?= origin/dev
32
+ lint\:houserules: ## Lint changed and new Python files with houserules (needs GEMINI_API_KEY)
33
+ uv run houserules --base "$$(git merge-base $(LINT_BASE) HEAD)" --verbose .
34
+ @untracked_python_files="$$(git ls-files --others --exclude-standard -- '*.py')"; \
35
+ if [ -n "$$untracked_python_files" ]; then \
36
+ uv run houserules --verbose $$untracked_python_files; \
37
+ fi
38
+
39
+ clean: ## Remove the venv and Python caches
40
+ rm -rf .venv .pytest_cache .ruff_cache
41
+ find . -type d -name __pycache__ -prune -exec rm -rf {} +
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.5
2
+ Name: benchspec
3
+ Version: 0.0.1
4
+ Summary: benchspec is a framework for evaluating AI agents with repeatable, isolated benchmarks. Write evals as Markdown, run each eval across named benchmark arms, and compare how agent behavior changes by harness, model, effort, and environment.
5
+ Project-URL: Homepage, https://github.com/theycallmeswift/benchspec
6
+ Project-URL: Documentation, https://github.com/theycallmeswift/benchspec/tree/dev/docs
7
+ Project-URL: Issues, https://github.com/theycallmeswift/benchspec/issues
8
+ Author-email: Swift <swift@majorleaguehacking.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: Pytest
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development :: Testing
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: pytest>=8
23
+ Requires-Dist: python-dotenv>=1.0
24
+ Requires-Dist: pyyaml>=6
25
+ Provides-Extra: claude
26
+ Provides-Extra: codex
27
+ Provides-Extra: microsandbox
28
+ Requires-Dist: microsandbox<0.7,>=0.6.16; extra == 'microsandbox'
29
+ Provides-Extra: opencode
30
+ Provides-Extra: testing
31
+ Description-Content-Type: text/markdown
32
+
33
+ <picture>
34
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/benchspec-wordmark-dark.svg">
35
+ <img alt="benchspec" src="docs/assets/benchspec-wordmark-light.svg" width="188" height="48">
36
+ </picture>
37
+
38
+ **Benchmark what your agent does, not what it says.**
39
+
40
+ [![CI](https://github.com/theycallmeswift/benchspec/actions/workflows/ci.yml/badge.svg)](https://github.com/theycallmeswift/benchspec/actions/workflows/ci.yml)
41
+ [![PyPI](https://img.shields.io/pypi/v/benchspec)](https://pypi.org/project/benchspec/)
42
+ [![Python](https://img.shields.io/pypi/pyversions/benchspec)](https://pypi.org/project/benchspec/)
43
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
44
+
45
+ benchspec runs an agent (Claude Code, Codex, or OpenCode) against a task
46
+ in a fresh microVM, checks what it actually did in the workspace, and reports the
47
+ result as a comparison: with your skill versus without, one model versus another,
48
+ one harness versus another.
49
+
50
+ <img src="docs/assets/benchmark-terminal.gif" width="800" alt="Animated terminal output: benchspec run prints a benchmark matrix with evals as rows, arms as columns, color-coded rates, and percentage-point deltas">
51
+
52
+ *End-of-run summary for a two-arm run of the in-repo [`hello`](evals/e2e/hello/)
53
+ suite (illustrative numbers).* Rows are evals, columns are arms (`baseline` ran
54
+ the agent bare, `trial` installed the skill), and every non-baseline cell shows
55
+ its assertion pass rate plus the delta against the baseline in percentage
56
+ points. The same matrix lands in `benchmark.md`, with machine-readable artifacts
57
+ alongside.
58
+
59
+ Teams pick harnesses, models, and prompts by anecdote: run it once, eyeball the
60
+ transcript, trust the vibe. benchspec turns that guess into a measurement.
61
+ Write the goal once, run it across the configurations you care about, and read
62
+ off — in percentage points — how good each one actually is at accomplishing it.
63
+
64
+ ## Getting Started
65
+
66
+ ```bash
67
+ pip install "benchspec[microsandbox]"
68
+ ```
69
+
70
+ > **Pre-1.0.** The eval format and the artifact schemas are the surfaces most
71
+ > likely to change. microsandbox is the only sandbox backend today; `docker` is
72
+ > recognized in config but fails fast as not implemented.
73
+
74
+ An eval is one Markdown file: a prompt, then a checklist of plain-prose claims
75
+ about the workspace after the agent is done. There is no checker syntax to learn;
76
+ the wording is the spec. `evals/hello/greets-by-name.eval.md`:
77
+
78
+ ```markdown
79
+ ---
80
+ ---
81
+
82
+ ## Prompt
83
+
84
+ You are working in a workspace rooted at your current working directory.
85
+ Greet Alice by name.
86
+
87
+ ## Assertions
88
+
89
+ - [ ] ./Greetings/Alice.md contains the exact line 'Hello, Alice!'
90
+ - [ ] Skill `hello` invoked
91
+ - [ ] The greeting feels warm and personable, not curt or robotic
92
+ ```
93
+
94
+ The benchmark is a block in `pyproject.toml`. Arms are the report columns; the
95
+ baseline is what the others are measured against:
96
+
97
+ ```toml
98
+ [tool.benchspec]
99
+ default-set = "default"
100
+
101
+ [tool.benchspec.sets.default]
102
+ harness = "claude-code"
103
+ model = "sonnet"
104
+ baseline = "baseline"
105
+ arms = [
106
+ { name = "baseline" }, # installs nothing
107
+ { name = "trial" }, # setup.sh installs the skill
108
+ ]
109
+ ```
110
+
111
+ Then:
112
+
113
+ ```bash
114
+ benchspec lint # static checks on the assertions
115
+ benchspec analyze # which assertions grade deterministically, which go to the judge
116
+ benchspec run # every (eval × arm) in its own microVM, graded, reported
117
+ ```
118
+
119
+ ## What you need
120
+
121
+ | | |
122
+ |---|---|
123
+ | Platform | Apple Silicon Mac, or Linux with `/dev/kvm`. Python 3.11+. |
124
+ | Agent CLI | `claude`, `codex`, or `opencode` on `PATH`, with its credential (for Claude Code, `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY`). |
125
+ | `GEMINI_API_KEY` | The binder: a fixed Gemini call that classifies each assertion. Required for every `analyze` and `run`. |
126
+ | Judge credential | The judge runs on the host through an agent CLI; the default is `claude-code` with `sonnet`. Prefer a different vendor from the arms (this repo's own suite judges Claude arms with Codex). |
127
+
128
+ Credentials can live in a repo-root `.env`. A graded run can touch up to three
129
+ vendors: the agent's, Gemini for the binder, and the judge's. `lint` is free;
130
+ `analyze` and `run` spend API calls, and `run` also boots VMs. Preflight lists
131
+ every missing piece and exits before anything is spent.
132
+
133
+ ## How a run works
134
+
135
+ ```mermaid
136
+ flowchart LR
137
+ E["greets-by-name.eval.md<br/>prompt + assertions"] --> A1["arm: baseline<br/>fresh microVM,<br/>setup.sh installs nothing"]
138
+ E --> A2["arm: trial<br/>fresh microVM,<br/>setup.sh installs the hello skill"]
139
+ A1 --> F1["facts: files, SHAs,<br/>final message, tool calls"]
140
+ A2 --> F2["facts"]
141
+ F1 --> G["binder: deterministic checkers<br/>everything else: LLM judge"]
142
+ F2 --> G
143
+ G --> R["benchmark.md + benchmark.json<br/>meta.json + index.jsonl"]
144
+ ```
145
+
146
+ Each `(eval × arm)` pair is one parametrized pytest test. A cell:
147
+
148
+ 1. **Boots a microVM** from a cached snapshot with the agent CLI already
149
+ installed. The first run builds the snapshot (a few minutes); later runs
150
+ reuse it, or pay the cost up front with `benchspec sandbox:build`.
151
+ 2. **Seeds the clean room** — the eval's optional `workspace/` files land in a
152
+ fresh directory mounted at `/workspace`, the agent's working directory.
153
+ 3. **Runs `setup.sh`**, where arms diverge: it sees `$BENCHSPEC_ARM`, so the
154
+ baseline branch exits early and the trial branch copies the skill into place.
155
+ 4. **Invokes the agent** on the eval's prompt.
156
+ 5. **Collects the facts** — file tree, contents, SHA-256s, the final message,
157
+ the tool calls.
158
+ 6. **Grades** — the binder maps each assertion to a deterministic checker where
159
+ it can do so without risk; the judge grades everything else from the
160
+ collected evidence alone.
161
+
162
+ Two guarantees hold throughout. Nothing in the guest can write back to your
163
+ checkout: `setup.sh` reaches the skill under test through a read-only staged
164
+ copy of your repo at `/project` (what a `git clone` would contain — never
165
+ `.env`, `.git`, or earlier runs' artifacts). And provider credentials are
166
+ injected at the network boundary, never as readable environment variables in
167
+ the guest.
168
+
169
+ `benchspec run` is pytest underneath, and everything after `--` goes to
170
+ pytest verbatim: `benchspec run -- -k greets-by-name` (equivalently
171
+ `pytest -k greets-by-name`) runs one eval, `-n 8` fans cells across eight
172
+ microVMs, and `--count 5` samples each cell five times so the report can flag a
173
+ delta that sits within noise.
174
+
175
+ ## Why benchspec
176
+
177
+ - **Comparison is first-class.** A single pass rate is a number without a
178
+ reference point. Arms and a baseline make the headline a delta; skip the
179
+ baseline when absolute rates are what you want.
180
+ - **Deterministic where possible, judged where necessary.** The binder is tuned
181
+ so a false positive, a surface check passing on wrong output, is the one
182
+ unacceptable error; anything doubtful goes to the judge, which sees the
183
+ collected evidence and never grades from recall.
184
+ - **Self-describing artifacts.** Every run writes `meta.json` (planned config
185
+ plus observed provenance, down to the agent version inside the guest),
186
+ `index.jsonl` (one row per sample), and `benchmark.json`, so other tools can
187
+ aggregate runs without knowing the directory layout.
188
+
189
+ ## Documentation
190
+
191
+ | | |
192
+ |---|---|
193
+ | [`docs/quickstart.md`](docs/quickstart.md) | Empty directory to a graded two-arm run. |
194
+ | [`docs/concepts.md`](docs/concepts.md) | The vocabulary: eval, arm, set, baseline, binder, judge. |
195
+ | [`docs/writing-evals.md`](docs/writing-evals.md) | The eval format, workspaces, `setup.sh`, and how grading decides what binds. |
196
+ | [`docs/configuration.md`](docs/configuration.md) | Sets, arms, the judge, every CLI flag, exit codes. |
197
+ | [`docs/sandbox.md`](docs/sandbox.md) | Snapshots, mounts, credentials, host requirements. |
198
+ | [`docs/results.md`](docs/results.md) | Reading `benchmark.md` and the machine-readable artifacts. |
199
+ | [`docs/harnesses.md`](docs/harnesses.md) | `claude-code`, `codex`, `opencode`, and adding your own. |
200
+
201
+ ## Contributing
202
+
203
+ Issues and pull requests are welcome at
204
+ [github.com/theycallmeswift/benchspec](https://github.com/theycallmeswift/benchspec).
205
+ Until `CONTRIBUTING.md` lands, [`docs/style/development.md`](docs/style/development.md)
206
+ is the code style, and `make test` plus `make lint` are the bar.
207
+
208
+ ## License
209
+
210
+ [MIT](LICENSE).
@@ -0,0 +1,178 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/benchspec-wordmark-dark.svg">
3
+ <img alt="benchspec" src="docs/assets/benchspec-wordmark-light.svg" width="188" height="48">
4
+ </picture>
5
+
6
+ **Benchmark what your agent does, not what it says.**
7
+
8
+ [![CI](https://github.com/theycallmeswift/benchspec/actions/workflows/ci.yml/badge.svg)](https://github.com/theycallmeswift/benchspec/actions/workflows/ci.yml)
9
+ [![PyPI](https://img.shields.io/pypi/v/benchspec)](https://pypi.org/project/benchspec/)
10
+ [![Python](https://img.shields.io/pypi/pyversions/benchspec)](https://pypi.org/project/benchspec/)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
+
13
+ benchspec runs an agent (Claude Code, Codex, or OpenCode) against a task
14
+ in a fresh microVM, checks what it actually did in the workspace, and reports the
15
+ result as a comparison: with your skill versus without, one model versus another,
16
+ one harness versus another.
17
+
18
+ <img src="docs/assets/benchmark-terminal.gif" width="800" alt="Animated terminal output: benchspec run prints a benchmark matrix with evals as rows, arms as columns, color-coded rates, and percentage-point deltas">
19
+
20
+ *End-of-run summary for a two-arm run of the in-repo [`hello`](evals/e2e/hello/)
21
+ suite (illustrative numbers).* Rows are evals, columns are arms (`baseline` ran
22
+ the agent bare, `trial` installed the skill), and every non-baseline cell shows
23
+ its assertion pass rate plus the delta against the baseline in percentage
24
+ points. The same matrix lands in `benchmark.md`, with machine-readable artifacts
25
+ alongside.
26
+
27
+ Teams pick harnesses, models, and prompts by anecdote: run it once, eyeball the
28
+ transcript, trust the vibe. benchspec turns that guess into a measurement.
29
+ Write the goal once, run it across the configurations you care about, and read
30
+ off — in percentage points — how good each one actually is at accomplishing it.
31
+
32
+ ## Getting Started
33
+
34
+ ```bash
35
+ pip install "benchspec[microsandbox]"
36
+ ```
37
+
38
+ > **Pre-1.0.** The eval format and the artifact schemas are the surfaces most
39
+ > likely to change. microsandbox is the only sandbox backend today; `docker` is
40
+ > recognized in config but fails fast as not implemented.
41
+
42
+ An eval is one Markdown file: a prompt, then a checklist of plain-prose claims
43
+ about the workspace after the agent is done. There is no checker syntax to learn;
44
+ the wording is the spec. `evals/hello/greets-by-name.eval.md`:
45
+
46
+ ```markdown
47
+ ---
48
+ ---
49
+
50
+ ## Prompt
51
+
52
+ You are working in a workspace rooted at your current working directory.
53
+ Greet Alice by name.
54
+
55
+ ## Assertions
56
+
57
+ - [ ] ./Greetings/Alice.md contains the exact line 'Hello, Alice!'
58
+ - [ ] Skill `hello` invoked
59
+ - [ ] The greeting feels warm and personable, not curt or robotic
60
+ ```
61
+
62
+ The benchmark is a block in `pyproject.toml`. Arms are the report columns; the
63
+ baseline is what the others are measured against:
64
+
65
+ ```toml
66
+ [tool.benchspec]
67
+ default-set = "default"
68
+
69
+ [tool.benchspec.sets.default]
70
+ harness = "claude-code"
71
+ model = "sonnet"
72
+ baseline = "baseline"
73
+ arms = [
74
+ { name = "baseline" }, # installs nothing
75
+ { name = "trial" }, # setup.sh installs the skill
76
+ ]
77
+ ```
78
+
79
+ Then:
80
+
81
+ ```bash
82
+ benchspec lint # static checks on the assertions
83
+ benchspec analyze # which assertions grade deterministically, which go to the judge
84
+ benchspec run # every (eval × arm) in its own microVM, graded, reported
85
+ ```
86
+
87
+ ## What you need
88
+
89
+ | | |
90
+ |---|---|
91
+ | Platform | Apple Silicon Mac, or Linux with `/dev/kvm`. Python 3.11+. |
92
+ | Agent CLI | `claude`, `codex`, or `opencode` on `PATH`, with its credential (for Claude Code, `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY`). |
93
+ | `GEMINI_API_KEY` | The binder: a fixed Gemini call that classifies each assertion. Required for every `analyze` and `run`. |
94
+ | Judge credential | The judge runs on the host through an agent CLI; the default is `claude-code` with `sonnet`. Prefer a different vendor from the arms (this repo's own suite judges Claude arms with Codex). |
95
+
96
+ Credentials can live in a repo-root `.env`. A graded run can touch up to three
97
+ vendors: the agent's, Gemini for the binder, and the judge's. `lint` is free;
98
+ `analyze` and `run` spend API calls, and `run` also boots VMs. Preflight lists
99
+ every missing piece and exits before anything is spent.
100
+
101
+ ## How a run works
102
+
103
+ ```mermaid
104
+ flowchart LR
105
+ E["greets-by-name.eval.md<br/>prompt + assertions"] --> A1["arm: baseline<br/>fresh microVM,<br/>setup.sh installs nothing"]
106
+ E --> A2["arm: trial<br/>fresh microVM,<br/>setup.sh installs the hello skill"]
107
+ A1 --> F1["facts: files, SHAs,<br/>final message, tool calls"]
108
+ A2 --> F2["facts"]
109
+ F1 --> G["binder: deterministic checkers<br/>everything else: LLM judge"]
110
+ F2 --> G
111
+ G --> R["benchmark.md + benchmark.json<br/>meta.json + index.jsonl"]
112
+ ```
113
+
114
+ Each `(eval × arm)` pair is one parametrized pytest test. A cell:
115
+
116
+ 1. **Boots a microVM** from a cached snapshot with the agent CLI already
117
+ installed. The first run builds the snapshot (a few minutes); later runs
118
+ reuse it, or pay the cost up front with `benchspec sandbox:build`.
119
+ 2. **Seeds the clean room** — the eval's optional `workspace/` files land in a
120
+ fresh directory mounted at `/workspace`, the agent's working directory.
121
+ 3. **Runs `setup.sh`**, where arms diverge: it sees `$BENCHSPEC_ARM`, so the
122
+ baseline branch exits early and the trial branch copies the skill into place.
123
+ 4. **Invokes the agent** on the eval's prompt.
124
+ 5. **Collects the facts** — file tree, contents, SHA-256s, the final message,
125
+ the tool calls.
126
+ 6. **Grades** — the binder maps each assertion to a deterministic checker where
127
+ it can do so without risk; the judge grades everything else from the
128
+ collected evidence alone.
129
+
130
+ Two guarantees hold throughout. Nothing in the guest can write back to your
131
+ checkout: `setup.sh` reaches the skill under test through a read-only staged
132
+ copy of your repo at `/project` (what a `git clone` would contain — never
133
+ `.env`, `.git`, or earlier runs' artifacts). And provider credentials are
134
+ injected at the network boundary, never as readable environment variables in
135
+ the guest.
136
+
137
+ `benchspec run` is pytest underneath, and everything after `--` goes to
138
+ pytest verbatim: `benchspec run -- -k greets-by-name` (equivalently
139
+ `pytest -k greets-by-name`) runs one eval, `-n 8` fans cells across eight
140
+ microVMs, and `--count 5` samples each cell five times so the report can flag a
141
+ delta that sits within noise.
142
+
143
+ ## Why benchspec
144
+
145
+ - **Comparison is first-class.** A single pass rate is a number without a
146
+ reference point. Arms and a baseline make the headline a delta; skip the
147
+ baseline when absolute rates are what you want.
148
+ - **Deterministic where possible, judged where necessary.** The binder is tuned
149
+ so a false positive, a surface check passing on wrong output, is the one
150
+ unacceptable error; anything doubtful goes to the judge, which sees the
151
+ collected evidence and never grades from recall.
152
+ - **Self-describing artifacts.** Every run writes `meta.json` (planned config
153
+ plus observed provenance, down to the agent version inside the guest),
154
+ `index.jsonl` (one row per sample), and `benchmark.json`, so other tools can
155
+ aggregate runs without knowing the directory layout.
156
+
157
+ ## Documentation
158
+
159
+ | | |
160
+ |---|---|
161
+ | [`docs/quickstart.md`](docs/quickstart.md) | Empty directory to a graded two-arm run. |
162
+ | [`docs/concepts.md`](docs/concepts.md) | The vocabulary: eval, arm, set, baseline, binder, judge. |
163
+ | [`docs/writing-evals.md`](docs/writing-evals.md) | The eval format, workspaces, `setup.sh`, and how grading decides what binds. |
164
+ | [`docs/configuration.md`](docs/configuration.md) | Sets, arms, the judge, every CLI flag, exit codes. |
165
+ | [`docs/sandbox.md`](docs/sandbox.md) | Snapshots, mounts, credentials, host requirements. |
166
+ | [`docs/results.md`](docs/results.md) | Reading `benchmark.md` and the machine-readable artifacts. |
167
+ | [`docs/harnesses.md`](docs/harnesses.md) | `claude-code`, `codex`, `opencode`, and adding your own. |
168
+
169
+ ## Contributing
170
+
171
+ Issues and pull requests are welcome at
172
+ [github.com/theycallmeswift/benchspec](https://github.com/theycallmeswift/benchspec).
173
+ Until `CONTRIBUTING.md` lands, [`docs/style/development.md`](docs/style/development.md)
174
+ is the code style, and `make test` plus `make lint` are the bar.
175
+
176
+ ## License
177
+
178
+ [MIT](LICENSE).
@@ -0,0 +1,174 @@
1
+ # Concepts
2
+
3
+ This page is the vocabulary: one short paragraph per term, alphabetized, each
4
+ with a one-line example and a link to the document that goes deepest. Skim the
5
+ index, or come back when a term in another page is unfamiliar.
6
+
7
+ [Arm](#arm) ·
8
+ [Assertion](#assertion) ·
9
+ [Baseline](#baseline) ·
10
+ [Binder](#binder) ·
11
+ [Cell](#cell) ·
12
+ [Checker](#checker) ·
13
+ [Clean room and /workspace](#clean-room-and-workspace) ·
14
+ [Errored versus failed](#errored-versus-failed) ·
15
+ [Eval](#eval) ·
16
+ [Eval set](#eval-set) ·
17
+ [Group](#group) ·
18
+ [Harness](#harness) ·
19
+ [Iteration](#iteration) ·
20
+ [Judge](#judge) ·
21
+ [/project](#project) ·
22
+ [Sample](#sample) ·
23
+ [Sandbox and snapshot](#sandbox-and-snapshot) ·
24
+ [setup.sh](#setupsh) ·
25
+ [Skill](#skill)
26
+
27
+ ## Arm
28
+
29
+ One column of the benchmark: a named configuration of harness, model, effort,
30
+ environment variables, and pass-through CLI arguments. Arms differ from each
31
+ other only in that configuration and in what their `setup.sh` branch installs.
32
+ Example: `{ name = "trial", model = "opus", effort = "high" }`. Reference:
33
+ [configuration.md](configuration.md).
34
+
35
+ ## Assertion
36
+
37
+ One `- [ ]` line in an eval: a single claim about the final workspace, the
38
+ agent's final message, or what the agent did. Assertions are prose, not checker
39
+ syntax; how each one is graded is decided at run time (see **binder**).
40
+ Example: `- [ ] ./Greetings/Alice.md contains the exact line 'Hello, Alice!'`.
41
+
42
+ ## Baseline
43
+
44
+ The arm every other arm's delta is measured against. With a baseline, the report
45
+ headline is a percentage-point delta (`+14pp`); without one, arms report absolute
46
+ pass rates. Example: `baseline = "baseline"`, an arm whose `setup.sh` installs
47
+ nothing. Reading deltas: [results.md](results.md).
48
+
49
+ ## Binder
50
+
51
+ A fixed, deliberately conservative classifier (one call to
52
+ `gemini-3.5-flash-lite` per assertion, which is why `GEMINI_API_KEY` is always
53
+ required) that decides, at grade time, whether an assertion can be checked
54
+ mechanically. It either **binds** the line to one checker or **punts** it to the
55
+ judge. It never grades anything itself. Reference:
56
+ [writing-evals.md](writing-evals.md#how-assertions-are-graded).
57
+
58
+ ## Cell
59
+
60
+ One `(eval × arm)` pair. Each cell becomes one parametrized pytest test that
61
+ boots its own microVM, runs the agent, and grades the result. A two-eval,
62
+ three-arm set has six cells. Example: `test_eval[hello-greets-by-name-trial]`.
63
+
64
+ ## Checker
65
+
66
+ One deterministic check run on the host against the final workspace or the
67
+ run's process facts: `file_exists`, `not_file_exists`, `glob_count`, `regex`,
68
+ `frontmatter_has`, `sha256_match`, `skill_invoked`, `not_skill_invoked`. Zero
69
+ variance, zero judge cost. Example: "./out/report.md exists" binds to
70
+ `file_exists`.
71
+
72
+ ## Clean room and `/workspace`
73
+
74
+ The clean room is a fresh temporary directory on the host, seeded from the
75
+ eval's `workspace/` folder (or empty), that is mounted read-write into the
76
+ microVM at `/workspace`. It is the agent's working directory, and the host
77
+ grades the same directory afterward. Every path in an eval is written
78
+ `./`-relative to it. Example: an eval with `workspace/request.md` starts the
79
+ agent in a directory containing exactly `request.md`.
80
+
81
+ ## Errored versus failed
82
+
83
+ A **failed** assertion is a measurement: the agent ran and the claim did not
84
+ hold. An **errored** sample is infrastructure: the CLI crashed or timed out,
85
+ `setup.sh` exited non-zero, or the judge failed at the transport level. Errored
86
+ samples are excluded from pass rates but counted in the report, so a
87
+ half-crashed run cannot read like a clean one. See
88
+ [results.md](results.md#noise-samples-and-flakiness).
89
+
90
+ ## Eval
91
+
92
+ One task and what success looks like: a prompt the agent receives, an optional
93
+ `history:` of prior turns, and a checklist of plain-prose **assertions**. An eval
94
+ is one Markdown file, `eval.md` or `<stem>.eval.md`, and its id is the folder
95
+ name or the file stem. Example: `evals/hello/greets-by-name.eval.md` asks the
96
+ agent to "Greet Alice by name" and asserts that `./Greetings/Alice.md` contains
97
+ `Hello, Alice!`. Format reference: [writing-evals.md](writing-evals.md).
98
+
99
+ ## Eval set
100
+
101
+ A named benchmark declared in `pyproject.toml`: its arms, the defaults they
102
+ inherit, and an optional baseline. One run resolves exactly one set. Example:
103
+ `[tool.benchspec.sets.default]` with arms `baseline` and `trial`. Reference:
104
+ [configuration.md](configuration.md#eval-sets).
105
+
106
+ ## Group
107
+
108
+ The folder an eval file sits in. Its name is the first half of an eval's
109
+ identity, `(group, eval_id)`, and the artifact tree is keyed on it. Sibling
110
+ `<stem>.eval.md` files in one folder share that folder's `workspace/` and
111
+ `setup.sh`. Example: `evals/hello/greets-by-name.eval.md` is eval
112
+ `greets-by-name` in group `hello`, and pytest names its cells
113
+ `test_eval[hello-greets-by-name-<arm>]`.
114
+
115
+ ## Harness
116
+
117
+ The agent CLI under test: `claude-code`, `codex`, or `opencode`. Each is
118
+ one adapter that knows how to install the CLI into a microVM, which credential it
119
+ needs, how to run it headless, and how to read its output stream. Chosen per arm.
120
+ Details and how to add one: [harnesses.md](harnesses.md).
121
+
122
+ ## Iteration
123
+
124
+ One run's artifact tree, `tmp/evals/iteration_NN/`, numbered once per run and
125
+ shared by every cell in it. It holds `benchmark.md`, `benchmark.json`,
126
+ `meta.json`, `index.jsonl`, and every sample's per-cell files. Reference:
127
+ [results.md](results.md).
128
+
129
+ ## Judge
130
+
131
+ The LLM that grades every punted assertion, from evidence only: the workspace
132
+ tree, file contents and SHA-256s, the agent's final message, and the tools and
133
+ skills it invoked. The judge runs on the host through one of the same harness
134
+ adapters, is configured once per run, and is independent of the arms. Example:
135
+ `[tool.benchspec.judge] harness = "codex"`. Reference:
136
+ [configuration.md](configuration.md#the-judge).
137
+
138
+ ## `/project`
139
+
140
+ A read-only mount of a staged copy of your repository (what a `git clone` would
141
+ contain, minus `.env` files, `.git`, and `tmp/`). It exists so the eval's own
142
+ `setup.sh` can copy the skill under test into the guest. The agent can read it
143
+ too, so keep prompts pointed at `./`. Details:
144
+ [sandbox.md](sandbox.md#what-project-contains).
145
+
146
+ ## Sample
147
+
148
+ One execution of a cell. A plain run takes one sample per cell (`sample-0/`);
149
+ `--count N` takes N, so the report can show flakiness and a noise band on each
150
+ delta. Example: `benchspec run -- --count 5`. See
151
+ [results.md](results.md#noise-samples-and-flakiness).
152
+
153
+ ## Sandbox and snapshot
154
+
155
+ Every cell runs inside a microVM, a small hardware-isolated virtual machine
156
+ booted from a **snapshot**: a sealed image with the base OS, the harness CLI,
157
+ and any suite-wide tools already installed. Snapshots build once per
158
+ configuration and are cached; cells boot from them in seconds. Reference:
159
+ [sandbox.md](sandbox.md).
160
+
161
+ ## `setup.sh`
162
+
163
+ An optional script beside the eval file that runs inside the microVM before the
164
+ prompt, with `BENCHSPEC_ARM` set to the arm's name. It is the one place arms
165
+ diverge: the canonical script installs a skill on `trial` and exits early on
166
+ `baseline`. Reference:
167
+ [writing-evals.md](writing-evals.md#setupsh-what-differs-per-arm).
168
+
169
+ ## Skill
170
+
171
+ An instruction file (`SKILL.md`) an agent can load and dispatch; in the common
172
+ "capability lift" benchmark, the skill is the thing under test. Skills install to
173
+ the fixed guest path `/home/benchspec/skills`, which every harness's native
174
+ skill directory links to. Example assertion: `` - [ ] Skill `hello` invoked ``.