pluginbench 0.5.0__tar.gz → 0.6.0__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 (57) hide show
  1. {pluginbench-0.5.0 → pluginbench-0.6.0}/PKG-INFO +16 -6
  2. {pluginbench-0.5.0 → pluginbench-0.6.0}/README.md +15 -5
  3. {pluginbench-0.5.0 → pluginbench-0.6.0}/pyproject.toml +1 -1
  4. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/__init__.py +1 -1
  5. pluginbench-0.6.0/src/pluginbench/arms.py +6 -0
  6. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/assessment-charts.html +11 -10
  7. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/assessment_charts.py +8 -1
  8. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/cli.py +17 -3
  9. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/codex.py +2 -1
  10. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/codex_events.py +181 -58
  11. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/compare.py +35 -6
  12. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/components.py +2 -2
  13. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/evaluation.py +37 -24
  14. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/evaluation_report.py +30 -5
  15. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/harbor_backend.py +34 -1
  16. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/isolation.py +20 -10
  17. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/judge.py +298 -33
  18. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/parallel.py +20 -10
  19. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/run_status.py +45 -15
  20. pluginbench-0.6.0/src/pluginbench/saved_usage.py +68 -0
  21. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/study.py +19 -2
  22. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/study_review.py +5 -0
  23. {pluginbench-0.5.0 → pluginbench-0.6.0}/.gitignore +0 -0
  24. {pluginbench-0.5.0 → pluginbench-0.6.0}/LICENSE +0 -0
  25. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/__main__.py +0 -0
  26. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/assessment.py +0 -0
  27. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/assessment_metrics.py +0 -0
  28. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/assessment_models.py +0 -0
  29. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/baselines.py +0 -0
  30. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/claude.py +0 -0
  31. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/code_diff.py +0 -0
  32. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/collectors.py +0 -0
  33. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/coverage_report.py +0 -0
  34. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/datasets.py +0 -0
  35. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/evaluation.example.yaml +0 -0
  36. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/fallow_parse.mjs +0 -0
  37. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/language_analysis.py +0 -0
  38. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/ledger.py +0 -0
  39. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/measurement_profiles.py +0 -0
  40. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/native_treatment_runtime.js +0 -0
  41. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/native_treatments.py +0 -0
  42. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/observer_data/go-analysis-v1/go.mod +0 -0
  43. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/observer_data/go-analysis-v1/go.sum +0 -0
  44. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/observer_data/go-analysis-v1/main.go +0 -0
  45. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/observer_data/oxc-parser-0.126.0/package-lock.json +0 -0
  46. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/observer_data/oxc-parser-0.126.0/package.json +0 -0
  47. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/processes.py +0 -0
  48. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/remeasure.py +0 -0
  49. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/run_lock.py +0 -0
  50. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/submissions.py +0 -0
  51. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/task_sources.py +0 -0
  52. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/treatments.py +0 -0
  53. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/trial_containers.py +0 -0
  54. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/vendor/ECHARTS-LICENSE +0 -0
  55. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/vendor/ECHARTS-NOTICE +0 -0
  56. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/vendor/ZRENDER-LICENSE +0 -0
  57. {pluginbench-0.5.0 → pluginbench-0.6.0}/src/pluginbench/vendor/echarts.min.js +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pluginbench
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Test whether skills help coding agents make fewer mistakes.
5
5
  Project-URL: Homepage, https://github.com/filipgutica/pluginbench
6
6
  Project-URL: Documentation, https://github.com/filipgutica/pluginbench/blob/main/docs/running-evaluations.md
@@ -70,7 +70,11 @@ PluginBench runs on Linux or macOS. Every run needs:
70
70
  | Python | 3.12 or later | Runs PluginBench |
71
71
  | Git | Any recent | Task sources and patches |
72
72
  | Docker | Linux containers on a local Unix socket, with Compose and Buildx | Each attempt and its verifier run in containers |
