pluginbench 0.5.0__py3-none-any.whl
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.
- pluginbench/__init__.py +3 -0
- pluginbench/__main__.py +3 -0
- pluginbench/assessment-charts.html +790 -0
- pluginbench/assessment.py +316 -0
- pluginbench/assessment_charts.py +1225 -0
- pluginbench/assessment_metrics.py +1956 -0
- pluginbench/assessment_models.py +127 -0
- pluginbench/baselines.py +792 -0
- pluginbench/claude.py +374 -0
- pluginbench/cli.py +672 -0
- pluginbench/code_diff.py +207 -0
- pluginbench/codex.py +451 -0
- pluginbench/codex_events.py +585 -0
- pluginbench/collectors.py +591 -0
- pluginbench/compare.py +927 -0
- pluginbench/components.py +129 -0
- pluginbench/coverage_report.py +610 -0
- pluginbench/datasets.py +207 -0
- pluginbench/evaluation.example.yaml +45 -0
- pluginbench/evaluation.py +1651 -0
- pluginbench/evaluation_report.py +1004 -0
- pluginbench/fallow_parse.mjs +101 -0
- pluginbench/harbor_backend.py +2946 -0
- pluginbench/isolation.py +314 -0
- pluginbench/judge.py +683 -0
- pluginbench/language_analysis.py +915 -0
- pluginbench/ledger.py +217 -0
- pluginbench/measurement_profiles.py +616 -0
- pluginbench/native_treatment_runtime.js +221 -0
- pluginbench/native_treatments.py +155 -0
- pluginbench/observer_data/go-analysis-v1/go.mod +10 -0
- pluginbench/observer_data/go-analysis-v1/go.sum +10 -0
- pluginbench/observer_data/go-analysis-v1/main.go +64 -0
- pluginbench/observer_data/oxc-parser-0.126.0/package-lock.json +449 -0
- pluginbench/observer_data/oxc-parser-0.126.0/package.json +5 -0
- pluginbench/parallel.py +101 -0
- pluginbench/processes.py +100 -0
- pluginbench/remeasure.py +992 -0
- pluginbench/run_lock.py +227 -0
- pluginbench/run_status.py +518 -0
- pluginbench/study.py +1858 -0
- pluginbench/study_review.py +924 -0
- pluginbench/submissions.py +486 -0
- pluginbench/task_sources.py +403 -0
- pluginbench/treatments.py +388 -0
- pluginbench/trial_containers.py +94 -0
- pluginbench/vendor/ECHARTS-LICENSE +222 -0
- pluginbench/vendor/ECHARTS-NOTICE +5 -0
- pluginbench/vendor/ZRENDER-LICENSE +29 -0
- pluginbench/vendor/echarts.min.js +48 -0
- pluginbench-0.5.0.dist-info/METADATA +294 -0
- pluginbench-0.5.0.dist-info/RECORD +58 -0
- pluginbench-0.5.0.dist-info/WHEEL +4 -0
- pluginbench-0.5.0.dist-info/entry_points.txt +2 -0
- pluginbench-0.5.0.dist-info/licenses/LICENSE +201 -0
- pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-LICENSE +222 -0
- pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-NOTICE +5 -0
- pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ZRENDER-LICENSE +29 -0
pluginbench/claude.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Claude Code capabilities and evidence for isolated read-only reviews."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
REQUIRED_FLAGS = (
|
|
14
|
+
"--safe-mode",
|
|
15
|
+
"--setting-sources",
|
|
16
|
+
"--strict-mcp-config",
|
|
17
|
+
"--mcp-config",
|
|
18
|
+
"--settings",
|
|
19
|
+
"--disable-slash-commands",
|
|
20
|
+
"--tools",
|
|
21
|
+
"--model",
|
|
22
|
+
"--effort",
|
|
23
|
+
"--output-format",
|
|
24
|
+
"--verbose",
|
|
25
|
+
"--no-session-persistence",
|
|
26
|
+
"--no-chrome",
|
|
27
|
+
"--restricted",
|
|
28
|
+
"--permission-mode",
|
|
29
|
+
"--permission-prompts",
|
|
30
|
+
"--json-schema",
|
|
31
|
+
)
|
|
32
|
+
AUTH_NAMES = ("CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY")
|
|
33
|
+
_NO_CREDENTIAL = (
|
|
34
|
+
"Claude authentication unavailable: neither CLAUDE_CODE_OAUTH_TOKEN nor ANTHROPIC_API_KEY "
|
|
35
|
+
"is set in the environment of this process. A service, such as a systemd unit, does not "
|
|
36
|
+
"inherit a variable that a shell profile exports; set one in the service's environment"
|
|
37
|
+
)
|
|
38
|
+
READ_TOOLS = "Read,Glob,Grep"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def preflight() -> dict[str, Any]:
|
|
42
|
+
"""Check installed flags without authenticating or invoking a model."""
|
|
43
|
+
outputs = {}
|
|
44
|
+
for name in ("version", "help"):
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
["claude", f"--{name}"], capture_output=True, text=True, timeout=15, check=False
|
|
47
|
+
)
|
|
48
|
+
if result.returncode:
|
|
49
|
+
raise ValueError(f"claude {name} check failed (exit {result.returncode})")
|
|
50
|
+
outputs[name] = result.stdout
|
|
51
|
+
missing = [flag for flag in REQUIRED_FLAGS if flag not in outputs["help"]]
|
|
52
|
+
if missing:
|
|
53
|
+
raise ValueError("native claude lacks required isolation flags: " + ", ".join(missing))
|
|
54
|
+
return {
|
|
55
|
+
"harness": "claude-cli",
|
|
56
|
+
"harness_version": outputs["version"].strip(),
|
|
57
|
+
"help": outputs["help"],
|
|
58
|
+
"required_flags": list(REQUIRED_FLAGS),
|
|
59
|
+
"paid_calls": 0,
|
|
60
|
+
"effective_effort_status": "unknown unless reported by native session events",
|
|
61
|
+
"managed_policy": "safe mode does not bypass managed settings",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def command(
|
|
66
|
+
*,
|
|
67
|
+
model: str,
|
|
68
|
+
effort: str = "medium",
|
|
69
|
+
) -> list[str]:
|
|
70
|
+
if effort not in {"low", "medium", "high", "xhigh", "max"}:
|
|
71
|
+
raise ValueError(f"unsupported Claude effort: {effort}")
|
|
72
|
+
# cwd is set by the shared runner, not a prompt or --add-dir discovery surface.
|
|
73
|
+
return [
|
|
74
|
+
"claude",
|
|
75
|
+
"--print",
|
|
76
|
+
"--safe-mode",
|
|
77
|
+
"--model",
|
|
78
|
+
model,
|
|
79
|
+
"--effort",
|
|
80
|
+
effort,
|
|
81
|
+
"--setting-sources",
|
|
82
|
+
"",
|
|
83
|
+
"--strict-mcp-config",
|
|
84
|
+
"--mcp-config",
|
|
85
|
+
'{"mcpServers":{}}',
|
|
86
|
+
"--settings",
|
|
87
|
+
'{"disableAllHooks":true,"autoMemoryEnabled":false}',
|
|
88
|
+
"--disable-slash-commands",
|
|
89
|
+
"--tools",
|
|
90
|
+
READ_TOOLS,
|
|
91
|
+
"--restricted",
|
|
92
|
+
"--permission-mode",
|
|
93
|
+
"dontAsk",
|
|
94
|
+
"--permission-prompts",
|
|
95
|
+
"none",
|
|
96
|
+
"--output-format",
|
|
97
|
+
"stream-json",
|
|
98
|
+
"--verbose",
|
|
99
|
+
"--no-session-persistence",
|
|
100
|
+
"--no-chrome",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def runtime_environment(
|
|
105
|
+
private: Path, host: str = "claude", *, auth_file: Path | None = None
|
|
106
|
+
) -> tuple[dict[str, str], tuple[str, ...]]:
|
|
107
|
+
"""Use a fresh config/home and pass only selected authentication scalars."""
|
|
108
|
+
if host != "claude":
|
|
109
|
+
raise ValueError(f"unsupported Claude host: {host}")
|
|
110
|
+
if auth_file is not None:
|
|
111
|
+
selected = json.loads(auth_file.read_text())
|
|
112
|
+
if not isinstance(selected, dict) or set(selected) - set(AUTH_NAMES):
|
|
113
|
+
raise ValueError("Claude auth file must contain only a supported credential variable")
|
|
114
|
+
else:
|
|
115
|
+
selected = {name: os.environ[name] for name in AUTH_NAMES if os.environ.get(name)}
|
|
116
|
+
if not selected:
|
|
117
|
+
# Retain the existing native macOS subscription-auth path, not its settings.
|
|
118
|
+
try:
|
|
119
|
+
credentials = subprocess.run(
|
|
120
|
+
["security", "find-generic-password", "-s", "Claude Code-credentials", "-w"],
|
|
121
|
+
capture_output=True,
|
|
122
|
+
text=True,
|
|
123
|
+
timeout=15,
|
|
124
|
+
check=False,
|
|
125
|
+
)
|
|
126
|
+
except OSError as error:
|
|
127
|
+
raise ValueError(_NO_CREDENTIAL) from error
|
|
128
|
+
if credentials.returncode:
|
|
129
|
+
raise ValueError(_NO_CREDENTIAL)
|
|
130
|
+
try:
|
|
131
|
+
token = json.loads(credentials.stdout)["claudeAiOauth"]["accessToken"]
|
|
132
|
+
except (KeyError, TypeError, ValueError) as error:
|
|
133
|
+
raise ValueError("Claude Keychain OAuth access token unavailable") from error
|
|
134
|
+
selected = {"CLAUDE_CODE_OAUTH_TOKEN": token}
|
|
135
|
+
if len(selected) != 1 or not all(
|
|
136
|
+
isinstance(value, str) and value.strip() for value in selected.values()
|
|
137
|
+
):
|
|
138
|
+
raise ValueError("Select exactly one nonempty Claude OAuth token or API key")
|
|
139
|
+
private.mkdir(mode=0o700)
|
|
140
|
+
home, temporary, config = (private / name for name in ("home", "tmp", "claude"))
|
|
141
|
+
for directory in (home, temporary, config):
|
|
142
|
+
directory.mkdir(mode=0o700)
|
|
143
|
+
env = {
|
|
144
|
+
key: value
|
|
145
|
+
for key, value in os.environ.items()
|
|
146
|
+
if key
|
|
147
|
+
in {
|
|
148
|
+
"PATH",
|
|
149
|
+
"LANG",
|
|
150
|
+
"LC_ALL",
|
|
151
|
+
"SSL_CERT_FILE",
|
|
152
|
+
"SSL_CERT_DIR",
|
|
153
|
+
"NODE_EXTRA_CA_CERTS",
|
|
154
|
+
"HTTP_PROXY",
|
|
155
|
+
"HTTPS_PROXY",
|
|
156
|
+
"NO_PROXY",
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
env.update(
|
|
160
|
+
HOME=str(home),
|
|
161
|
+
TMPDIR=str(temporary),
|
|
162
|
+
XDG_CONFIG_HOME=str(home / ".config"),
|
|
163
|
+
NO_COLOR="1",
|
|
164
|
+
CLAUDE_CONFIG_DIR=str(config),
|
|
165
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC="1",
|
|
166
|
+
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB="1",
|
|
167
|
+
)
|
|
168
|
+
env.update(selected)
|
|
169
|
+
return env, tuple(selected.values())
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _count(value: object) -> bool:
|
|
173
|
+
return isinstance(value, int) and not isinstance(value, bool) and value >= 0
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _usage(result: dict[str, Any]) -> tuple[dict[str, int] | None, str]:
|
|
177
|
+
# modelUsage covers nested/auxiliary pipeline calls; usage covers the main loop.
|
|
178
|
+
models = result.get("modelUsage")
|
|
179
|
+
if isinstance(models, dict) and models:
|
|
180
|
+
entries = list(models.values())
|
|
181
|
+
fields = {
|
|
182
|
+
"input_tokens": ("inputTokens", "cacheReadInputTokens", "cacheCreationInputTokens"),
|
|
183
|
+
"output_tokens": ("outputTokens",),
|
|
184
|
+
"cached_input_tokens": ("cacheReadInputTokens",),
|
|
185
|
+
"cache_creation_input_tokens": ("cacheCreationInputTokens",),
|
|
186
|
+
}
|
|
187
|
+
source = "native modelUsage; all query-pipeline models; cached input included"
|
|
188
|
+
else:
|
|
189
|
+
usage = result.get("usage")
|
|
190
|
+
if not isinstance(usage, dict):
|
|
191
|
+
return None, "unknown"
|
|
192
|
+
entries = [usage]
|
|
193
|
+
fields = {
|
|
194
|
+
"input_tokens": (
|
|
195
|
+
"input_tokens",
|
|
196
|
+
"cache_read_input_tokens",
|
|
197
|
+
"cache_creation_input_tokens",
|
|
198
|
+
),
|
|
199
|
+
"output_tokens": ("output_tokens",),
|
|
200
|
+
"cached_input_tokens": ("cache_read_input_tokens",),
|
|
201
|
+
"cache_creation_input_tokens": ("cache_creation_input_tokens",),
|
|
202
|
+
}
|
|
203
|
+
source = "native usage; main loop only; cached input included"
|
|
204
|
+
measured = {}
|
|
205
|
+
for key, native_keys in fields.items():
|
|
206
|
+
if all(
|
|
207
|
+
isinstance(entry, dict) and all(_count(entry.get(k)) for k in native_keys)
|
|
208
|
+
for entry in entries
|
|
209
|
+
):
|
|
210
|
+
measured[key] = sum(entry[k] for entry in entries for k in native_keys)
|
|
211
|
+
return measured or None, source
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def observed_cli_version(startup: object) -> str | None:
|
|
215
|
+
if not isinstance(startup, list):
|
|
216
|
+
return None
|
|
217
|
+
versions = [
|
|
218
|
+
event.get("claude_code_version", event.get("cli_version"))
|
|
219
|
+
for event in startup
|
|
220
|
+
if isinstance(event, dict)
|
|
221
|
+
and event.get("type") == "system"
|
|
222
|
+
and event.get("subtype") == "init"
|
|
223
|
+
]
|
|
224
|
+
if not versions or not all(
|
|
225
|
+
isinstance(version, str) and version.strip() for version in versions
|
|
226
|
+
):
|
|
227
|
+
return None
|
|
228
|
+
return versions[0] if len(set(versions)) == 1 else None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _builtin_plugin(plugin: object) -> bool:
|
|
232
|
+
"""A plugin the CLI ships and loads by itself; no setting of the run selects it.
|
|
233
|
+
|
|
234
|
+
Its `path` is the word `builtin`, where an installed plugin has a directory. The set is
|
|
235
|
+
part of the CLI version, which the run records, and stays in the saved startup event.
|
|
236
|
+
"""
|
|
237
|
+
return (
|
|
238
|
+
isinstance(plugin, dict)
|
|
239
|
+
and plugin.get("path") == "builtin"
|
|
240
|
+
and isinstance(plugin.get("name"), str)
|
|
241
|
+
and plugin.get("source") == f"{plugin['name']}@builtin"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def parse_events(
|
|
246
|
+
stdout: str, traces: list[str], host: str = "claude", *, requested_effort: str = "medium"
|
|
247
|
+
) -> dict[str, Any]:
|
|
248
|
+
"""Normalize native receipts; never infer effective effort from the launch flag."""
|
|
249
|
+
if host != "claude":
|
|
250
|
+
raise ValueError(f"unsupported Claude host: {host}")
|
|
251
|
+
errors: list[Any] = []
|
|
252
|
+
startup: list[dict[str, Any]] = []
|
|
253
|
+
models: set[str] = set()
|
|
254
|
+
efforts: set[str] = set()
|
|
255
|
+
commands: list[dict[str, Any]] = []
|
|
256
|
+
tools: list[dict[str, Any]] = []
|
|
257
|
+
messages: list[str] = []
|
|
258
|
+
pending: dict[str, dict[str, Any]] = {}
|
|
259
|
+
result: dict[str, Any] = {}
|
|
260
|
+
for line in stdout.splitlines():
|
|
261
|
+
try:
|
|
262
|
+
event = json.loads(line)
|
|
263
|
+
except ValueError:
|
|
264
|
+
errors.append("malformed stdout JSONL")
|
|
265
|
+
continue
|
|
266
|
+
if not isinstance(event, dict):
|
|
267
|
+
errors.append("stdout JSONL event is not an object")
|
|
268
|
+
continue
|
|
269
|
+
if event.get("type") == "system" and event.get("subtype") == "init":
|
|
270
|
+
startup.append(event)
|
|
271
|
+
if isinstance(event.get("effort"), str) and event["effort"].strip():
|
|
272
|
+
efforts.add(event["effort"])
|
|
273
|
+
plugins = event.get("plugins") or []
|
|
274
|
+
if (
|
|
275
|
+
not isinstance(plugins, list)
|
|
276
|
+
or not all(_builtin_plugin(plugin) for plugin in plugins)
|
|
277
|
+
or any(
|
|
278
|
+
event.get(key) for key in ("mcp_servers", "plugin_errors", "mcp_server_errors")
|
|
279
|
+
)
|
|
280
|
+
):
|
|
281
|
+
errors.append("custom plugin or MCP configuration present in native startup")
|
|
282
|
+
if event.get("type") == "system" and str(event.get("subtype", "")).startswith("hook_"):
|
|
283
|
+
errors.append("native hook ran during isolated invocation")
|
|
284
|
+
if event.get("type") == "result":
|
|
285
|
+
result = event
|
|
286
|
+
if event.get("type") not in {"assistant", "user"}:
|
|
287
|
+
continue
|
|
288
|
+
if event.get("parent_tool_use_id") is not None:
|
|
289
|
+
errors.append("delegated work appeared in isolated native invocation")
|
|
290
|
+
message = event.get("message")
|
|
291
|
+
if (
|
|
292
|
+
isinstance(message, dict)
|
|
293
|
+
and event.get("type") == "user"
|
|
294
|
+
and isinstance(message.get("content"), str)
|
|
295
|
+
):
|
|
296
|
+
continue
|
|
297
|
+
if not isinstance(message, dict) or not isinstance(message.get("content"), list):
|
|
298
|
+
errors.append("malformed native message")
|
|
299
|
+
continue
|
|
300
|
+
if (
|
|
301
|
+
event.get("type") == "assistant"
|
|
302
|
+
and isinstance(message.get("model"), str)
|
|
303
|
+
and message["model"].strip()
|
|
304
|
+
):
|
|
305
|
+
models.add(message["model"])
|
|
306
|
+
for content in message["content"]:
|
|
307
|
+
if not isinstance(content, dict):
|
|
308
|
+
errors.append("malformed native content block")
|
|
309
|
+
elif content.get("type") == "text" and isinstance(content.get("text"), str):
|
|
310
|
+
if event.get("type") == "assistant":
|
|
311
|
+
messages.append(content["text"])
|
|
312
|
+
elif content.get("type") == "tool_use":
|
|
313
|
+
tools.append(content)
|
|
314
|
+
if isinstance(content.get("id"), str):
|
|
315
|
+
pending[content["id"]] = content
|
|
316
|
+
elif content.get("type") == "tool_result":
|
|
317
|
+
tool_id = content.get("tool_use_id")
|
|
318
|
+
call = pending.get(tool_id, {}) if isinstance(tool_id, str) else {}
|
|
319
|
+
commands.append(
|
|
320
|
+
{
|
|
321
|
+
"id": content.get("tool_use_id"),
|
|
322
|
+
"command": call,
|
|
323
|
+
"status": "completed",
|
|
324
|
+
"exit_code": 1 if content.get("is_error") else 0,
|
|
325
|
+
"aggregated_output": content.get("content"),
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
if not models:
|
|
329
|
+
models.update(
|
|
330
|
+
event["model"]
|
|
331
|
+
for event in startup
|
|
332
|
+
if isinstance(event.get("model"), str) and event["model"].strip()
|
|
333
|
+
)
|
|
334
|
+
usage, token_format = _usage(result)
|
|
335
|
+
structured = result.get("structured_output")
|
|
336
|
+
final = (
|
|
337
|
+
json.dumps(structured) if isinstance(structured, (dict, list)) else result.get("result", "")
|
|
338
|
+
)
|
|
339
|
+
if not isinstance(final, str):
|
|
340
|
+
errors.append("invalid native result text")
|
|
341
|
+
final = ""
|
|
342
|
+
if result.get("is_error") or result.get("errors"):
|
|
343
|
+
errors.append(result)
|
|
344
|
+
if not startup:
|
|
345
|
+
errors.append("native initialization evidence missing")
|
|
346
|
+
dollars = result.get("total_cost_usd")
|
|
347
|
+
if (
|
|
348
|
+
not isinstance(dollars, (int, float))
|
|
349
|
+
or isinstance(dollars, bool)
|
|
350
|
+
or not math.isfinite(dollars)
|
|
351
|
+
or dollars < 0
|
|
352
|
+
):
|
|
353
|
+
dollars = None
|
|
354
|
+
return {
|
|
355
|
+
"command_items": commands,
|
|
356
|
+
"actual_primary_models": sorted(models),
|
|
357
|
+
"usage": usage,
|
|
358
|
+
"partial_usage": [],
|
|
359
|
+
"token_format": token_format,
|
|
360
|
+
"native_dollars": dollars,
|
|
361
|
+
"dollars_status": "reported estimate" if dollars is not None else "unknown",
|
|
362
|
+
"requested_effort": requested_effort,
|
|
363
|
+
"runtime_effort_evidence": sorted(efforts),
|
|
364
|
+
"effort_evidence_source": "native session event"
|
|
365
|
+
if efforts
|
|
366
|
+
else "requested launch setting; effective effort unverified",
|
|
367
|
+
"startup": startup,
|
|
368
|
+
"observed_cli_version": observed_cli_version(startup),
|
|
369
|
+
"other_tools": tools,
|
|
370
|
+
"stream_errors": errors,
|
|
371
|
+
"final": final,
|
|
372
|
+
"agent_messages": messages,
|
|
373
|
+
"successful_native_completion": result.get("subtype") == "success" and not errors,
|
|
374
|
+
}
|