evalproof 0.4.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.4.0 → evalproof-0.4.1}/CHANGELOG.md +45 -1
- {evalproof-0.4.0 → evalproof-0.4.1}/PKG-INFO +83 -30
- {evalproof-0.4.0 → evalproof-0.4.1}/README.md +82 -29
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/_version.py +1 -1
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/doctor.py +22 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/agent_cli_experimental.py +228 -63
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/scaffold.py +1 -1
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/fakecli_experimental.py +136 -7
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_agent_cli_experimental.py +354 -48
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_init.py +2 -2
- {evalproof-0.4.0 → evalproof-0.4.1}/.gitignore +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/CITATION.cff +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/LICENSE +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/NOTICE +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/pyproject.toml +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/__init__.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/cache.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/calibration.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/cli.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/compare.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/config.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/dataset.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/gate.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/importers/__init__.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/importers/_common.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/importers/deepeval.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/importers/inspect_ai.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/importers/lm_eval.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/importers/promptfoo.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/importers/table.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/models.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/__init__.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/agent_cli.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/anthropic.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/base.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/ollama.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/openai.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/profiles.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/py.typed +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/report.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/runner.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/scorers/__init__.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/scorers/deterministic.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/scorers/judge.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/scorers/retrieval.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/stats.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/systems.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/templates/SKILL.md +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/templates/cases.jsonl +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/templates/evals-readme.md +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/templates/gitignore +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/templates/suite.yaml +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/templates/workflow-api-key.yml +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/templates/workflow-keyless.yml +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/__init__.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/conftest.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/fakecli.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/README.md +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/deepeval/test_run.json +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/inspect/log_formats.json +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/lm-eval/gpt2/samples_hellaswag_2024-05-01T10-20-30.123456.jsonl +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/lm-eval/llama3/samples_gsm8k_2024-05-01T10-20-30.123456.jsonl +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/promptfoo/sample-export.json +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/table/long.csv +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/table/long.jsonl +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/table/wide.csv +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_action.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_agent_cli.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_cache_systems.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_calibration.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_compare_gate_report.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_config_dataset.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_doctor.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_holm_gate.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_importers.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_judge_fingerprint.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_live_agent_cli.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_providers.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_public_api.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_release_fixes.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_runner_cli.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_scorers.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_security.py +0 -0
- {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_stats.py +0 -0
|
@@ -4,6 +4,49 @@ 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
|
+
|
|
7
50
|
## [0.4.0] — 2026-11-13
|
|
8
51
|
|
|
9
52
|
### Added
|
|
@@ -244,7 +287,8 @@ First release.
|
|
|
244
287
|
- Two examples: an offline ticket router (CI's gate contract) and a product support
|
|
245
288
|
assistant judged by a local model, with measurements.
|
|
246
289
|
|
|
247
|
-
[Unreleased]: https://github.com/technobasant/evalproof/compare/v0.4.
|
|
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
|
|
248
292
|
[0.4.0]: https://github.com/technobasant/evalproof/compare/v0.3.0...v0.4.0
|
|
249
293
|
[0.3.0]: https://github.com/technobasant/evalproof/compare/v0.2.1...v0.3.0
|
|
250
294
|
[0.2.1]: https://github.com/technobasant/evalproof/compare/v0.2.0...v0.2.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: evalproof
|
|
3
|
-
Version: 0.4.
|
|
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
|
|
@@ -181,7 +181,7 @@ ones run OpenCode, Cursor and GitHub Copilot the same way: see
|
|
|
181
181
|
| `codex` | `codex:gpt-6-luna` | none: your ChatGPT login | local CLI, see below |
|
|
182
182
|
| `opencode` | `opencode:ollama/qwen3:8b` | none: OpenCode's own login, per model provider | local CLI, experimental |
|
|
183
183
|
| `cursor` | `cursor:auto` | none: your Cursor login | local CLI, experimental |
|
|
184
|
-
| `copilot` | `copilot:
|
|
184
|
+
| `copilot` | `copilot:auto` | none: your GitHub Copilot login | local CLI, experimental |
|
|
185
185
|
|
|
186
186
|
```yaml
|
|
187
187
|
system:
|
|
@@ -293,14 +293,17 @@ it is the same model either way. `evalproof doctor` prints the mode in effect an
|
|
|
293
293
|
### Experimental providers
|
|
294
294
|
|
|
295
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
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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:
|
|
304
307
|
|
|
305
308
|
* **`opencode:<provider>/<model>`** runs `opencode run --standalone --archived --format json`
|
|
306
309
|
with a custom agent that has every tool off and every permission denied, your system text as
|
|
@@ -318,25 +321,75 @@ checked, and what was not:
|
|
|
318
321
|
break CI. `--archived` keeps a session out of OpenCode's lists but does not delete it, so
|
|
319
322
|
sessions accumulate in OpenCode's data directory (each run is a private `--standalone`
|
|
320
323
|
server in a temporary project); prune them yourself.
|
|
321
|
-
* **`cursor:<model>`** runs `cursor-agent -p --mode ask --trust`
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
no
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
*
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
340
393
|
`COPILOT_GITHUB_TOKEN` is removed; `GH_TOKEN` and `GITHUB_TOKEN` are left because `gh` and CI
|
|
341
394
|
set them for other reasons. `doctor` prints `auth: not checked`.
|
|
342
395
|
|
|
@@ -487,7 +540,7 @@ The composite action runs `compare --gate`, appends the report to the job summar
|
|
|
487
540
|
fails the job on exit codes 1, 3 or 4:
|
|
488
541
|
|
|
489
542
|
```yaml
|
|
490
|
-
- uses: technobasant/evalproof@v0.4.
|
|
543
|
+
- uses: technobasant/evalproof@v0.4.1
|
|
491
544
|
with:
|
|
492
545
|
baseline: eval/baseline.json # produced on main, e.g. as a build artifact
|
|
493
546
|
candidate: eval/candidate.json # produced on this branch
|
|
@@ -145,7 +145,7 @@ ones run OpenCode, Cursor and GitHub Copilot the same way: see
|
|
|
145
145
|
| `codex` | `codex:gpt-6-luna` | none: your ChatGPT login | local CLI, see below |
|
|
146
146
|
| `opencode` | `opencode:ollama/qwen3:8b` | none: OpenCode's own login, per model provider | local CLI, experimental |
|
|
147
147
|
| `cursor` | `cursor:auto` | none: your Cursor login | local CLI, experimental |
|
|
148
|
-
| `copilot` | `copilot:
|
|
148
|
+
| `copilot` | `copilot:auto` | none: your GitHub Copilot login | local CLI, experimental |
|
|
149
149
|
|
|
150
150
|
```yaml
|
|
151
151
|
system:
|
|
@@ -257,14 +257,17 @@ it is the same model either way. `evalproof doctor` prints the mode in effect an
|
|
|
257
257
|
### Experimental providers
|
|
258
258
|
|
|
259
259
|
`opencode:`, `cursor:` and `copilot:` follow the same rules as the two above: your own logged-in
|
|
260
|
-
binary, run as a subprocess in a fresh
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
260
|
+
binary, run as a subprocess in a fresh directory in the system temp directory (empty but for
|
|
261
|
+
Cursor's permission file), its API-key variable removed from the child's environment unless
|
|
262
|
+
`api_key_env` names one, a bounded runtime, and the CLI, its version and an isolation tag
|
|
263
|
+
(`opencode/0`, `cursor/2`, `copilot/2`) in the run and in the cache key. They are experimental
|
|
264
|
+
because their isolation is less proven than Claude Code's and Codex's. Each prints
|
|
265
|
+
`evalproof: <name> provider is experimental` on stderr the first time it is used in a process.
|
|
266
|
+
Like the other two they ignore `temperature`, `seed` and `max_tokens`, and `evalproof doctor`
|
|
267
|
+
without a suite lists each of them whose binary is on `PATH`. One difference from the two
|
|
268
|
+
above: Cursor and Copilot have no system-prompt option, so they keep their own agent system
|
|
269
|
+
prompt and your system text goes in front of the prompt under a heading, as an instruction in
|
|
270
|
+
the user turn. What was checked, and what was not:
|
|
268
271
|
|
|
269
272
|
* **`opencode:<provider>/<model>`** runs `opencode run --standalone --archived --format json`
|
|
270
273
|
with a custom agent that has every tool off and every permission denied, your system text as
|
|
@@ -282,25 +285,75 @@ checked, and what was not:
|
|
|
282
285
|
break CI. `--archived` keeps a session out of OpenCode's lists but does not delete it, so
|
|
283
286
|
sessions accumulate in OpenCode's data directory (each run is a private `--standalone`
|
|
284
287
|
server in a temporary project); prune them yourself.
|
|
285
|
-
* **`cursor:<model>`** runs `cursor-agent -p --mode ask --trust`
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
no
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
*
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
288
|
+
* **`cursor:<model>`** runs `cursor-agent -p --output-format stream-json --mode ask --trust`
|
|
289
|
+
with a `--workspace` that holds only a `.cursor/cli.json` permission file (nothing allowed;
|
|
290
|
+
reads, writes and shell commands denied), and with `CURSOR_CONFIG_DIR` and `CURSOR_DATA_DIR`
|
|
291
|
+
pointed at empty directories in the call's temp root. *Verified live* on 2026-11-13 with
|
|
292
|
+
cursor-agent 2026.11.11:
|
|
293
|
+
* Ask mode has no write or shell tool, but it is **not** tool-free: it has a read tool that
|
|
294
|
+
runs without approval and reads files inside the workspace and outside it by absolute path.
|
|
295
|
+
The permission file made every read fail with "Permission denied", inside and outside, but
|
|
296
|
+
the model still tries, so the stream carries a `tool_call` event; evalproof fails the call
|
|
297
|
+
on any such event (`cursor used a tool (readToolCall); isolation broken`) and discards the
|
|
298
|
+
reply. The write and shell entries in the deny list are a second guard for later versions;
|
|
299
|
+
only reads were tested.
|
|
300
|
+
* Cursor reads instruction files (`AGENTS.md`, `CLAUDE.md`, `.cursor/rules`) from the
|
|
301
|
+
workspace **and from the directories above it**. The workspace is empty and sits in the
|
|
302
|
+
system temp directory, so nothing is found there unless `TMPDIR` points inside a project.
|
|
303
|
+
`~/.claude/CLAUDE.md` was not read.
|
|
304
|
+
* **Your skills list is sent.** Cursor puts the names and descriptions of the skills under
|
|
305
|
+
your home directory (`~/.claude/skills`, `~/.cursor/skills`, `~/.codex/skills`,
|
|
306
|
+
`~/.agents/skills`, plugin skills) and its own subagents in every prompt. The paths are
|
|
307
|
+
fixed and no setting turns them off; moving `HOME` logs the CLI out. They count as input
|
|
308
|
+
tokens on every call, and `doctor` warns about it. Verified with a login; with
|
|
309
|
+
`CURSOR_API_KEY` it was not tested.
|
|
310
|
+
* The redirected config and data directories keep the login (it is in the system keychain),
|
|
311
|
+
keep each call's chat out of `~/.cursor/chats` and `~/.cursor/projects`, and mean your own
|
|
312
|
+
`~/.cursor/cli-config.json` (and its allow list) is not read. A fresh config directory gets
|
|
313
|
+
a default allow list with `Shell(ls)`, so evalproof writes one that allows nothing.
|
|
314
|
+
|
|
315
|
+
`stream-json` rather than `json` because the json result joins the model's preamble text
|
|
316
|
+
("I'll read the file now.") with the answer and carries no tool events. Token counts come from
|
|
317
|
+
the result event (output includes reasoning). The model recorded is the one you asked for;
|
|
318
|
+
the result carries no model id. The prompt is an argument (nothing says the CLI reads stdin),
|
|
319
|
+
so it is visible in `ps` while the call runs and a prompt over 100,000 bytes is refused. The
|
|
320
|
+
key variable is `CURSOR_API_KEY`, passed through the environment, never as `--api-key`.
|
|
321
|
+
`doctor` reads `cursor-agent about` for the login state (`status` hung) and prints no account.
|
|
322
|
+
* **`copilot:<model>`** runs `copilot --no-auto-update --no-ask-user --no-custom-instructions
|
|
323
|
+
--disable-builtin-mcps --available-tools=evalproof-no-tools --output-format json
|
|
324
|
+
--usage-output-file <tmp>`, with the prompt on stdin and `COPILOT_HOME` pointed at an empty
|
|
325
|
+
directory in the call's temp root. *Verified live* on 2026-11-13 with copilot 1.0.110:
|
|
326
|
+
* An empty allow-list (`--available-tools=`) is **ignored**: the model called its `view` tool
|
|
327
|
+
and read a file. An allow-list of one name that is no tool disables every tool
|
|
328
|
+
(`session.info: Disabled tools: bash, create, edit, glob, grep, ...`), and that is what
|
|
329
|
+
evalproof passes. Any `tool.execution*` event or tool request in the output also fails the
|
|
330
|
+
call (`copilot used a tool (view); isolation broken`); with tools off it never fired live,
|
|
331
|
+
so that guard is tested only against canned output. No `--allow-*` flag is ever passed.
|
|
332
|
+
With no tools the model sometimes writes a made-up tool call and its "result" as plain
|
|
333
|
+
text; that is reply text, not a tool run, and is scored as such.
|
|
334
|
+
* `--no-custom-instructions` works: without it a canary `AGENTS.md`, `CLAUDE.md` and
|
|
335
|
+
`.github/copilot-instructions.md` in the working directory were all read; with it none was,
|
|
336
|
+
nor an `AGENTS.md` in the directory above. No skills or MCP servers were offered.
|
|
337
|
+
* Copilot keeps its own coding-agent system prompt and adds the date and a note that it is
|
|
338
|
+
not sandboxed; your system text is an instruction in the user turn.
|
|
339
|
+
* `COPILOT_HOME` in the temp root keeps the login (it is in the system keychain) and keeps
|
|
340
|
+
each call's session, logs and config out of `~/.copilot` (verified: no new file there after
|
|
341
|
+
a call). Your own Copilot config, plugins and MCP servers live in that directory, so they
|
|
342
|
+
are not used either; that follows from the redirect and was not tested separately.
|
|
343
|
+
* Models and cost: on the test account named models (`gpt-6-luna`, `claude-haiku-5.5`) were
|
|
344
|
+
rejected with "is not available", and only `copilot:auto` worked; which models you can name
|
|
345
|
+
depends on your plan and your organisation's policy. `auto` routes each call to a model of
|
|
346
|
+
Copilot's choosing (`gpt-6-luna` on one call, `mai-code-1.1-flash` on another), so the run
|
|
347
|
+
artifact records every model that answered, not `auto`, and a suite run on `auto` can mix
|
|
348
|
+
models. `auto` used one premium request per call there; the rate depends on the model and
|
|
349
|
+
the plan.
|
|
350
|
+
* **`copilot:auto` makes a poor judge.** The judge's resolved model is part of what
|
|
351
|
+
[`compare` and the gate check](#use-your-claude-code-or-codex-login): two runs on `auto` often saw
|
|
352
|
+
different models, so they are refused as a judge change (exit 4) unless you allow it.
|
|
353
|
+
`doctor` warns about it. Name a model for the judge if your plan allows one.
|
|
354
|
+
|
|
355
|
+
Token counts are read from the usage file (input already includes cache reads and writes,
|
|
356
|
+
output includes reasoning); a missing file means 0, never a failure. Only
|
|
304
357
|
`COPILOT_GITHUB_TOKEN` is removed; `GH_TOKEN` and `GITHUB_TOKEN` are left because `gh` and CI
|
|
305
358
|
set them for other reasons. `doctor` prints `auth: not checked`.
|
|
306
359
|
|
|
@@ -469,7 +522,7 @@ The composite action runs `compare --gate`, appends the report to the job summar
|
|
|
469
522
|
fails the job on exit codes 1, 3 or 4:
|
|
470
523
|
|
|
471
524
|
```yaml
|
|
472
|
-
- uses: technobasant/evalproof@v0.4.
|
|
525
|
+
- uses: technobasant/evalproof@v0.4.1
|
|
473
526
|
with:
|
|
474
527
|
baseline: eval/baseline.json # produced on main, e.g. as a build artifact
|
|
475
528
|
candidate: eval/candidate.json # produced on this branch
|
|
@@ -48,6 +48,26 @@ def suite_targets(path: str | None) -> list[tuple[TargetConfig, list[str]]]:
|
|
|
48
48
|
return list(found.values())
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def experimental_warnings(kind: str, model: str, roles: list[str]) -> list[str]:
|
|
52
|
+
"""Known limits of an experimental CLI that a user should hear about before a run."""
|
|
53
|
+
warnings: list[str] = []
|
|
54
|
+
if kind == "cursor":
|
|
55
|
+
warnings.append(
|
|
56
|
+
"cursor-agent puts the names and descriptions of your skills (~/.claude/skills, "
|
|
57
|
+
"~/.cursor/skills, ~/.codex/skills, ~/.agents/skills, plugin skills) in every "
|
|
58
|
+
"prompt; no option turns this off, so they are sent to Cursor and count as input "
|
|
59
|
+
"tokens on each call"
|
|
60
|
+
)
|
|
61
|
+
if kind == "copilot" and model == "auto" and "judge" in roles:
|
|
62
|
+
warnings.append(
|
|
63
|
+
"copilot:auto as the judge: Copilot picks the model per call and the run records "
|
|
64
|
+
"every model it saw, so two runs often differ and compare and gate refuse them as "
|
|
65
|
+
"a judge change (exit 4) unless the gate sets allow_judge_change; name a model if "
|
|
66
|
+
"your plan allows one"
|
|
67
|
+
)
|
|
68
|
+
return warnings
|
|
69
|
+
|
|
70
|
+
|
|
51
71
|
def check_target(cfg: TargetConfig, roles: list[str], out: TextIO) -> bool:
|
|
52
72
|
"""Print what is known about one target; True when it is usable."""
|
|
53
73
|
kind, model = parse_target(cfg.target)
|
|
@@ -88,6 +108,8 @@ def check_target(cfg: TargetConfig, roles: list[str], out: TextIO) -> bool:
|
|
|
88
108
|
out.write(" note: experimental provider; its isolation is less proven (see the README)\n")
|
|
89
109
|
if provider.extra_env:
|
|
90
110
|
out.write(f" env: {' '.join(f'{k}={v}' for k, v in provider.extra_env.items())}\n")
|
|
111
|
+
for line in experimental_warnings(kind, model, roles):
|
|
112
|
+
out.write(f" warning: {line}\n")
|
|
91
113
|
if cfg.api_key_env:
|
|
92
114
|
out.write(f" key: ${cfg.api_key_env} is passed to the CLI as {provider.key_var}\n")
|
|
93
115
|
elif provider.auth_mode == "api-key":
|