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.
Files changed (84) hide show
  1. {evalproof-0.3.0 → evalproof-0.4.1}/CHANGELOG.md +111 -1
  2. {evalproof-0.3.0 → evalproof-0.4.1}/PKG-INFO +222 -5
  3. {evalproof-0.3.0 → evalproof-0.4.1}/README.md +221 -4
  4. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/_version.py +1 -1
  5. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/cli.py +59 -1
  6. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/compare.py +29 -0
  7. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/config.py +3 -0
  8. evalproof-0.4.1/src/evalproof/doctor.py +152 -0
  9. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/gate.py +20 -6
  10. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/providers/__init__.py +40 -2
  11. evalproof-0.4.1/src/evalproof/providers/agent_cli.py +617 -0
  12. evalproof-0.4.1/src/evalproof/providers/agent_cli_experimental.py +617 -0
  13. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/providers/base.py +8 -2
  14. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/report.py +11 -0
  15. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/runner.py +73 -6
  16. evalproof-0.4.1/src/evalproof/scaffold.py +369 -0
  17. evalproof-0.4.1/src/evalproof/templates/SKILL.md +86 -0
  18. evalproof-0.4.1/src/evalproof/templates/cases.jsonl +3 -0
  19. evalproof-0.4.1/src/evalproof/templates/evals-readme.md +11 -0
  20. evalproof-0.4.1/src/evalproof/templates/gitignore +3 -0
  21. evalproof-0.4.1/src/evalproof/templates/suite.yaml +70 -0
  22. evalproof-0.4.1/src/evalproof/templates/workflow-api-key.yml +74 -0
  23. evalproof-0.4.1/src/evalproof/templates/workflow-keyless.yml +67 -0
  24. evalproof-0.4.1/tests/fakecli.py +260 -0
  25. evalproof-0.4.1/tests/fakecli_experimental.py +336 -0
  26. evalproof-0.4.1/tests/test_agent_cli.py +810 -0
  27. evalproof-0.4.1/tests/test_agent_cli_experimental.py +1175 -0
  28. evalproof-0.4.1/tests/test_doctor.py +251 -0
  29. evalproof-0.4.1/tests/test_init.py +699 -0
  30. evalproof-0.4.1/tests/test_judge_fingerprint.py +272 -0
  31. evalproof-0.4.1/tests/test_live_agent_cli.py +47 -0
  32. {evalproof-0.3.0 → evalproof-0.4.1}/.gitignore +0 -0
  33. {evalproof-0.3.0 → evalproof-0.4.1}/CITATION.cff +0 -0
  34. {evalproof-0.3.0 → evalproof-0.4.1}/LICENSE +0 -0
  35. {evalproof-0.3.0 → evalproof-0.4.1}/NOTICE +0 -0
  36. {evalproof-0.3.0 → evalproof-0.4.1}/pyproject.toml +0 -0
  37. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/__init__.py +0 -0
  38. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/cache.py +0 -0
  39. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/calibration.py +0 -0
  40. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/dataset.py +0 -0
  41. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/importers/__init__.py +0 -0
  42. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/importers/_common.py +0 -0
  43. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/importers/deepeval.py +0 -0
  44. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/importers/inspect_ai.py +0 -0
  45. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/importers/lm_eval.py +0 -0
  46. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/importers/promptfoo.py +0 -0
  47. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/importers/table.py +0 -0
  48. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/models.py +0 -0
  49. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/providers/anthropic.py +0 -0
  50. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/providers/ollama.py +0 -0
  51. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/providers/openai.py +0 -0
  52. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/providers/profiles.py +0 -0
  53. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/py.typed +0 -0
  54. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/scorers/__init__.py +0 -0
  55. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/scorers/deterministic.py +0 -0
  56. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/scorers/judge.py +0 -0
  57. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/scorers/retrieval.py +0 -0
  58. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/stats.py +0 -0
  59. {evalproof-0.3.0 → evalproof-0.4.1}/src/evalproof/systems.py +0 -0
  60. {evalproof-0.3.0 → evalproof-0.4.1}/tests/__init__.py +0 -0
  61. {evalproof-0.3.0 → evalproof-0.4.1}/tests/conftest.py +0 -0
  62. {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/README.md +0 -0
  63. {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/deepeval/test_run.json +0 -0
  64. {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/inspect/log_formats.json +0 -0
  65. {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/lm-eval/gpt2/samples_hellaswag_2024-05-01T10-20-30.123456.jsonl +0 -0
  66. {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/lm-eval/llama3/samples_gsm8k_2024-05-01T10-20-30.123456.jsonl +0 -0
  67. {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/promptfoo/sample-export.json +0 -0
  68. {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/table/long.csv +0 -0
  69. {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/table/long.jsonl +0 -0
  70. {evalproof-0.3.0 → evalproof-0.4.1}/tests/fixtures/table/wide.csv +0 -0
  71. {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_action.py +0 -0
  72. {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_cache_systems.py +0 -0
  73. {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_calibration.py +0 -0
  74. {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_compare_gate_report.py +0 -0
  75. {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_config_dataset.py +0 -0
  76. {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_holm_gate.py +0 -0
  77. {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_importers.py +0 -0
  78. {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_providers.py +0 -0
  79. {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_public_api.py +0 -0
  80. {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_release_fixes.py +0 -0
  81. {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_runner_cli.py +0 -0
  82. {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_scorers.py +0 -0
  83. {evalproof-0.3.0 → evalproof-0.4.1}/tests/test_security.py +0 -0
  84. {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.3.0...HEAD
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.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, or the runs used different data |
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.3.0
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,