73
- | Codex CLI | Exactly 0.162.1 or 0.167.0, logged in | Coding agent. PluginBench checks the `codex` on your `PATH` and copies its login from `~/.codex/auth.json`. Other versions fail `check`. |
73
+ | Codex CLI | Logged in; verified trace versions: 0.162.1, 0.167.0, 0.167.1 | Coding agent. PluginBench copies its login from `~/.codex/auth.json` to the pinned container CLI. Unverified versions warn and continue; incompatible traces or unverified inherited history leave tokens unavailable. |
74
+
75
+ Set `runner.cli_version: "0.167.0"` to select the exact coding CLI version installed in task containers.
76
+ When omitted, preparation pins the host CLI's version. Resume keeps that pin across host CLI updates.
77
+ Independent reviews use the host CLI. See [verified coding CLIs](docs/running-evaluations.md#verified-coding-clis) for credential compatibility.
74
78
 
75
79
  Some measures and commands need more:
76
80
 
@@ -84,7 +88,14 @@ Some measures and commands need more:
84
88
 
85
89
  ## Install
86
90
 
87
- There is no registry release yet. Install from a clone:
91
+ ```sh
92
+ pipx install pluginbench
93
+ pluginbench --version
94
+ ```
95
+
96
+ `uv tool install pluginbench` works too. Either one also installs Harbor 0.24.0. If your default Python is older than 3.12, add `--python python3.12`.
97
+
98
+ To change PluginBench itself, install from a clone instead:
88
99
 
89
100
  ```sh
90
101
  git clone https://github.com/filipgutica/pluginbench.git
@@ -92,7 +103,6 @@ cd pluginbench
92
103
  python3 -m venv .venv
93
104
  source .venv/bin/activate
94
105
  pip install -e .
95
- pluginbench --version
96
106
  ```
97
107
 
98
108
  ## Quickstart
@@ -139,7 +149,7 @@ This run uses one public DeepSWE task, vanilla only, and one repeat. It needs no
139
149
  pluginbench run evaluation.yaml
140
150
  ```
141
151
 
142
- `run` prints the plan and asks `Start evaluation?` (`--yes` skips the question).
152
+ `run` prints the plan and asks `Start evaluation?` (`--yes` skips the question; in a script with no answer to give, pass `--yes`).
143
153
  It then runs the same checks as `check`, starts the run in the background, and prints the run directory, for example `runs/20261110T101500123456Z`.
144
154
 
145
155
  5. Follow it:
@@ -263,7 +273,7 @@ Every setting lives in the config. Flags only control how a command runs.
263
273
 
264
274
  ## Stop, resume, and reuse
265
275
 
266
- - `pluginbench stop runs/<run>` stops new attempts. With `jobs` above 1, attempts already coding finish and are kept. With `jobs: 1`, the attempt in progress is interrupted, and a resume codes its slot again with a new attempt.
276
+ - `pluginbench stop runs/<run>` stops new attempts. Attempts already coding finish and are kept, including with `jobs: 1`. Resume never codes complete attempts again.
267
277
  - `pluginbench run runs/<run>` resumes. A run that was killed, for example by a host shutdown, shows `interrupted`, and `status` prints this command. Complete attempts are kept and never coded again. An attempt cut off mid-call is recorded as `discarded-interrupted` and a new attempt takes its slot. Containers the killed attempt left running are stopped before coding resumes, and `status` names them until then. Resume cannot repair a failing solution.
268
278
  - If Codex fails before its first model turn, for example during a network outage, the run stops instead of scoring the attempt. Resume when Codex is reachable.
269
279
  - Vanilla attempts are cached in `$XDG_CACHE_HOME/pluginbench` (default `~/.cache/pluginbench`) and reused by any later run with the same task, model, and settings. Grading and measures run again. Reports mark reused attempts. [Baselines and reports](docs/running-evaluations.md#baselines-and-reports).
@@ -34,7 +34,11 @@ PluginBench runs on Linux or macOS. Every run needs:
34
34
  | Python | 3.12 or later | Runs PluginBench |
35
35
  | Git | Any recent | Task sources and patches |
36
36
  | Docker | Linux containers on a local Unix socket, with Compose and Buildx | Each attempt and its verifier run in containers |
37
- | Codex CLI | Exactly 0.162.1 or 0.167.0, logged in | Coding agent. PluginBench checks the `codex` on your `PATH` and copies its login from `~/.codex/auth.json`. Other versions fail `check`. |
37
+ | Codex CLI | Logged in; verified trace versions: 0.162.1, 0.167.0, 0.167.1 | Coding agent. PluginBench copies its login from `~/.codex/auth.json` to the pinned container CLI. Unverified versions warn and continue; incompatible traces or unverified inherited history leave tokens unavailable. |
38
+
39
+ Set `runner.cli_version: "0.167.0"` to select the exact coding CLI version installed in task containers.
40
+ When omitted, preparation pins the host CLI's version. Resume keeps that pin across host CLI updates.
41
+ Independent reviews use the host CLI. See [verified coding CLIs](docs/running-evaluations.md#verified-coding-clis) for credential compatibility.
38
42
 
39
43
  Some measures and commands need more:
40
44
 
@@ -48,7 +52,14 @@ Some measures and commands need more:
48
52
 
49
53
  ## Install
50
54
 
51
- There is no registry release yet. Install from a clone:
55
+ ```sh
56
+ pipx install pluginbench
57
+ pluginbench --version
58
+ ```
59
+
60
+ `uv tool install pluginbench` works too. Either one also installs Harbor 0.24.0. If your default Python is older than 3.12, add `--python python3.12`.
61
+
62
+ To change PluginBench itself, install from a clone instead:
52
63
 
53
64
  ```sh
54
65
  git clone https://github.com/filipgutica/pluginbench.git
@@ -56,7 +67,6 @@ cd pluginbench
56
67
  python3 -m venv .venv
57
68
  source .venv/bin/activate
58
69
  pip install -e .
59
- pluginbench --version
60
70
  ```
61
71
 
62
72
  ## Quickstart
@@ -103,7 +113,7 @@ This run uses one public DeepSWE task, vanilla only, and one repeat. It needs no
103
113
  pluginbench run evaluation.yaml
104
114
  ```
105
115
 
106
- `run` prints the plan and asks `Start evaluation?` (`--yes` skips the question).
116
+ `run` prints the plan and asks `Start evaluation?` (`--yes` skips the question; in a script with no answer to give, pass `--yes`).
107
117
  It then runs the same checks as `check`, starts the run in the background, and prints the run directory, for example `runs/20261110T101500123456Z`.
108
118
 
109
119
  5. Follow it:
@@ -227,7 +237,7 @@ Every setting lives in the config. Flags only control how a command runs.
227
237
 
228
238
  ## Stop, resume, and reuse
229
239
 
230
- - `pluginbench stop runs/<run>` stops new attempts. With `jobs` above 1, attempts already coding finish and are kept. With `jobs: 1`, the attempt in progress is interrupted, and a resume codes its slot again with a new attempt.
240
+ - `pluginbench stop runs/<run>` stops new attempts. Attempts already coding finish and are kept, including with `jobs: 1`. Resume never codes complete attempts again.
231
241
  - `pluginbench run runs/<run>` resumes. A run that was killed, for example by a host shutdown, shows `interrupted`, and `status` prints this command. Complete attempts are kept and never coded again. An attempt cut off mid-call is recorded as `discarded-interrupted` and a new attempt takes its slot. Containers the killed attempt left running are stopped before coding resumes, and `status` names them until then. Resume cannot repair a failing solution.
232
242
  - If Codex fails before its first model turn, for example during a network outage, the run stops instead of scoring the attempt. Resume when Codex is reachable.
233
243
  - Vanilla attempts are cached in `$XDG_CACHE_HOME/pluginbench` (default `~/.cache/pluginbench`) and reused by any later run with the same task, model, and settings. Grading and measures run again. Reports mark reused attempts. [Baselines and reports](docs/running-evaluations.md#baselines-and-reports).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pluginbench"
7
- version = "0.5.0"
7
+ version = "0.6.0"
8
8
  description = "Test whether skills help coding agents make fewer mistakes."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -1,3 +1,3 @@
1
1
  """Compare native instructions, skills, and plugins with vanilla coding agents."""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.6.0"
@@ -0,0 +1,6 @@
1
+ """Public arm names, independent of frozen run and cache identities."""
2
+
3
+
4
+ def normalize_arm_name(name: str) -> str:
5
+ """Accept the stored/legacy name and the public name for the no-guidance arm."""
6
+ return "vanilla" if name == "baseline" else name
@@ -273,7 +273,8 @@ const formatTick = value => new Intl.NumberFormat(undefined, { notation: Math.ab
273
273
  // Values below one keep two significant digits so a small real difference never reads as zero.
274
274
  const formatCompact = value => value === null || value === undefined ? "Unknown" : new Intl.NumberFormat(undefined, Math.abs(value) >= 10000 ? { notation: "compact", maximumFractionDigits: 2 } : value !== 0 && Math.abs(value) < 1 ? { maximumSignificantDigits: 2 } : { maximumFractionDigits: 2 }).format(value);
275
275
  const formatSigned = value => (value > 0 ? "+" : "") + formatCompact(value);
276
- const armLabel = (arm) => arm === "baseline" ? "Vanilla" : arm === "unknown" ? "Unlabelled skill set" : arm;
276
+ const vanillaArm = data.kind === "evaluation" ? "vanilla" : "baseline";
277
+ const armLabel = (arm) => arm === vanillaArm ? "Vanilla" : arm === "unknown" ? "Unlabelled skill set" : arm;
277
278
  const choices = (select, values) => {
278
279
  for (const value of values) create("option", value, select).value = value;
279
280
  };
@@ -283,10 +284,10 @@ const knownTasks = unique(observations.map(row => row.task).filter(task => task
283
284
  choices(taskSelect, knownTasks);
284
285
  taskSelect.value = "";
285
286
  // One package order and color per package everywhere: Vanilla first, then packages by name.
286
- const packageNames = unique([...observations.map(row => row.arm), ...data.attempts.map(row => row.guidance)]).filter(arm => arm !== "baseline").sort();
287
- const armRank = arm => arm === "baseline" ? -1 : packageNames.indexOf(arm);
287
+ const packageNames = unique([...observations.map(row => row.arm), ...data.attempts.map(row => row.guidance)]).filter(arm => arm !== vanillaArm).sort();
288
+ const armRank = arm => arm === vanillaArm ? -1 : packageNames.indexOf(arm);
288
289
  const byArm = (a, b) => armRank(a) - armRank(b);
289
- const seriesVariable = arm => arm !== "baseline" && armRank(arm) >= 0 ? "--series-" + (armRank(arm) % 8 + 1) : "--pb-muted";
290
+ const seriesVariable = arm => arm !== vanillaArm && armRank(arm) >= 0 ? "--series-" + (armRank(arm) % 8 + 1) : "--pb-muted";
290
291
  const viewLabel = create("label", "Chart values", document.querySelector(".controls"));
291
292
  const comparisonView = create("select", undefined, viewLabel);
292
293
  for (const [value, label] of [["delta", "Delta vs vanilla"], ["raw", "Raw values"]]) create("option", label, comparisonView).value = value;
@@ -414,7 +415,7 @@ function showPackageSummary() {
414
415
  const text = result?.value !== undefined && result.value !== null ? formatCompact(result.value) : "Unknown";
415
416
  create("div", text + (groups.length > 1 && result?.analysis_group ? " · " + result.analysis_group : ""), cell);
416
417
  if (!taskSelect.value && result) create("small", result.measured_tasks + "/" + result.assigned_tasks + " tasks", cell);
417
- if (arm !== "baseline") {
418
+ if (arm !== vanillaArm) {
418
419
  const deltaRows = taskSelect.value ? (data.comparisons || []).filter(row => row.task === taskSelect.value && row.metric === metric && row.guidance === arm) : [result];
419
420
  const delta = deltaRows.length === 1 ? deltaRows[0] : null;
420
421
  const value = delta?.absolute_delta_vs_vanilla ?? null;
@@ -496,7 +497,7 @@ function showCompleteness() {
496
497
  // Task × measure × package: measured, unknown with its reason, or not configured for the task.
497
498
  const matrix = (data.completeness_matrix || []).filter(row => !taskSelect.value || row.task === taskSelect.value);
498
499
  if (!matrix.length) return;
499
- const arms = unique(matrix.flatMap(row => Object.keys(row.cells)));
500
+ const arms = unique(matrix.flatMap(row => Object.keys(row.cells))).sort(byArm);
500
501
  const grid = create("table", undefined, body);
501
502
  const head = create("tr", undefined, grid);
502
503
  for (const label of ["Task", "Measure", ...arms.map(armLabel)]) create("th", label, head);
@@ -626,10 +627,10 @@ function evaluationPlotRows({ rows, metric }) {
626
627
  const raw = comparisonView.value === "raw", overview = !taskSelect.value;
627
628
  if (overview && chartBreakdown.value === "tasks") {
628
629
  const medians = raw ? (data.raw_medians || []) : (data.comparisons || []);
629
- return medians.filter(row => row.metric === metric).map(row => ({ ...row, guidance: row.guidance || row.arm, label: row.task.replaceAll("-", " "), plotted: raw ? row.value : row.absolute_delta_vs_vanilla, status: raw ? "measured" : row.status, reason: raw ? "Raw task median for one measurement profile. Comparison eligibility is separate." : row.reason, population: raw ? row.measured + " runs" : "Vanilla " + row.baseline_measured + "/" + row.baseline_assigned + " · guidance " + row.guidance_measured + "/" + row.guidance_assigned + " runs", rawRuns: [] })).sort((a, b) => a.task.localeCompare(b.task) || Number(b.guidance === "baseline") - Number(a.guidance === "baseline") || a.guidance.localeCompare(b.guidance));
630
+ return medians.filter(row => row.metric === metric).map(row => ({ ...row, guidance: row.guidance || row.arm, label: row.task.replaceAll("-", " "), plotted: raw ? row.value : row.absolute_delta_vs_vanilla, status: raw ? "measured" : row.status, reason: raw ? "Raw task median for one measurement profile. Comparison eligibility is separate." : row.reason, population: raw ? row.measured + " runs" : "Vanilla " + row.vanilla_measured + "/" + row.vanilla_assigned + " · guidance " + row.guidance_measured + "/" + row.guidance_assigned + " runs", rawRuns: [] })).sort((a, b) => a.task.localeCompare(b.task) || Number(b.guidance === "vanilla") - Number(a.guidance === "vanilla") || a.guidance.localeCompare(b.guidance));
630
631
  }
631
632
  if (overview) return (data.aggregates || []).filter(row => row.metric === metric).map(row => ({ ...row, status: raw ? row.status : row.comparison_status, label: armLabel(row.guidance) + (row.analysis_group ? " · " + row.analysis_group : ""), plotted: raw ? row.value : row.absolute_delta_vs_vanilla, population: row.measured_tasks + "/" + row.assigned_tasks + " tasks", rawRuns: [] }));
632
- if (!raw) return (data.comparisons || []).filter(row => row.metric === metric && row.task === taskSelect.value).map(row => ({ ...row, label: row.guidance, plotted: row.absolute_delta_vs_vanilla, population: "Vanilla " + row.baseline_measured + "/" + row.baseline_assigned + " · guidance " + row.guidance_measured + "/" + row.guidance_assigned + " runs", rawRuns: [] }));
633
+ if (!raw) return (data.comparisons || []).filter(row => row.metric === metric && row.task === taskSelect.value).map(row => ({ ...row, label: row.guidance, plotted: row.absolute_delta_vs_vanilla, population: "Vanilla " + row.vanilla_measured + "/" + row.vanilla_assigned + " · guidance " + row.guidance_measured + "/" + row.guidance_assigned + " runs", rawRuns: [] }));
633
634
  const medians = (data.raw_medians || []).filter(row => row.role === "handoff" && row.metric === metric && row.task === taskSelect.value);
634
635
  const arms = unique(rows.map(row => row.arm));
635
636
  return arms.flatMap(arm => {
@@ -652,9 +653,9 @@ function drawEvaluationPlot({ parent, rows, metric }) {
652
653
  for (const label of ["Set", raw ? "Value" : "Delta vs vanilla", ...(!raw ? ["Vanilla reference", "% vs vanilla"] : []), "Status", "Measured / assigned"]) create("th", label, head);
653
654
  for (const row of plotRows) {
654
655
  const tr = create("tr", undefined, table);
655
- for (const value of [byTask ? row.label + " · " + armLabel(row.guidance) : row.label, format(row.plotted), ...(!raw ? [format(row.baseline_value ?? row.baseline_median ?? null), format(row.percentage_delta_vs_vanilla)] : []), row.status, row.population]) create("td", value, tr);
656
+ for (const value of [byTask ? row.label + " · " + armLabel(row.guidance) : row.label, format(row.plotted), ...(!raw ? [format(row.vanilla_value ?? row.vanilla_median ?? null), format(row.percentage_delta_vs_vanilla)] : []), row.status, row.population]) create("td", value, tr);
656
657
  }
657
- if (!plotRows.some(row => row.plotted !== null || row.rawRuns.length)) { create("p", raw ? "No defined values to plot. Complete empty scans and missing measurements are listed separately." : "No matching baseline values for a delta. Switch to Raw values to see the available measurements.", parent); return 0; }
658
+ if (!plotRows.some(row => row.plotted !== null || row.rawRuns.length)) { create("p", raw ? "No defined values to plot. Complete empty scans and missing measurements are listed separately." : "No matching vanilla values for a delta. Switch to Raw values to see the available measurements.", parent); return 0; }
658
659
  const container = create("div", undefined, parent); container.className = "chart";
659
660
  const arms = unique(plotRows.map(row => row.guidance));
660
661
  const categories = grouped ? unique(plotRows.map(row => row.group)) : plotRows.map(row => byTask ? row.label + " · " + armLabel(row.guidance) : row.label);
@@ -623,6 +623,9 @@ def _observation(
623
623
  eligible = isinstance(work.get("token_format"), str) and bool(work["token_format"])
624
624
  identity.update(provenance)
625
625
  reason = "native tokens at handoff; cached input and reasoning output subsets are not added"
626
+ validation = _map(work.get("usage_validation"))
627
+ if validation:
628
+ provenance["usage_validation"] = validation
626
629
  elif kind == "loc_change":
627
630
  classification = "source" if key.startswith("source_") else "test"
628
631
  code_size = _map(stage.get("code_size" if classification == "source" else "test_code_size"))
@@ -897,6 +900,10 @@ def _observation(
897
900
  if evidence.get("reason")
898
901
  else "measurement unavailable; unknown is not zero",
899
902
  )
903
+ if kind == "work":
904
+ validation_reasons = _map(work.get("usage_validation")).get("reasons")
905
+ if isinstance(validation_reasons, list) and validation_reasons:
906
+ reason += "; " + "; ".join(str(item) for item in validation_reasons)
900
907
  else:
901
908
  status = "measured" if compatibility else "not comparable"
902
909
  change = _map(stage.get("change"))
@@ -1024,7 +1031,7 @@ def chart_data(report: dict[str, object]) -> dict[str, object]:
1024
1031
  for row in observations:
1025
1032
  if row.compatibility and not groups.get((row.role, row.compatibility, "baseline")):
1026
1033
  row.status = "not comparable"
1027
- row.reason += "; no matching explicit baseline observation"
1034
+ row.reason += "; no matching explicit vanilla observation"
1028
1035
  medians = []
1029
1036
  for (role, identity, arm), rows in groups.items():
1030
1037
  baseline = groups.get((role, identity, "baseline"), [])
@@ -71,6 +71,17 @@ def pick(
71
71
  _error(exc)
72
72
 
73
73
 
74
+ def _confirm(question: str) -> bool:
75
+ """Ask before paid work; when no answer can be read, explain how to start instead."""
76
+ try:
77
+ return typer.confirm(question)
78
+ except typer.Abort:
79
+ if sys.stdin.isatty():
80
+ raise # Ctrl-C at the prompt.
81
+ typer.echo("No answer to confirm with. Pass --yes to start without confirmation.", err=True)
82
+ raise typer.Exit(1) from None
83
+
84
+
74
85
  def _error(exc: Exception) -> None:
75
86
  if isinstance(exc, CheckFailed):
76
87
  typer.echo(_readiness_table(exc.report), err=True)
@@ -392,7 +403,7 @@ def run(
392
403
  "no notification will be sent.",
393
404
  err=True,
394
405
  )
395
- if not yes and not typer.confirm("Start evaluation?"):
406
+ if not yes and not _confirm("Start evaluation?"):
396
407
  raise typer.Exit()
397
408
  progress = _notifying_progress(plan["coding_calls"]) if notify else typer.echo
398
409
  if evaluation is None:
@@ -563,7 +574,7 @@ def review(
563
574
  with run_lock.hold(run):
564
575
  plan = plan_review(run, revision)
565
576
  typer.echo(f"{plan['review_calls']} review calls as review revision {revision}.")
566
- if not yes and not typer.confirm("Start review?"):
577
+ if not yes and not _confirm("Start review?"):
567
578
  raise typer.Exit()
568
579
  _review(run, revision)
569
580
  except (OSError, ValueError, subprocess.SubprocessError) as exc:
@@ -658,12 +669,15 @@ def judge_run(
658
669
  @judge_app.command("tally")
659
670
  def judge_tally(
660
671
  directory: Path = typer.Argument(..., help="A directory whose judge calls are complete."),
672
+ require_bound: bool = typer.Option(
673
+ False, help="Refuse legacy verdicts without input bindings."
674
+ ),
661
675
  ) -> None:
662
676
  """Unblind with the key and score each pair; writes tally.json; no model calls."""
663
677
  from pluginbench.judge import format_tally, tally
664
678
 
665
679
  try:
666
- result = tally(directory)
680
+ result = tally(directory, require_bound=require_bound)
667
681
  (directory / "tally.json").write_text(json.dumps(result, indent=2) + "\n")
668
682
  except (OSError, ValueError, KeyError) as exc:
669
683
  _error(exc)
@@ -299,9 +299,10 @@ def invoke(
299
299
  prompt: str,
300
300
  timeout: int,
301
301
  output_schema: Path | None = None,
302
+ artifacts: Path | None = None,
302
303
  ) -> dict[str, Any]:
303
304
  """Invoke a local reviewer with an OS-enforced read-only source packet."""
304
- artifacts = area / "artifacts"
305
+ artifacts = area / "artifacts" if artifacts is None else artifacts
305
306
  artifacts.mkdir(exist_ok=True)
306
307
  if any((artifacts / name).exists() for name in ("invocation.json", "execution.json")):
307
308
  raise ValueError("native invocation artifacts already exist; use a fresh trial area")