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.
Files changed (84) hide show
  1. {evalproof-0.4.0 → evalproof-0.4.1}/CHANGELOG.md +45 -1
  2. {evalproof-0.4.0 → evalproof-0.4.1}/PKG-INFO +83 -30
  3. {evalproof-0.4.0 → evalproof-0.4.1}/README.md +82 -29
  4. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/_version.py +1 -1
  5. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/doctor.py +22 -0
  6. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/agent_cli_experimental.py +228 -63
  7. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/scaffold.py +1 -1
  8. {evalproof-0.4.0 → evalproof-0.4.1}/tests/fakecli_experimental.py +136 -7
  9. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_agent_cli_experimental.py +354 -48
  10. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_init.py +2 -2
  11. {evalproof-0.4.0 → evalproof-0.4.1}/.gitignore +0 -0
  12. {evalproof-0.4.0 → evalproof-0.4.1}/CITATION.cff +0 -0
  13. {evalproof-0.4.0 → evalproof-0.4.1}/LICENSE +0 -0
  14. {evalproof-0.4.0 → evalproof-0.4.1}/NOTICE +0 -0
  15. {evalproof-0.4.0 → evalproof-0.4.1}/pyproject.toml +0 -0
  16. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/__init__.py +0 -0
  17. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/cache.py +0 -0
  18. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/calibration.py +0 -0
  19. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/cli.py +0 -0
  20. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/compare.py +0 -0
  21. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/config.py +0 -0
  22. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/dataset.py +0 -0
  23. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/gate.py +0 -0
  24. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/importers/__init__.py +0 -0
  25. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/importers/_common.py +0 -0
  26. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/importers/deepeval.py +0 -0
  27. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/importers/inspect_ai.py +0 -0
  28. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/importers/lm_eval.py +0 -0
  29. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/importers/promptfoo.py +0 -0
  30. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/importers/table.py +0 -0
  31. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/models.py +0 -0
  32. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/__init__.py +0 -0
  33. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/agent_cli.py +0 -0
  34. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/anthropic.py +0 -0
  35. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/base.py +0 -0
  36. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/ollama.py +0 -0
  37. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/openai.py +0 -0
  38. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/providers/profiles.py +0 -0
  39. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/py.typed +0 -0
  40. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/report.py +0 -0
  41. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/runner.py +0 -0
  42. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/scorers/__init__.py +0 -0
  43. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/scorers/deterministic.py +0 -0
  44. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/scorers/judge.py +0 -0
  45. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/scorers/retrieval.py +0 -0
  46. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/stats.py +0 -0
  47. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/systems.py +0 -0
  48. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/templates/SKILL.md +0 -0
  49. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/templates/cases.jsonl +0 -0
  50. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/templates/evals-readme.md +0 -0
  51. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/templates/gitignore +0 -0
  52. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/templates/suite.yaml +0 -0
  53. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/templates/workflow-api-key.yml +0 -0
  54. {evalproof-0.4.0 → evalproof-0.4.1}/src/evalproof/templates/workflow-keyless.yml +0 -0
  55. {evalproof-0.4.0 → evalproof-0.4.1}/tests/__init__.py +0 -0
  56. {evalproof-0.4.0 → evalproof-0.4.1}/tests/conftest.py +0 -0
  57. {evalproof-0.4.0 → evalproof-0.4.1}/tests/fakecli.py +0 -0
  58. {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/README.md +0 -0
  59. {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/deepeval/test_run.json +0 -0
  60. {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/inspect/log_formats.json +0 -0
  61. {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/lm-eval/gpt2/samples_hellaswag_2024-05-01T10-20-30.123456.jsonl +0 -0
  62. {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/lm-eval/llama3/samples_gsm8k_2024-05-01T10-20-30.123456.jsonl +0 -0
  63. {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/promptfoo/sample-export.json +0 -0
  64. {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/table/long.csv +0 -0
  65. {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/table/long.jsonl +0 -0
  66. {evalproof-0.4.0 → evalproof-0.4.1}/tests/fixtures/table/wide.csv +0 -0
  67. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_action.py +0 -0
  68. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_agent_cli.py +0 -0
  69. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_cache_systems.py +0 -0
  70. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_calibration.py +0 -0
  71. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_compare_gate_report.py +0 -0
  72. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_config_dataset.py +0 -0
  73. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_doctor.py +0 -0
  74. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_holm_gate.py +0 -0
  75. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_importers.py +0 -0
  76. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_judge_fingerprint.py +0 -0
  77. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_live_agent_cli.py +0 -0
  78. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_providers.py +0 -0
  79. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_public_api.py +0 -0
  80. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_release_fixes.py +0 -0
  81. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_runner_cli.py +0 -0
  82. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_scorers.py +0 -0
  83. {evalproof-0.4.0 → evalproof-0.4.1}/tests/test_security.py +0 -0
  84. {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.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
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.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
@@ -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:gpt-5` | none: your GitHub Copilot login | local CLI, experimental |
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 empty directory, its API-key variable removed from the
297
- child's environment unless `api_key_env` names one, a bounded runtime, and the CLI, its version
298
- and an isolation tag (`opencode/0`, `cursor/0`, `copilot/0`) in the run and in the cache key.
299
- They are experimental because their isolation is less proven than Claude Code's and Codex's.
300
- Each prints `evalproof: <name> provider is experimental` on stderr the first time it is used in
301
- a process. Like the other two they ignore `temperature`, `seed` and `max_tokens`, and
302
- `evalproof doctor` without a suite lists each of them whose binary is on `PATH`. What was
303
- checked, and what was not:
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` with an empty `--workspace`.
322
- *Not verified:* Cursor was not logged in on the test machine, so no call succeeded. The flags
323
- come from the CLI's help and documentation, and the parser from its source; it is lenient (a
324
- JSON result object, an array of events, terminal noise around it) and an unexpected shape
325
- fails with a message that says what was expected. The empty workspace is the only isolation:
326
- no flag ignores rules files, user-level rules were not checked, and whether ask mode really
327
- has no tools is untested. There is no system-prompt option, so your system text goes in front
328
- of the prompt under a heading, and the prompt is an argument (nothing says the CLI reads
329
- stdin), so it is visible in `ps` while the call runs and a prompt over 100,000 bytes is
330
- refused. The key variable is `CURSOR_API_KEY`, passed through the environment, never as
331
- `--api-key`. `doctor` reads `cursor-agent about` for the login state (`status` hung) and
332
- prints no account.
333
- * **`copilot:<model>`** runs `copilot -s --no-auto-update --no-ask-user --available-tools= -p`.
334
- *Not verified:* Copilot CLI was not installed on the test machine; every flag comes from
335
- GitHub's documentation. `--available-tools=` is an allow-list with nothing in it, the most
336
- restrictive documented tool option; whether the CLI accepts an empty list was not tried. No
337
- `--allow-*` flag is ever passed, so a tool that needs approval cannot get it. The reply is
338
- plain text, so token counts are 0. `COPILOT_HOME` is not redirected, so user-level Copilot
339
- instructions may still be read. System text and prompt travel as for Cursor. Only
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.0
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:gpt-5` | none: your GitHub Copilot login | local CLI, experimental |
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 empty directory, its API-key variable removed from the
261
- child's environment unless `api_key_env` names one, a bounded runtime, and the CLI, its version
262
- and an isolation tag (`opencode/0`, `cursor/0`, `copilot/0`) in the run and in the cache key.
263
- They are experimental because their isolation is less proven than Claude Code's and Codex's.
264
- Each prints `evalproof: <name> provider is experimental` on stderr the first time it is used in
265
- a process. Like the other two they ignore `temperature`, `seed` and `max_tokens`, and
266
- `evalproof doctor` without a suite lists each of them whose binary is on `PATH`. What was
267
- checked, and what was not:
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` with an empty `--workspace`.
286
- *Not verified:* Cursor was not logged in on the test machine, so no call succeeded. The flags
287
- come from the CLI's help and documentation, and the parser from its source; it is lenient (a
288
- JSON result object, an array of events, terminal noise around it) and an unexpected shape
289
- fails with a message that says what was expected. The empty workspace is the only isolation:
290
- no flag ignores rules files, user-level rules were not checked, and whether ask mode really
291
- has no tools is untested. There is no system-prompt option, so your system text goes in front
292
- of the prompt under a heading, and the prompt is an argument (nothing says the CLI reads
293
- stdin), so it is visible in `ps` while the call runs and a prompt over 100,000 bytes is
294
- refused. The key variable is `CURSOR_API_KEY`, passed through the environment, never as
295
- `--api-key`. `doctor` reads `cursor-agent about` for the login state (`status` hung) and
296
- prints no account.
297
- * **`copilot:<model>`** runs `copilot -s --no-auto-update --no-ask-user --available-tools= -p`.
298
- *Not verified:* Copilot CLI was not installed on the test machine; every flag comes from
299
- GitHub's documentation. `--available-tools=` is an allow-list with nothing in it, the most
300
- restrictive documented tool option; whether the CLI accepts an empty list was not tried. No
301
- `--allow-*` flag is ever passed, so a tool that needs approval cannot get it. The reply is
302
- plain text, so token counts are 0. `COPILOT_HOME` is not redirected, so user-level Copilot
303
- instructions may still be read. System text and prompt travel as for Cursor. Only
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.0
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
@@ -1,3 +1,3 @@
1
1
  """The package version: the single place it is written. pyproject.toml reads it at build time."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.4.1"
@@ -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":