evalproof 0.3.0__tar.gz → 0.4.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.
- {evalproof-0.3.0 → evalproof-0.4.1}/CHANGELOG.md +111 -1
- {evalproof-0.3.0 → evalproof-0.4.1}/PKG-INFO +222 -5
- {evalproof-0.3.0 → evalproof-0.4.1}/README.md +221 -4
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/_version.py +1 -1
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/cli.py +59 -1
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/compare.py +29 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/config.py +3 -0
- evalproof-0.4.1/src/evalproof/doctor.py +152 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/gate.py +20 -6
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/providers/__init__.py +40 -2
- evalproof-0.4.1/src/evalproof/providers/agent_cli.py +617 -0
- evalproof-0.4.1/src/evalproof/providers/agent_cli_experimental.py +617 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/providers/base.py +8 -2
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/report.py +11 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/runner.py +73 -6
- evalproof-0.4.1/src/evalproof/scaffold.py +369 -0
- evalproof-0.4.1/src/evalproof/templates/SKILL.md +86 -0
- evalproof-0.4.1/src/evalproof/templates/cases.jsonl +3 -0
- evalproof-0.4.1/src/evalproof/templates/evals-readme.md +11 -0
- evalproof-0.4.1/src/evalproof/templates/gitignore +3 -0
- evalproof-0.4.1/src/evalproof/templates/suite.yaml +70 -0
- evalproof-0.4.1/src/evalproof/templates/workflow-api-key.yml +74 -0
- evalproof-0.4.1/src/evalproof/templates/workflow-keyless.yml +67 -0
- evalproof-0.4.1/tests/fakecli.py +260 -0
- evalproof-0.4.1/tests/fakecli_experimental.py +336 -0
- evalproof-0.4.1/tests/test_agent_cli.py +810 -0
- evalproof-0.4.1/tests/test_agent_cli_experimental.py +1175 -0
- evalproof-0.4.1/tests/test_doctor.py +251 -0
- evalproof-0.4.1/tests/test_init.py +699 -0
- evalproof-0.4.1/tests/test_judge_fingerprint.py +272 -0
- evalproof-0.4.1/tests/test_live_agent_cli.py +47 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/.gitignore +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/CITATION.cff +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/LICENSE +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/NOTICE +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/pyproject.toml +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/__init__.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/cache.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/calibration.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/dataset.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/importers/__init__.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/importers/_common.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/importers/deepeval.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/importers/inspect_ai.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/importers/lm_eval.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/importers/promptfoo.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/importers/table.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/models.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/providers/anthropic.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/providers/ollama.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/providers/openai.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/providers/profiles.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/py.typed +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/scorers/__init__.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/scorers/deterministic.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/scorers/judge.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/scorers/retrieval.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/stats.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/systems.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/__init__.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/conftest.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/README.md +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/deepeval/test_run.json +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/inspect/log_formats.json +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/lm-eval/gpt2/samples_hellaswag_2024-05-01T10-20-30.123456.jsonl +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/lm-eval/llama3/samples_gsm8k_2024-05-01T10-20-30.123456.jsonl +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/promptfoo/sample-export.json +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/table/long.csv +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/table/long.jsonl +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/table/wide.csv +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_action.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_cache_systems.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_calibration.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_compare_gate_report.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_config_dataset.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_holm_gate.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_importers.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_providers.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_public_api.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_release_fixes.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_runner_cli.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_scorers.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_security.py +0 -0
- {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_stats.py +0 -0
|
@@ -4,6 +4,114 @@ All notable changes are listed here. The project follows [Semantic Versioning](h
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.4.1] — 2026-11-13
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `cursor:` ask mode is not tool-free: it has a read tool that runs without approval and reads
|
|
12
|
+
files in the workspace and outside it. The workspace now carries a `.cursor/cli.json` that
|
|
13
|
+
allows nothing and denies reads (verified inside and outside the workspace), writes and shell
|
|
14
|
+
commands (ask mode has no write or shell tool; those entries are a second guard), and any
|
|
15
|
+
`tool_call` event in the output fails the call as an isolation break.
|
|
16
|
+
- `cursor:` wrote every call's chat to `~/.cursor/chats` and read the user's
|
|
17
|
+
`~/.cursor/cli-config.json`. `CURSOR_CONFIG_DIR` and `CURSOR_DATA_DIR` now point into the
|
|
18
|
+
call's temp root, which keeps the login (verified) and starts from a config that allows
|
|
19
|
+
nothing.
|
|
20
|
+
- `copilot:` kept every call's session in `~/.copilot` and read the user's Copilot config.
|
|
21
|
+
`COPILOT_HOME` now points into the call's temp root, which keeps the login (verified).
|
|
22
|
+
- `copilot:` passed `--available-tools=`, which copilot 1.0.110 ignores, so the model could use
|
|
23
|
+
its tools (it read a file). It now passes an allow-list with one unknown name, which disables
|
|
24
|
+
every tool (verified), and any tool event or tool request in the output fails the call.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `cursor:` uses `--output-format stream-json` (the json result joined the model's preamble
|
|
29
|
+
with the answer).
|
|
30
|
+
- `copilot:` takes the prompt on stdin, prints JSONL, runs with `--no-custom-instructions` and
|
|
31
|
+
`--disable-builtin-mcps`, reports real token counts from `--usage-output-file`, and records
|
|
32
|
+
the model that answered each call: with `copilot:auto` Copilot routes every call, so the run
|
|
33
|
+
artifact shows the routed model, not `auto`.
|
|
34
|
+
- Isolation tags are now `cursor/2` and `copilot/2`, so earlier cached replies for these two
|
|
35
|
+
are not reused.
|
|
36
|
+
- `evalproof doctor` warns that `cursor-agent` sends the names and descriptions of the user's
|
|
37
|
+
skills (`~/.claude/skills`, `~/.cursor/skills` and others) in every prompt, which no option
|
|
38
|
+
turns off, and that `copilot:auto` as the judge makes compare and the gate refuse runs as a
|
|
39
|
+
judge change (Copilot picks the model per call).
|
|
40
|
+
|
|
41
|
+
### Documentation
|
|
42
|
+
|
|
43
|
+
- The experimental providers section now says what was verified and what was not: Cursor
|
|
44
|
+
reads instruction files from the directories above the workspace; Cursor and Copilot keep
|
|
45
|
+
their own system prompt; Copilot's `--no-custom-instructions` was verified with canary
|
|
46
|
+
files; the Copilot tool guard has not fired live; named-model availability and the
|
|
47
|
+
premium-request rate depend on plan and policy; with tools off Copilot may write a made-up
|
|
48
|
+
tool call as text.
|
|
49
|
+
|
|
50
|
+
## [0.4.0] — 2026-11-13
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- `claude-code:<model>` and `codex:<model>` providers: run your own logged-in Claude Code or
|
|
55
|
+
Codex CLI as a judge or a system under test, with no API key. Each call runs in a fresh
|
|
56
|
+
empty directory with the agent's tools, settings, MCP servers, skills and memory switched
|
|
57
|
+
off and no permission-bypass flag; API-key variables are removed from the child's
|
|
58
|
+
environment unless `api_key_env` names one or `EVALPROOF_CLI_AUTH=api-key` keeps them.
|
|
59
|
+
Neither CLI takes a temperature, seed or token limit, so those params are ignored (Codex
|
|
60
|
+
maps `think` to its reasoning effort). Claude Code runs with no tools; Codex still offers a
|
|
61
|
+
JavaScript isolate, sub-agents and a question to the user, so any item in its output other
|
|
62
|
+
than a message, reasoning, an error or a plan fails the call as an isolation break, and a
|
|
63
|
+
codex too old for the flags evalproof sets gets a message to upgrade. ADR 0004 has the
|
|
64
|
+
reasoning.
|
|
65
|
+
- `EVALPROOF_CLI_AUTH` (`login`, the default, or `api-key`) chooses whether a CLI provider
|
|
66
|
+
uses the subscription login or its own API key variable; `evalproof doctor` prints the mode
|
|
67
|
+
in effect and warns when `api-key` has no key to use. `api_key_env` still wins.
|
|
68
|
+
- `evalproof doctor [suite.yaml]`: reports each CLI's path, version, login mode and the
|
|
69
|
+
isolation flags it will use, without a model call, and exits 1 when a target of the suite
|
|
70
|
+
is not usable. Without a suite it surveys the installed CLIs and exits 1 only when none is
|
|
71
|
+
usable. It prints no account details.
|
|
72
|
+
- Run facts for CLI targets in `runner.environment`: `{judge,system}_cli`,
|
|
73
|
+
`{judge,system}_cli_version`, `{judge,system}_isolation` and `{judge,system}_model_resolved`
|
|
74
|
+
(the model ids that answered, sorted, cached replies included). The response cache keys on
|
|
75
|
+
the CLI fingerprint, so a CLI upgrade is a cache miss.
|
|
76
|
+
- `compare` reports `same_judge`, `judge_changes` and `system_changes`, and the gate refuses
|
|
77
|
+
to decide (exit 4) when the judge changed between the runs. `--allow-judge-change` or
|
|
78
|
+
`gate.allow_judge_change: true` accepts the change and keeps a warning.
|
|
79
|
+
- Experimental `opencode:<provider>/<model>`, `cursor:<model>` and `copilot:<model>`
|
|
80
|
+
providers, on the same footing as the CLI providers above (fresh empty directory, the CLI's
|
|
81
|
+
key variable removed unless `api_key_env` names one, bounded runtime, CLI fingerprint with the
|
|
82
|
+
isolation tags `opencode/0`, `cursor/0`, `copilot/0`). Each prints one warning on stderr the
|
|
83
|
+
first time it is used. Only OpenCode was exercised for real, and only against a local ollama
|
|
84
|
+
model; Cursor (not logged in) and Copilot (not installed) follow their documentation, and the
|
|
85
|
+
README's "Experimental providers" says what was and was not verified. `evalproof doctor`
|
|
86
|
+
without a suite now also lists whichever of these CLIs are on `PATH`.
|
|
87
|
+
- `evalproof.providers.TransientError`, a `ProviderError` that `with_retries` retries; the CLI
|
|
88
|
+
providers raise it for rate limits and server errors.
|
|
89
|
+
- `tests/test_live_agent_cli.py`: one real call per installed CLI, skipped unless
|
|
90
|
+
`EVALPROOF_LIVE=1`.
|
|
91
|
+
- `evalproof init [--dir PATH] [--judge TARGET] [--ci keyless|api-key|none] [--no-skill]
|
|
92
|
+
[--force]`: writes `evals/suite.yaml`, three sample cases in `evals/cases.jsonl`, an
|
|
93
|
+
`evals/README.md`, a GitHub Actions workflow at `.github/workflows/evalproof.yml` and an
|
|
94
|
+
agent skill at `.agents/skills/evalproof/SKILL.md` and `.claude/skills/evalproof/SKILL.md`.
|
|
95
|
+
The judge is the first of `claude`, `codex`, `ollama` found on `PATH`, else a hosted
|
|
96
|
+
Anthropic model. The keyless workflow makes no model calls and gates the committed
|
|
97
|
+
`evals/runs/latest.json` against the base branch; the api-key workflow runs the suite in CI
|
|
98
|
+
(with `EVALPROOF_CLI_AUTH: api-key` for a CLI judge, so no second suite file is written)
|
|
99
|
+
and pins the judge CLI to the version found at init time. Existing files are kept unless
|
|
100
|
+
`--force` is passed, nothing is written outside `--dir`, and no model is called.
|
|
101
|
+
|
|
102
|
+
### Changed
|
|
103
|
+
|
|
104
|
+
- A gate that passed in 0.3.0 can now exit 4 when the judge's target, CLI version, isolation
|
|
105
|
+
or resolved model differs between two runs that both carry these keys. Runs written before
|
|
106
|
+
this release carry none and compare as before.
|
|
107
|
+
- A changed system target or CLI version between two runs now adds a warning, so the status
|
|
108
|
+
is `warn` instead of `pass` (the exit code stays 0).
|
|
109
|
+
- `runner.environment` records `judge_target` and `system_target` for every model target (not
|
|
110
|
+
only CLI ones), and the system target it records is the one in effect, with any override
|
|
111
|
+
applied.
|
|
112
|
+
- The gate's "too few paired cases" reason now says how to fix it (add cases, or lower
|
|
113
|
+
`gate.min_cases`).
|
|
114
|
+
|
|
7
115
|
## [0.3.0] — 2026-11-12
|
|
8
116
|
|
|
9
117
|
### Added
|
|
@@ -179,7 +287,9 @@ First release.
|
|
|
179
287
|
- Two examples: an offline ticket router (CI's gate contract) and a product support
|
|
180
288
|
assistant judged by a local model, with measurements.
|
|
181
289
|
|
|
182
|
-
[Unreleased]: https://github.com/technobasant/evalproof/compare/v0.
|
|
290
|
+
[Unreleased]: https://github.com/technobasant/evalproof/compare/v0.4.1...HEAD
|
|
291
|
+
[0.4.1]: https://github.com/technobasant/evalproof/compare/v0.4.0...v0.4.1
|
|
292
|
+
[0.4.0]: https://github.com/technobasant/evalproof/compare/v0.3.0...v0.4.0
|
|
183
293
|
[0.3.0]: https://github.com/technobasant/evalproof/compare/v0.2.1...v0.3.0
|
|
184
294
|
[0.2.1]: https://github.com/technobasant/evalproof/compare/v0.2.0...v0.2.1
|
|
185
295
|
[0.2.0]: https://github.com/technobasant/evalproof/compare/v0.1.2...v0.2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: evalproof
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Treat LLM evaluation as measurement: calibrate the judge, correct the pass rate, gate CI on significant regressions.
|
|
5
5
|
Project-URL: Homepage, https://github.com/technobasant/evalproof
|
|
6
6
|
Project-URL: Documentation, https://github.com/technobasant/evalproof#readme
|
|
@@ -100,6 +100,23 @@ questions, one Apple M5. Full method and every table in [MEASUREMENTS.md](https:
|
|
|
100
100
|
|
|
101
101
|
## Five-minute quickstart
|
|
102
102
|
|
|
103
|
+
In your own repository, one command writes a starter suite, a CI workflow and an agent skill,
|
|
104
|
+
and a second runs it:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
uvx evalproof init
|
|
108
|
+
uvx evalproof run evals/suite.yaml --out evals/runs/latest.json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`init` picks the judge from what is on your `PATH`: a logged-in `claude`, then `codex`, then
|
|
112
|
+
Ollama, then a hosted Anthropic model with `ANTHROPIC_API_KEY`. It writes `evals/` (the
|
|
113
|
+
suite, three sample cases, a README), `.github/workflows/evalproof.yml` (`--ci keyless`, the
|
|
114
|
+
default, makes no model calls in CI; `--ci api-key` runs the suite there; `--ci none` writes
|
|
115
|
+
no workflow) and a skill for coding agents at `.agents/skills/evalproof/SKILL.md` and
|
|
116
|
+
`.claude/skills/evalproof/SKILL.md` (`--no-skill` skips it). It prints one line per file,
|
|
117
|
+
never overwrites one without `--force`, and makes no model call. Replace the sample cases
|
|
118
|
+
with your own: the gate needs 20 before it will decide.
|
|
119
|
+
|
|
103
120
|
The offline example lives in the repository:
|
|
104
121
|
|
|
105
122
|
```bash
|
|
@@ -135,7 +152,11 @@ The target is `<provider>:<model>`; the model name is everything after the first
|
|
|
135
152
|
`openrouter:anthropic/claude-sonnet-4.5` and `ollama:qwen3:8b` both work. Two native adapters
|
|
136
153
|
(`ollama`, `anthropic`) and one OpenAI-compatible adapter driven by a table of provider
|
|
137
154
|
profiles ([`providers/profiles.py`](https://github.com/technobasant/evalproof/blob/main/src/evalproof/providers/profiles.py), each row cites the
|
|
138
|
-
provider's documentation). Plain HTTP throughout, no vendor SDK.
|
|
155
|
+
provider's documentation). Plain HTTP throughout, no vendor SDK. Two more targets run a coding
|
|
156
|
+
agent you already have logged in, `claude-code` and `codex`: see
|
|
157
|
+
[Use your Claude Code or Codex login](#use-your-claude-code-or-codex-login). Three experimental
|
|
158
|
+
ones run OpenCode, Cursor and GitHub Copilot the same way: see
|
|
159
|
+
[Experimental providers](#experimental-providers).
|
|
139
160
|
|
|
140
161
|
| Provider | Example target | Key variable | Where |
|
|
141
162
|
|---|---|---|---|
|
|
@@ -156,6 +177,11 @@ provider's documentation). Plain HTTP throughout, no vendor SDK.
|
|
|
156
177
|
| `openrouter` | `openrouter:anthropic/claude-sonnet-4.5` | `OPENROUTER_API_KEY` | hosted |
|
|
157
178
|
| `cerebras` | `cerebras:gpt-oss-120b` | `CEREBRAS_API_KEY` | hosted |
|
|
158
179
|
| `openai-compatible` | `openai-compatible:<model>` | optional, via `api_key_env` | any server, needs `base_url` |
|
|
180
|
+
| `claude-code` | `claude-code:haiku` | none: your Claude Code login | local CLI, see below |
|
|
181
|
+
| `codex` | `codex:gpt-6-luna` | none: your ChatGPT login | local CLI, see below |
|
|
182
|
+
| `opencode` | `opencode:ollama/qwen3:8b` | none: OpenCode's own login, per model provider | local CLI, experimental |
|
|
183
|
+
| `cursor` | `cursor:auto` | none: your Cursor login | local CLI, experimental |
|
|
184
|
+
| `copilot` | `copilot:auto` | none: your GitHub Copilot login | local CLI, experimental |
|
|
159
185
|
|
|
160
186
|
```yaml
|
|
161
187
|
system:
|
|
@@ -186,6 +212,187 @@ system:
|
|
|
186
212
|
* AWS Bedrock and Google Vertex AI sign requests with cloud credentials, which evalproof does
|
|
187
213
|
not implement. Use a gateway's OpenAI-compatible endpoint through `openai-compatible`.
|
|
188
214
|
|
|
215
|
+
## Use your Claude Code or Codex login
|
|
216
|
+
|
|
217
|
+
If Claude Code or Codex is already logged in on your machine, either can be the judge or the
|
|
218
|
+
system under test, with no API key:
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
judge:
|
|
222
|
+
target: claude-code:haiku # an alias (haiku, sonnet, opus) or a full model id
|
|
223
|
+
system:
|
|
224
|
+
target: codex:gpt-6-luna
|
|
225
|
+
concurrency: 4
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Run `evalproof doctor suite.yaml` first. It prints each CLI's path, version, login mode and
|
|
229
|
+
the isolation flags it will pass, and exits 1 if a target of the suite is not usable. Without a
|
|
230
|
+
suite it surveys every CLI it knows that is installed, and exits 1 only when none is usable. It
|
|
231
|
+
makes no model call.
|
|
232
|
+
|
|
233
|
+
evalproof runs your own `claude` or `codex` binary as a subprocess, sends the prompt on stdin
|
|
234
|
+
and reads the JSON it prints. It never reads a token file or the keychain. The environment
|
|
235
|
+
variable `EVALPROOF_CLI_AUTH` says which credentials the CLI uses:
|
|
236
|
+
|
|
237
|
+
* `login` (the default, also when it is unset or empty) removes the CLI's key variables from
|
|
238
|
+
the call (`ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` for Claude Code, `OPENAI_API_KEY`
|
|
239
|
+
and `CODEX_API_KEY` for Codex), so the login is what gets used.
|
|
240
|
+
* `api-key` leaves them in place, so the CLI uses its own key. This is the route for CI.
|
|
241
|
+
* Any other value is an error.
|
|
242
|
+
|
|
243
|
+
`api_key_env` in the suite works in either mode and wins: it passes that variable to the CLI as
|
|
244
|
+
its own key variable. The mode is not part of the run's fingerprint or the cache key, because
|
|
245
|
+
it is the same model either way. `evalproof doctor` prints the mode in effect and warns when
|
|
246
|
+
`api-key` is chosen but the CLI's key variable is not set.
|
|
247
|
+
|
|
248
|
+
* **Isolation is a requirement, not an option.** A coding agent is not a model endpoint. Left
|
|
249
|
+
alone it loads the project's `CLAUDE.md` or `AGENTS.md`, your memory, plugins, MCP servers
|
|
250
|
+
and skills into the prompt, and it can run commands. A judge that has read your project's
|
|
251
|
+
instructions is not a judge. So every call runs in a fresh empty directory, with the
|
|
252
|
+
agent's own system prompt replaced by yours (or one neutral line), its tools, settings, MCP
|
|
253
|
+
servers and skills switched off, and no permission-bypass flag. `evalproof doctor` prints
|
|
254
|
+
the exact flags. The two CLIs do not isolate equally well:
|
|
255
|
+
* Claude Code is run with **no tools at all** (`--tools ''`).
|
|
256
|
+
* Codex runs in a read-only sandbox with its shell, plugins, apps, hooks, multi-agent mode,
|
|
257
|
+
sleep and image tools, bundled skills and your user config and rules switched off, but
|
|
258
|
+
codex-cli 0.167.2 has no flag that removes every tool. Still offered are a JavaScript
|
|
259
|
+
isolate (`exec`: no Node, no file system, no network, with a few built-in helpers such as
|
|
260
|
+
the current time, MCP resource listing and `apply_patch`), sub-agents that get the same
|
|
261
|
+
restricted tools, and a question to the user, which cannot be answered in exec mode. **Any
|
|
262
|
+
tool use fails the call**: if the output holds an item that is not a message, reasoning,
|
|
263
|
+
an error or a plan, evalproof raises `codex used a tool (<type>); isolation broken` and
|
|
264
|
+
discards the reply. A codex too old to know a flag evalproof sets exits with `Unknown
|
|
265
|
+
feature flag`, and evalproof says to upgrade it (it was tested with codex-cli 0.167.2).
|
|
266
|
+
* **No temperature, seed or token limit.** Neither CLI exposes one, so `temperature`, `seed`
|
|
267
|
+
and `max_tokens` are ignored. `think` maps to Codex's reasoning effort (`false` is `low`,
|
|
268
|
+
`true` is `medium`; `params.extra.reasoning_effort` sets it directly). A run is therefore
|
|
269
|
+
not repeatable the way a seeded local run is, so the run records the CLI, its version, the
|
|
270
|
+
isolation scheme and the model id that actually answered, and the cache keys on the CLI
|
|
271
|
+
version. To see how much a judge wobbles, score the same outputs twice with `--no-cache`, or
|
|
272
|
+
[calibrate](#concepts) it against labels.
|
|
273
|
+
* **A changed judge is not comparable.** `compare` and the gate refuse (exit 4) when the judge's
|
|
274
|
+
target, CLI version, isolation or resolved model differs between the two runs, because the
|
|
275
|
+
scores then come from a different instrument. Pass `--allow-judge-change` or set
|
|
276
|
+
`gate.allow_judge_change: true` to accept it. A changed system target or CLI version only
|
|
277
|
+
warns, since that is usually the change under test. Runs written before 0.4.0 carry none of
|
|
278
|
+
these keys and compare as before.
|
|
279
|
+
* **Latency and cost.** A call takes about 1.4 s on `claude-code:haiku` and about 4 s on
|
|
280
|
+
Codex, mostly start-up, so set `concurrency`. There is no API bill; the calls count against
|
|
281
|
+
your subscription's usage limits.
|
|
282
|
+
* **Terms.** OpenAI documents Codex CLI use on a ChatGPT plan, including automation.
|
|
283
|
+
Anthropic's terms allow a user to run the unmodified Claude Code binary on their own
|
|
284
|
+
subscription; its plan limits assume ordinary individual use, and it forbids third-party
|
|
285
|
+
products from routing other users' requests through subscription credentials. evalproof
|
|
286
|
+
never touches credentials: it runs your own `claude` binary, on your machine, for your own
|
|
287
|
+
evals. For CI or team-wide use, use an API key (`EVALPROOF_CLI_AUTH=api-key`, or
|
|
288
|
+
`api_key_env`). See Anthropic's
|
|
289
|
+
[legal and compliance page](https://code.claude.com/docs/en/legal-and-compliance). Nothing
|
|
290
|
+
uses a CLI unless a suite names a `claude-code:` or `codex:` target (or an experimental one,
|
|
291
|
+
below).
|
|
292
|
+
|
|
293
|
+
### Experimental providers
|
|
294
|
+
|
|
295
|
+
`opencode:`, `cursor:` and `copilot:` follow the same rules as the two above: your own logged-in
|
|
296
|
+
binary, run as a subprocess in a fresh directory in the system temp directory (empty but for
|
|
297
|
+
Cursor's permission file), its API-key variable removed from the child's environment unless
|
|
298
|
+
`api_key_env` names one, a bounded runtime, and the CLI, its version and an isolation tag
|
|
299
|
+
(`opencode/0`, `cursor/2`, `copilot/2`) in the run and in the cache key. They are experimental
|
|
300
|
+
because their isolation is less proven than Claude Code's and Codex's. Each prints
|
|
301
|
+
`evalproof: <name> provider is experimental` on stderr the first time it is used in a process.
|
|
302
|
+
Like the other two they ignore `temperature`, `seed` and `max_tokens`, and `evalproof doctor`
|
|
303
|
+
without a suite lists each of them whose binary is on `PATH`. One difference from the two
|
|
304
|
+
above: Cursor and Copilot have no system-prompt option, so they keep their own agent system
|
|
305
|
+
prompt and your system text goes in front of the prompt under a heading, as an instruction in
|
|
306
|
+
the user turn. What was checked, and what was not:
|
|
307
|
+
|
|
308
|
+
* **`opencode:<provider>/<model>`** runs `opencode run --standalone --archived --format json`
|
|
309
|
+
with a custom agent that has every tool off and every permission denied, your system text as
|
|
310
|
+
that agent's prompt, and project config, the global config directory and the Claude Code
|
|
311
|
+
compatibility files switched off. The prompt is on stdin. *Verified* only against local
|
|
312
|
+
ollama models (`qwen3:8b`, a handful of calls): no tool event, a canary `AGENTS.md` in the
|
|
313
|
+
project not read, a canary `AGENTS.md` in the global config directory read by default and not
|
|
314
|
+
read with these settings, the parser and the error path. *Not verified:* any hosted model;
|
|
315
|
+
whether `~/.claude/CLAUDE.md` is read (that test was inconclusive); whether the empty
|
|
316
|
+
`OPENCODE_CONFIG_DIR` hides a login stored there; the token counts (input adds cache reads
|
|
317
|
+
and writes, output adds reasoning, summed per field name). OpenCode has no single login, so
|
|
318
|
+
`doctor` prints `auth: not checked`. Only `OPENCODE_API_KEY` is removed from the environment:
|
|
319
|
+
the key of the model's own provider (`ANTHROPIC_API_KEY` for `opencode:anthropic/...`) is
|
|
320
|
+
left, because naming that provider in the model id is choosing its key, and removing it would
|
|
321
|
+
break CI. `--archived` keeps a session out of OpenCode's lists but does not delete it, so
|
|
322
|
+
sessions accumulate in OpenCode's data directory (each run is a private `--standalone`
|
|
323
|
+
server in a temporary project); prune them yourself.
|
|
324
|
+
* **`cursor:<model>`** runs `cursor-agent -p --output-format stream-json --mode ask --trust`
|
|
325
|
+
with a `--workspace` that holds only a `.cursor/cli.json` permission file (nothing allowed;
|
|
326
|
+
reads, writes and shell commands denied), and with `CURSOR_CONFIG_DIR` and `CURSOR_DATA_DIR`
|
|
327
|
+
pointed at empty directories in the call's temp root. *Verified live* on 2026-11-13 with
|
|
328
|
+
cursor-agent 2026.11.11:
|
|
329
|
+
* Ask mode has no write or shell tool, but it is **not** tool-free: it has a read tool that
|
|
330
|
+
runs without approval and reads files inside the workspace and outside it by absolute path.
|
|
331
|
+
The permission file made every read fail with "Permission denied", inside and outside, but
|
|
332
|
+
the model still tries, so the stream carries a `tool_call` event; evalproof fails the call
|
|
333
|
+
on any such event (`cursor used a tool (readToolCall); isolation broken`) and discards the
|
|
334
|
+
reply. The write and shell entries in the deny list are a second guard for later versions;
|
|
335
|
+
only reads were tested.
|
|
336
|
+
* Cursor reads instruction files (`AGENTS.md`, `CLAUDE.md`, `.cursor/rules`) from the
|
|
337
|
+
workspace **and from the directories above it**. The workspace is empty and sits in the
|
|
338
|
+
system temp directory, so nothing is found there unless `TMPDIR` points inside a project.
|
|
339
|
+
`~/.claude/CLAUDE.md` was not read.
|
|
340
|
+
* **Your skills list is sent.** Cursor puts the names and descriptions of the skills under
|
|
341
|
+
your home directory (`~/.claude/skills`, `~/.cursor/skills`, `~/.codex/skills`,
|
|
342
|
+
`~/.agents/skills`, plugin skills) and its own subagents in every prompt. The paths are
|
|
343
|
+
fixed and no setting turns them off; moving `HOME` logs the CLI out. They count as input
|
|
344
|
+
tokens on every call, and `doctor` warns about it. Verified with a login; with
|
|
345
|
+
`CURSOR_API_KEY` it was not tested.
|
|
346
|
+
* The redirected config and data directories keep the login (it is in the system keychain),
|
|
347
|
+
keep each call's chat out of `~/.cursor/chats` and `~/.cursor/projects`, and mean your own
|
|
348
|
+
`~/.cursor/cli-config.json` (and its allow list) is not read. A fresh config directory gets
|
|
349
|
+
a default allow list with `Shell(ls)`, so evalproof writes one that allows nothing.
|
|
350
|
+
|
|
351
|
+
`stream-json` rather than `json` because the json result joins the model's preamble text
|
|
352
|
+
("I'll read the file now.") with the answer and carries no tool events. Token counts come from
|
|
353
|
+
the result event (output includes reasoning). The model recorded is the one you asked for;
|
|
354
|
+
the result carries no model id. The prompt is an argument (nothing says the CLI reads stdin),
|
|
355
|
+
so it is visible in `ps` while the call runs and a prompt over 100,000 bytes is refused. The
|
|
356
|
+
key variable is `CURSOR_API_KEY`, passed through the environment, never as `--api-key`.
|
|
357
|
+
`doctor` reads `cursor-agent about` for the login state (`status` hung) and prints no account.
|
|
358
|
+
* **`copilot:<model>`** runs `copilot --no-auto-update --no-ask-user --no-custom-instructions
|
|
359
|
+
--disable-builtin-mcps --available-tools=evalproof-no-tools --output-format json
|
|
360
|
+
--usage-output-file <tmp>`, with the prompt on stdin and `COPILOT_HOME` pointed at an empty
|
|
361
|
+
directory in the call's temp root. *Verified live* on 2026-11-13 with copilot 1.0.110:
|
|
362
|
+
* An empty allow-list (`--available-tools=`) is **ignored**: the model called its `view` tool
|
|
363
|
+
and read a file. An allow-list of one name that is no tool disables every tool
|
|
364
|
+
(`session.info: Disabled tools: bash, create, edit, glob, grep, ...`), and that is what
|
|
365
|
+
evalproof passes. Any `tool.execution*` event or tool request in the output also fails the
|
|
366
|
+
call (`copilot used a tool (view); isolation broken`); with tools off it never fired live,
|
|
367
|
+
so that guard is tested only against canned output. No `--allow-*` flag is ever passed.
|
|
368
|
+
With no tools the model sometimes writes a made-up tool call and its "result" as plain
|
|
369
|
+
text; that is reply text, not a tool run, and is scored as such.
|
|
370
|
+
* `--no-custom-instructions` works: without it a canary `AGENTS.md`, `CLAUDE.md` and
|
|
371
|
+
`.github/copilot-instructions.md` in the working directory were all read; with it none was,
|
|
372
|
+
nor an `AGENTS.md` in the directory above. No skills or MCP servers were offered.
|
|
373
|
+
* Copilot keeps its own coding-agent system prompt and adds the date and a note that it is
|
|
374
|
+
not sandboxed; your system text is an instruction in the user turn.
|
|
375
|
+
* `COPILOT_HOME` in the temp root keeps the login (it is in the system keychain) and keeps
|
|
376
|
+
each call's session, logs and config out of `~/.copilot` (verified: no new file there after
|
|
377
|
+
a call). Your own Copilot config, plugins and MCP servers live in that directory, so they
|
|
378
|
+
are not used either; that follows from the redirect and was not tested separately.
|
|
379
|
+
* Models and cost: on the test account named models (`gpt-6-luna`, `claude-haiku-5.5`) were
|
|
380
|
+
rejected with "is not available", and only `copilot:auto` worked; which models you can name
|
|
381
|
+
depends on your plan and your organisation's policy. `auto` routes each call to a model of
|
|
382
|
+
Copilot's choosing (`gpt-6-luna` on one call, `mai-code-1.1-flash` on another), so the run
|
|
383
|
+
artifact records every model that answered, not `auto`, and a suite run on `auto` can mix
|
|
384
|
+
models. `auto` used one premium request per call there; the rate depends on the model and
|
|
385
|
+
the plan.
|
|
386
|
+
* **`copilot:auto` makes a poor judge.** The judge's resolved model is part of what
|
|
387
|
+
[`compare` and the gate check](#use-your-claude-code-or-codex-login): two runs on `auto` often saw
|
|
388
|
+
different models, so they are refused as a judge change (exit 4) unless you allow it.
|
|
389
|
+
`doctor` warns about it. Name a model for the judge if your plan allows one.
|
|
390
|
+
|
|
391
|
+
Token counts are read from the usage file (input already includes cache reads and writes,
|
|
392
|
+
output includes reasoning); a missing file means 0, never a failure. Only
|
|
393
|
+
`COPILOT_GITHUB_TOKEN` is removed; `GH_TOKEN` and `GITHUB_TOKEN` are left because `gh` and CI
|
|
394
|
+
set them for other reasons. `doctor` prints `auth: not checked`.
|
|
395
|
+
|
|
189
396
|
## Concepts
|
|
190
397
|
|
|
191
398
|
**Suite** — one YAML file: a JSONL golden set, the system under test, scorers, an optional
|
|
@@ -210,7 +417,8 @@ gate: {metric: judge, max_drop: 0.03, alpha: 0.05, min_cases: 40, max_p95_latenc
|
|
|
210
417
|
|
|
211
418
|
**Run** — `evalproof run suite.yaml` writes one self-describing JSON file: every case's
|
|
212
419
|
output, latency, tokens and scores, plus the dataset hash, seed, Ollama version and model
|
|
213
|
-
digests
|
|
420
|
+
digests, and for a `claude-code` or `codex` target the CLI, its version and the model that
|
|
421
|
+
answered.
|
|
214
422
|
|
|
215
423
|
**Scorers** — `exact_match`, `contains`, `regex` (accept/reject answer keys),
|
|
216
424
|
`json_schema`, `numeric_tolerance`, `recall_at_k`, `mrr`, `ndcg`, and `llm_judge`: a strict
|
|
@@ -235,7 +443,7 @@ orders and reports order-dependent verdicts as ties.
|
|
|
235
443
|
| 0 | pass — possibly with warnings (a big drop that is not significant; a significant drop within tolerance) |
|
|
236
444
|
| 1 | quality regression: drop ≥ `max_drop` **and** p < `alpha` |
|
|
237
445
|
| 3 | budget breach: candidate p95 latency or cost over the limit |
|
|
238
|
-
| 4 | cannot gate: fewer than `min_cases` paired cases,
|
|
446
|
+
| 4 | cannot gate: fewer than `min_cases` paired cases, the runs used different data, or the judge changed (unless `--allow-judge-change`) |
|
|
239
447
|
| 2 | usage or configuration error |
|
|
240
448
|
|
|
241
449
|
The gate checks one metric by default. To gate several, repeat `--metric` or list them in
|
|
@@ -322,6 +530,7 @@ The architecture diagram is in the [README on GitHub](https://github.com/technob
|
|
|
322
530
|
| Local Ollama by default, HTTP-only providers, content-addressed cache | Hosted APIs and vendor SDKs | Reproducible, free to re-run, tied to a model digest; no SDK dependency | [0002](https://github.com/technobasant/evalproof/blob/main/docs/adr/0002-local-first-providers.md) |
|
|
323
531
|
| Gate on "large AND significant", on paired cases | "Fail if the score dropped by X" | On a few dozen cases, X-point drops are mostly noise, and a noisy gate gets switched off | [0003](https://github.com/technobasant/evalproof/blob/main/docs/adr/0003-gate-on-significance-not-delta.md) |
|
|
324
532
|
| One metric by default; several with Holm correction | Gate every metric at α each | Testing many metrics at α = 0.05 each inflates false failures | 0003 |
|
|
533
|
+
| Run a coding-agent CLI as a provider only in isolation, and gate on its fingerprint | Pass the CLI's defaults through, or ignore its version | A CLI that reads your project's instructions is not a judge, and a silent upgrade changes the instrument | [0004](https://github.com/technobasant/evalproof/blob/main/docs/adr/0004-agent-clis-as-providers.md) |
|
|
325
534
|
| Pointwise verdicts for gating; pairwise as a diagnostic | Pairwise preference for gating | Measured: 42% of pairwise verdicts flipped with answer order, with a strong length preference | [MEASUREMENTS §6](https://github.com/technobasant/evalproof/blob/main/MEASUREMENTS.md) |
|
|
326
535
|
| numpy and the standard library for statistics | SciPy, statsmodels | Small enough to read and to test against hand-worked values | — |
|
|
327
536
|
|
|
@@ -331,7 +540,7 @@ The composite action runs `compare --gate`, appends the report to the job summar
|
|
|
331
540
|
fails the job on exit codes 1, 3 or 4:
|
|
332
541
|
|
|
333
542
|
```yaml
|
|
334
|
-
- uses: technobasant/evalproof@v0.
|
|
543
|
+
- uses: technobasant/evalproof@v0.4.1
|
|
335
544
|
with:
|
|
336
545
|
baseline: eval/baseline.json # produced on main, e.g. as a build artifact
|
|
337
546
|
candidate: eval/candidate.json # produced on this branch
|
|
@@ -342,6 +551,14 @@ fails the job on exit codes 1, 3 or 4:
|
|
|
342
551
|
This repository's own CI runs the offline example on every commit and asserts that the
|
|
343
552
|
gate returns 1 for the real regression and 0 for the noisy and refactored candidates.
|
|
344
553
|
|
|
554
|
+
If the judge is a Claude Code or Codex CLI, `evalproof init --ci api-key` writes a workflow that
|
|
555
|
+
installs the CLI at the version found at init time and runs `evals/suite.yaml` as it is. The run
|
|
556
|
+
step sets `EVALPROOF_CLI_AUTH: api-key` and the repository secret (`ANTHROPIC_API_KEY` for
|
|
557
|
+
Claude Code, `OPENAI_API_KEY` for Codex), so the CLI uses that key instead of a login. The
|
|
558
|
+
suite names no key and there is no second suite file; a suite with an `api_key_env` still uses
|
|
559
|
+
that variable. Keep the pinned CLI version equal to the one in your baseline run, because the
|
|
560
|
+
gate exits 4 when the judge's CLI version differs.
|
|
561
|
+
|
|
345
562
|
## How it relates to other tools
|
|
346
563
|
|
|
347
564
|
[promptfoo](https://www.promptfoo.dev/docs/intro/) (open-source, local evals of prompts,
|