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.
Files changed (58) hide show
  1. pluginbench/__init__.py +3 -0
  2. pluginbench/__main__.py +3 -0
  3. pluginbench/assessment-charts.html +790 -0
  4. pluginbench/assessment.py +316 -0
  5. pluginbench/assessment_charts.py +1225 -0
  6. pluginbench/assessment_metrics.py +1956 -0
  7. pluginbench/assessment_models.py +127 -0
  8. pluginbench/baselines.py +792 -0
  9. pluginbench/claude.py +374 -0
  10. pluginbench/cli.py +672 -0
  11. pluginbench/code_diff.py +207 -0
  12. pluginbench/codex.py +451 -0
  13. pluginbench/codex_events.py +585 -0
  14. pluginbench/collectors.py +591 -0
  15. pluginbench/compare.py +927 -0
  16. pluginbench/components.py +129 -0
  17. pluginbench/coverage_report.py +610 -0
  18. pluginbench/datasets.py +207 -0
  19. pluginbench/evaluation.example.yaml +45 -0
  20. pluginbench/evaluation.py +1651 -0
  21. pluginbench/evaluation_report.py +1004 -0
  22. pluginbench/fallow_parse.mjs +101 -0
  23. pluginbench/harbor_backend.py +2946 -0
  24. pluginbench/isolation.py +314 -0
  25. pluginbench/judge.py +683 -0
  26. pluginbench/language_analysis.py +915 -0
  27. pluginbench/ledger.py +217 -0
  28. pluginbench/measurement_profiles.py +616 -0
  29. pluginbench/native_treatment_runtime.js +221 -0
  30. pluginbench/native_treatments.py +155 -0
  31. pluginbench/observer_data/go-analysis-v1/go.mod +10 -0
  32. pluginbench/observer_data/go-analysis-v1/go.sum +10 -0
  33. pluginbench/observer_data/go-analysis-v1/main.go +64 -0
  34. pluginbench/observer_data/oxc-parser-0.126.0/package-lock.json +449 -0
  35. pluginbench/observer_data/oxc-parser-0.126.0/package.json +5 -0
  36. pluginbench/parallel.py +101 -0
  37. pluginbench/processes.py +100 -0
  38. pluginbench/remeasure.py +992 -0
  39. pluginbench/run_lock.py +227 -0
  40. pluginbench/run_status.py +518 -0
  41. pluginbench/study.py +1858 -0
  42. pluginbench/study_review.py +924 -0
  43. pluginbench/submissions.py +486 -0
  44. pluginbench/task_sources.py +403 -0
  45. pluginbench/treatments.py +388 -0
  46. pluginbench/trial_containers.py +94 -0
  47. pluginbench/vendor/ECHARTS-LICENSE +222 -0
  48. pluginbench/vendor/ECHARTS-NOTICE +5 -0
  49. pluginbench/vendor/ZRENDER-LICENSE +29 -0
  50. pluginbench/vendor/echarts.min.js +48 -0
  51. pluginbench-0.5.0.dist-info/METADATA +294 -0
  52. pluginbench-0.5.0.dist-info/RECORD +58 -0
  53. pluginbench-0.5.0.dist-info/WHEEL +4 -0
  54. pluginbench-0.5.0.dist-info/entry_points.txt +2 -0
  55. pluginbench-0.5.0.dist-info/licenses/LICENSE +201 -0
  56. pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-LICENSE +222 -0
  57. pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-NOTICE +5 -0
  58. 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
+ }