@tangle-network/agent-bench 0.1.0 → 0.3.5

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 (139) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/HARNESS.md +302 -0
  3. package/README.md +26 -1
  4. package/fixtures/aec-bench.json +18 -0
  5. package/fixtures/agentbench-dbbench.json +22 -0
  6. package/fixtures/bfcl.json +45 -0
  7. package/fixtures/commit0.json +72 -0
  8. package/fixtures/crag.json +10 -0
  9. package/fixtures/dabstep.json +22 -0
  10. package/fixtures/enterpriseops-gym.json +103 -0
  11. package/fixtures/finresearchbench.json +21 -0
  12. package/fixtures/finsearchcomp.json +66 -0
  13. package/fixtures/frames.json +26 -0
  14. package/fixtures/hotpotqa.json +182 -0
  15. package/fixtures/nomiracl.json +26 -0
  16. package/fixtures/open-rag-bench.json +16 -0
  17. package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
  18. package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
  19. package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
  20. package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
  21. package/fixtures/pier-agent/no-model-task/task.toml +35 -0
  22. package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
  23. package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
  24. package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
  25. package/fixtures/programbench.json +17 -0
  26. package/fixtures/ragbench.json +21 -0
  27. package/fixtures/simpleqa.json +121 -0
  28. package/fixtures/t2-ragbench.json +13 -0
  29. package/fixtures/tau2-bench.json +16 -0
  30. package/fixtures/tau3-banking.json +16 -0
  31. package/fixtures/toollm.json +28 -0
  32. package/fixtures/webarena-verified.json +20 -0
  33. package/package.json +39 -15
  34. package/pier_agents/__init__.py +18 -0
  35. package/pier_agents/candidate_contract.py +755 -0
  36. package/pier_agents/process_boundary.py +321 -0
  37. package/pier_agents/tangle_candidate.py +907 -0
  38. package/pier_agents/workspace_boundary.py +368 -0
  39. package/scripts/appworld_driver.py +359 -0
  40. package/scripts/cadbench_prepare.py +22 -0
  41. package/scripts/cadgenbench_hard_parts.py +48 -0
  42. package/scripts/clbench_codebase_judge.py +73 -0
  43. package/scripts/commit0_judge.py +170 -0
  44. package/scripts/dabstep_judge.py +42 -0
  45. package/scripts/enterpriseops_gym_judge.py +281 -0
  46. package/scripts/programbench_judge.py +120 -0
  47. package/scripts/render-gate-chart.mjs +176 -0
  48. package/scripts/run-package-tests.mjs +56 -0
  49. package/scripts/terminate-pier-trial.mts +66 -0
  50. package/scripts/trata-hedge/README.md +56 -0
  51. package/scripts/trata-hedge/run.sh +60 -0
  52. package/scripts/trata-hedge/solve.py +83 -0
  53. package/scripts/verify-packed-consumer.mjs +224 -0
  54. package/scripts/verify-pier-agent.mts +715 -0
  55. package/scripts/verify-pier-pair.mts +74 -0
  56. package/scripts/verify-pier-recovery.mts +139 -0
  57. package/src/adapters.ts +26 -0
  58. package/src/benchmarks/_harness.test.mts +178 -0
  59. package/src/benchmarks/_harness.ts +239 -16
  60. package/src/benchmarks/agentbench.ts +163 -0
  61. package/src/benchmarks/appworld.test.mts +15 -9
  62. package/src/benchmarks/bfcl.ts +346 -0
  63. package/src/benchmarks/crag.ts +137 -0
  64. package/src/benchmarks/dabstep.test.mts +70 -0
  65. package/src/benchmarks/dabstep.ts +212 -0
  66. package/src/benchmarks/external-adapters.test.mts +150 -0
  67. package/src/benchmarks/finresearchbench.ts +269 -0
  68. package/src/benchmarks/humaneval.ts +20 -8
  69. package/src/benchmarks/nomiracl.ts +180 -0
  70. package/src/benchmarks/open-rag-bench.ts +153 -0
  71. package/src/benchmarks/rag-benchmarks.test.mts +138 -0
  72. package/src/benchmarks/rag-shared.ts +327 -0
  73. package/src/benchmarks/ragbench.ts +171 -0
  74. package/src/benchmarks/swe-bench.test.mts +61 -0
  75. package/src/benchmarks/swe-bench.ts +201 -19
  76. package/src/benchmarks/t2-ragbench.ts +166 -0
  77. package/src/benchmarks/tau-bench-shared.ts +214 -0
  78. package/src/benchmarks/tau2-bench.ts +30 -0
  79. package/src/benchmarks/tau3-banking.ts +29 -0
  80. package/src/benchmarks/terminal-bench.test.mts +33 -0
  81. package/src/benchmarks/terminal-bench.ts +23 -8
  82. package/src/benchmarks/toollm.ts +254 -0
  83. package/src/benchmarks/types.ts +42 -0
  84. package/src/benchmarks/webarena-verified.ts +200 -0
  85. package/src/commit0-prereqs.sh +0 -0
  86. package/src/coordination-mcp-container-reach.mts +181 -0
  87. package/src/decoder-live.mts +1 -1
  88. package/src/examples/README.md +103 -39
  89. package/src/examples/benchmark-matrix.mts +101 -0
  90. package/src/examples/lean-proof-gate.README.md +77 -0
  91. package/src/examples/lean-proof-gate.mts +162 -0
  92. package/src/examples/lean-verify.ts +95 -0
  93. package/src/examples/lean.Dockerfile +12 -0
  94. package/src/examples/math-demo.mts +9 -7
  95. package/src/examples/strategy-demo.mts +10 -12
  96. package/src/gate.ts +3 -2
  97. package/src/hev-eval.mts +69 -0
  98. package/src/hev-improve.mts +169 -0
  99. package/src/hev-structural.mts +688 -0
  100. package/src/index.ts +73 -0
  101. package/src/mbpp-structural.mts +662 -0
  102. package/src/pier-agent.test-fixtures.mts +19 -0
  103. package/src/pier-agent.test.mts +363 -0
  104. package/src/pier-agent.ts +657 -0
  105. package/src/pier-result-grader.mjs +30 -0
  106. package/src/pier-result-grader.test.mts +62 -0
  107. package/src/pier-result-grader.ts +108 -0
  108. package/src/pier-task-outcome.test.mts +117 -0
  109. package/src/pier-task-outcome.ts +240 -0
  110. package/src/pier-trial-controller.test.mts +412 -0
  111. package/src/pier-trial-controller.ts +858 -0
  112. package/src/pier-trial-supervisor.mjs +352 -0
  113. package/src/resolve-client.ts +25 -2
  114. package/src/run-benchmarks-cli.mts +72 -0
  115. package/src/run-benchmarks-report.ts +66 -0
  116. package/src/run-benchmarks.test.mts +231 -0
  117. package/src/run-benchmarks.ts +589 -0
  118. package/src/smoke-structural-rollout.mts +393 -0
  119. package/src/swe-bench-env.test.ts +207 -0
  120. package/src/swe-bench-env.ts +554 -0
  121. package/src/swe-jail.ts +293 -0
  122. package/src/swe-self-improve.mts +84 -0
  123. package/src/swe-structural-judge-policy.test.ts +117 -0
  124. package/src/swe-structural-judge-policy.ts +133 -0
  125. package/src/swe-structural-policy.test.ts +124 -0
  126. package/src/swe-structural-policy.ts +132 -0
  127. package/src/swe-structural-provenance.test.ts +93 -0
  128. package/src/swe-structural-provenance.ts +138 -0
  129. package/src/swe-structural.mts +1260 -0
  130. package/src/swe-temp.ts +14 -0
  131. package/src/tb-container-executor.mts +234 -0
  132. package/src/tb-container-executor.test.mts +99 -0
  133. package/src/tb-supervisor-sidecar.mts +222 -0
  134. package/src/trata-gepa.mts +1 -1
  135. package/steerers/eops-itsm-population.json +1 -0
  136. package/tb_agents/opencode_refine_agent.py +117 -0
  137. package/tb_agents/opencode_router_agent.py +406 -0
  138. package/tb_agents/opencode_supervisor_agent.py +239 -0
  139. package/tb_agents/script_agent.py +66 -0
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env python3
2
+ """DABStep judge bridge.
3
+
4
+ Reads {"prediction": str, "golds": list} from stdin and delegates scoring to
5
+ the official DABStep grade.py module. This script owns no grading semantics.
6
+ """
7
+
8
+ import argparse
9
+ import importlib.util
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+
14
+
15
+ def load_grade(grade_file: Path):
16
+ spec = importlib.util.spec_from_file_location("dabstep_grade", grade_file)
17
+ if spec is None or spec.loader is None:
18
+ raise RuntimeError(f"could not import DABStep grade file: {grade_file}")
19
+ module = importlib.util.module_from_spec(spec)
20
+ spec.loader.exec_module(module)
21
+ return module.grade
22
+
23
+
24
+ def main() -> int:
25
+ parser = argparse.ArgumentParser(description="Score one DABStep answer")
26
+ parser.add_argument("--grade-file", required=True)
27
+ args = parser.parse_args()
28
+
29
+ try:
30
+ payload = json.loads(sys.stdin.read())
31
+ prediction = payload["prediction"]
32
+ golds = payload["golds"]
33
+ correct = bool(load_grade(Path(args.grade_file))(prediction, golds))
34
+ print(json.dumps({"correct": correct, "score": 1.0 if correct else 0.0}))
35
+ return 0
36
+ except Exception as exc:
37
+ print(json.dumps({"error": str(exc)}))
38
+ return 1
39
+
40
+
41
+ if __name__ == "__main__":
42
+ raise SystemExit(main())
@@ -0,0 +1,281 @@
1
+ # EnterpriseOps-Gym judge driver.
2
+ #
3
+ # Scores a worker's tool-call transcript against a task's verifiers, deterministically,
4
+ # by REPLAYING the transcript into a FRESHLY-SEEDED gym database and then reading the
5
+ # verifier SQL back. The full deployable-checker loop (matches the official benchmark/
6
+ # executor.py protocol):
7
+ #
8
+ # 1. SEED per gym: create an isolated database (its own database_id) from the task's
9
+ # seed_database_file SQL snapshot via POST /api/seed-database. Every later call
10
+ # targets that database via the `x-database-id` header, so concurrent judges on
11
+ # one container never collide.
12
+ # 2. REPLAY each transcript call as an MCP JSON-RPC `tools/call` to /mcp (NOT a bare
13
+ # {tool,arguments} POST, which the server accepts as a no-op 204 and never
14
+ # executes). A tool-call HTTP error is the AGENT's mistake (wrong args / policy
15
+ # violation) — counted + surfaced, never fatal; the verifiers score the result.
16
+ # A transport failure (server down) IS fatal (fail loud, never a fake score).
17
+ # 3. VERIFY each database_state verifier's SQL via /api/sql-runner (parsing the server's
18
+ # {"data":[{...}]} shape), compared to expected_value under comparison_type.
19
+ # 4. CLEAN delete each seeded database (best-effort).
20
+ #
21
+ # Seed snapshots are resolved against EOPS_GYM_DBS_DIR (the unzipped gym_dbs.zip). A task
22
+ # whose server has no seed_database_file, or an unset EOPS_GYM_DBS_DIR, fails loud — an
23
+ # unseeded run would score every solve against the same default state (no signal).
24
+
25
+ import argparse
26
+ import json
27
+ import os
28
+ import re
29
+ import sys
30
+ import urllib.error
31
+ import urllib.request
32
+ import uuid
33
+
34
+
35
+ def fail(msg: str) -> None:
36
+ print(json.dumps({"error": msg}))
37
+ sys.exit(1)
38
+
39
+
40
+ def request(url: str, payload: dict, headers: dict, method: str = "POST", timeout: float = 60.0) -> str:
41
+ body = json.dumps(payload).encode("utf-8")
42
+ req = urllib.request.Request(url, data=body, method=method)
43
+ req.add_header("Content-Type", "application/json")
44
+ req.add_header("Accept", "application/json, text/event-stream")
45
+ for k, v in (headers or {}).items():
46
+ req.add_header(k, str(v))
47
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
48
+ return resp.read().decode("utf-8")
49
+
50
+
51
+ def parse_body(raw: str) -> object:
52
+ """Parse a JSON body OR an SSE `data: {...}` stream (the /mcp endpoint streams SSE)."""
53
+ raw = raw.strip()
54
+ if not raw:
55
+ return {}
56
+ if raw.startswith("event:") or raw.startswith("data:") or "\ndata:" in raw:
57
+ matches = re.findall(r"data:\s*(\{.*\})", raw)
58
+ if matches:
59
+ return json.loads(matches[-1])
60
+ return json.loads(raw)
61
+
62
+
63
+ def auth_headers(server: dict) -> dict:
64
+ """Context auth headers (e.g. x-itsm-user-token) plus the per-run x-database-id."""
65
+ headers = dict(server.get("context") or {})
66
+ db_id = server.get("_database_id")
67
+ if db_id:
68
+ headers["x-database-id"] = db_id
69
+ return headers
70
+
71
+
72
+ def server_for_gym(servers: list, gym_name: str) -> dict:
73
+ by_name = {s.get("mcp_server_name"): s for s in servers}
74
+ if gym_name in by_name:
75
+ return by_name[gym_name]
76
+ if len(servers) == 1:
77
+ return servers[0]
78
+ fail(f"verifier gym_name {gym_name!r} not in gym_servers_config ({sorted(by_name)})")
79
+
80
+
81
+ def seed_databases(servers: list, gym_dbs_dir: str) -> None:
82
+ for s in servers:
83
+ seed_file = s.get("seed_database_file")
84
+ if not seed_file:
85
+ fail(
86
+ f"server {s.get('mcp_server_name')!r} has no seed_database_file — cannot seed a "
87
+ "deterministic state (an unseeded run scores every solve against the same default DB)"
88
+ )
89
+ if not gym_dbs_dir:
90
+ fail(
91
+ "EOPS_GYM_DBS_DIR is unset — cannot resolve seed_database_file. Unzip gym_dbs.zip "
92
+ "(from ServiceNow/EnterpriseOps-Gym) and set EOPS_GYM_DBS_DIR to its directory."
93
+ )
94
+ path = seed_file if os.path.isabs(seed_file) else os.path.join(gym_dbs_dir, seed_file)
95
+ try:
96
+ sql = open(path, encoding="utf-8").read()
97
+ except Exception as e: # noqa: BLE001
98
+ fail(f"reading seed file {path} failed: {e}")
99
+ db_id = "gate_" + uuid.uuid4().hex[:12]
100
+ url = s["mcp_server_url"].rstrip("/") + "/api/seed-database"
101
+ payload = {
102
+ "database_id": db_id,
103
+ "name": f"gate_{db_id}",
104
+ "description": f"gate seed from {os.path.basename(seed_file)}",
105
+ "sql_content": sql,
106
+ }
107
+ try:
108
+ request(url, payload, {}, timeout=max(300.0, len(sql) / 1024))
109
+ except Exception as e: # noqa: BLE001
110
+ fail(f"seed-database POST to {url} failed: {e}")
111
+ s["_database_id"] = db_id
112
+
113
+
114
+ def delete_databases(servers: list) -> None:
115
+ for s in servers:
116
+ db_id = s.get("_database_id")
117
+ if not db_id:
118
+ continue
119
+ url = s["mcp_server_url"].rstrip("/") + "/api/delete-database"
120
+ try:
121
+ request(url, {"database_id": db_id}, {}, method="DELETE", timeout=30)
122
+ except Exception: # noqa: BLE001
123
+ pass # best-effort cleanup; a leaked db never corrupts a future run (unique id)
124
+
125
+
126
+ def replay_transcript(servers: list, transcript: list) -> int:
127
+ """Replay each call as an MCP tools/call. Returns the count of agent-side call errors
128
+ (surfaced, never fatal). A transport failure fails loud."""
129
+ errors = 0
130
+ for i, call in enumerate(transcript):
131
+ tool = call.get("tool")
132
+ if not isinstance(tool, str) or not tool:
133
+ errors += 1
134
+ continue
135
+ gym_name = call.get("gym_name")
136
+ server = server_for_gym(servers, gym_name) if gym_name else servers[0]
137
+ url = server["mcp_server_url"].rstrip("/") + "/mcp"
138
+ payload = {
139
+ "jsonrpc": "2.0",
140
+ "id": i + 1,
141
+ "method": "tools/call",
142
+ "params": {"name": tool, "arguments": call.get("arguments") or {}},
143
+ }
144
+ try:
145
+ raw = request(url, payload, auth_headers(server), timeout=60)
146
+ result = parse_body(raw)
147
+ # A JSON-RPC error or an isError tool result is the agent's mistake, not a judge
148
+ # failure — count it so a wholesale replay failure is visible, but keep going.
149
+ if isinstance(result, dict) and (result.get("error") or (result.get("result") or {}).get("isError")):
150
+ errors += 1
151
+ except urllib.error.HTTPError:
152
+ errors += 1 # the agent issued a bad call (wrong args / policy) — verifiers score it
153
+ except urllib.error.URLError as e:
154
+ fail(f"gym server unreachable at {url} replaying tool {tool!r}: {e}")
155
+ except Exception as e: # noqa: BLE001
156
+ fail(f"tool call {tool!r} against {url} failed unexpectedly: {e}")
157
+ return errors
158
+
159
+
160
+ def run_sql(server: dict, query: str) -> object:
161
+ url = server["mcp_server_url"].rstrip("/") + "/api/sql-runner"
162
+ payload = {"query": query, "database_id": server.get("_database_id")}
163
+ try:
164
+ out = parse_body(request(url, payload, auth_headers(server), timeout=60))
165
+ except urllib.error.URLError as e:
166
+ fail(f"gym server unreachable at {url}: {e}")
167
+ except Exception as e: # noqa: BLE001
168
+ fail(f"sql-runner POST to {url} failed: {e}")
169
+ if isinstance(out, dict):
170
+ if out.get("error"):
171
+ fail(f"sql-runner error for query {query!r}: {out['error']}")
172
+ # The gym server returns {"data":[{col:val}], ...}; older/other shapes use result/rows.
173
+ for key in ("data", "rows"):
174
+ rows = out.get(key)
175
+ if rows:
176
+ first = rows[0]
177
+ if isinstance(first, dict) and first:
178
+ return next(iter(first.values()))
179
+ if isinstance(first, list) and first:
180
+ return first[0]
181
+ return first
182
+ if "result" in out:
183
+ return out["result"]
184
+ return out
185
+
186
+
187
+ def compare(actual: object, expected: object, comparison_type: str) -> bool:
188
+ if comparison_type == "equals":
189
+ try:
190
+ return float(actual) == float(expected)
191
+ except (TypeError, ValueError):
192
+ return str(actual) == str(expected)
193
+ if comparison_type == "greater_than":
194
+ return float(actual) > float(expected)
195
+ if comparison_type == "less_than":
196
+ return float(actual) < float(expected)
197
+ if comparison_type == "contains":
198
+ return str(expected) in str(actual)
199
+ fail(f"unsupported comparison_type {comparison_type!r}")
200
+
201
+
202
+ def cmd_judge(args) -> None:
203
+ raw_transcript = sys.stdin.read()
204
+ try:
205
+ task = json.load(open(args.task_json, encoding="utf-8"))
206
+ except Exception as e: # noqa: BLE001
207
+ fail(f"reading task json {args.task_json} failed: {e}")
208
+
209
+ servers = task.get("gym_servers_config")
210
+ if isinstance(servers, str):
211
+ servers = json.loads(servers)
212
+ if not isinstance(servers, list) or not servers:
213
+ fail("task has no gym_servers_config list")
214
+
215
+ verifiers = task.get("verifiers")
216
+ if isinstance(verifiers, str):
217
+ verifiers = json.loads(verifiers)
218
+ if not isinstance(verifiers, list) or not verifiers:
219
+ fail("task has no verifiers list")
220
+
221
+ transcript: list = []
222
+ if raw_transcript.strip():
223
+ try:
224
+ parsed = json.loads(raw_transcript)
225
+ except Exception as e: # noqa: BLE001
226
+ fail(f"transcript is not valid JSON: {e}")
227
+ transcript = parsed.get("calls", []) if isinstance(parsed, dict) else parsed
228
+ if not isinstance(transcript, list):
229
+ fail('transcript must be a JSON list of tool calls (or {"calls":[...]})')
230
+
231
+ gym_dbs_dir = os.environ.get("EOPS_GYM_DBS_DIR", "")
232
+ seed_databases(servers, gym_dbs_dir)
233
+ try:
234
+ replay_errors = replay_transcript(servers, transcript)
235
+
236
+ results = []
237
+ passes = 0
238
+ for v in verifiers:
239
+ vtype = v.get("verifier_type")
240
+ if vtype != "database_state":
241
+ fail(f"verifier {v.get('name')!r} has non-deterministic verifier_type {vtype!r}")
242
+ cfg = v.get("validation_config") or {}
243
+ query = cfg.get("query")
244
+ if not isinstance(query, str) or not query.strip():
245
+ fail(f"verifier {v.get('name')!r} has no SQL query")
246
+ server = server_for_gym(servers, v.get("gym_name"))
247
+ actual = run_sql(server, query)
248
+ ok = compare(actual, cfg.get("expected_value"), cfg.get("comparison_type", "equals"))
249
+ if ok:
250
+ passes += 1
251
+ results.append({"name": v.get("name"), "passed": bool(ok)})
252
+
253
+ total = len(verifiers)
254
+ print(
255
+ json.dumps(
256
+ {
257
+ "success": passes == total,
258
+ "passes": passes,
259
+ "total": total,
260
+ "replay_calls": len(transcript),
261
+ "replay_errors": replay_errors,
262
+ "verifiers": results,
263
+ }
264
+ )
265
+ )
266
+ finally:
267
+ delete_databases(servers)
268
+
269
+
270
+ def main() -> None:
271
+ ap = argparse.ArgumentParser(description="EnterpriseOps-Gym judge driver")
272
+ sub = ap.add_subparsers(dest="cmd", required=True)
273
+ p = sub.add_parser("judge")
274
+ p.add_argument("--task-json", required=True)
275
+ args = ap.parse_args()
276
+ if args.cmd == "judge":
277
+ cmd_judge(args)
278
+
279
+
280
+ if __name__ == "__main__":
281
+ main()
@@ -0,0 +1,120 @@
1
+ # ProgramBench judge driver: materialize the worker's file-manifest submission
2
+ # (read from stdin, ===FILE:<path>=== envelopes) into a run-dir
3
+ # (<run>/<instance>/submission.tar.gz + <instance>.traj.json), pull the hidden
4
+ # test blobs with `programbench blob sync`, run `programbench eval`, then parse
5
+ # <instance>.eval.json applying the tests.json ignore mask. Emit
6
+ # {"passed","total","resolved"} as the LAST stdout line. Fail loud: any harness or
7
+ # Docker error prints {"error": "..."} and exits nonzero — never a fabricated score.
8
+
9
+ import argparse
10
+ import io
11
+ import json
12
+ import os
13
+ import subprocess
14
+ import sys
15
+ import tarfile
16
+ import tempfile
17
+ from pathlib import Path
18
+
19
+
20
+ def fail(msg: str) -> None:
21
+ print(json.dumps({"error": msg}))
22
+ sys.exit(1)
23
+
24
+
25
+ def parse_manifest(text: str) -> dict[str, str]:
26
+ """Split the ===FILE:<path>=== envelope stream into {path: contents}."""
27
+ files: dict[str, str] = {}
28
+ parts = text.split("===FILE:")
29
+ for part in parts[1:]:
30
+ header, _, body = part.partition("\n")
31
+ path = header.replace("===", "").strip()
32
+ if path:
33
+ files[path] = body
34
+ return files
35
+
36
+
37
+ def run(cmd: list[str], cwd: str | None = None) -> subprocess.CompletedProcess:
38
+ return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
39
+
40
+
41
+ def main() -> None:
42
+ ap = argparse.ArgumentParser(description="programbench single-instance judge")
43
+ ap.add_argument("--instance", required=True)
44
+ args = ap.parse_args()
45
+
46
+ submission_text = sys.stdin.read()
47
+ files = parse_manifest(submission_text)
48
+ # A submission with no compile.sh cannot build; the harness scores it 0, which
49
+ # is the honest fail-closed outcome — we still stage it so `total` is real.
50
+
51
+ pb = os.environ.get("PROGRAMBENCH_BIN", "programbench")
52
+
53
+ run_dir = tempfile.mkdtemp(prefix="programbench-run-")
54
+ try:
55
+ inst_dir = Path(run_dir) / args.instance
56
+ inst_dir.mkdir(parents=True, exist_ok=True)
57
+
58
+ # submission.tar.gz = gzipped tar of the agent's workspace files.
59
+ tar_path = inst_dir / "submission.tar.gz"
60
+ with tarfile.open(tar_path, "w:gz") as tar:
61
+ for path, content in files.items():
62
+ data = content.encode("utf-8")
63
+ info = tarfile.TarInfo(name=path)
64
+ info.size = len(data)
65
+ # compile.sh must be executable for the harness to run it.
66
+ info.mode = 0o755 if path.endswith(".sh") else 0o644
67
+ tar.addfile(info, io.BytesIO(data))
68
+
69
+ # Minimal trajectory file the harness expects alongside the submission.
70
+ (inst_dir / f"{args.instance}.traj.json").write_text(json.dumps({"instance_id": args.instance, "steps": []}))
71
+
72
+ # Pull the hidden behavioral test blobs for this instance.
73
+ sync = run([pb, "blob", "sync", args.instance], cwd=run_dir)
74
+ if sync.returncode != 0:
75
+ fail(f"programbench blob sync failed: {sync.stderr.strip()[:600]}")
76
+
77
+ # Run the cleanroom Docker image, build via compile.sh, run hidden tests.
78
+ ev = run([pb, "eval", run_dir], cwd=run_dir)
79
+ if ev.returncode != 0:
80
+ fail(f"programbench eval failed: {ev.stderr.strip()[:800]}")
81
+
82
+ eval_path = inst_dir / f"{args.instance}.eval.json"
83
+ if not eval_path.exists():
84
+ fail(f"programbench wrote no eval.json at {eval_path}: {ev.stdout.strip()[:400]}")
85
+
86
+ report = json.loads(eval_path.read_text())
87
+ if report.get("error_code"):
88
+ fail(f"programbench eval error_code={report['error_code']}: {report.get('error_details')}")
89
+
90
+ results = report.get("test_results", [])
91
+
92
+ # Apply the tests.json ignore mask (the same logic `programbench info`
93
+ # applies). The mask ships in the package data; load it if present.
94
+ ignored: set[str] = set()
95
+ try:
96
+ import programbench # noqa: F401
97
+ from importlib import resources
98
+
99
+ data = resources.files("programbench").joinpath("data/tests.json")
100
+ if data.is_file():
101
+ mask = json.loads(data.read_text())
102
+ entry = mask.get(args.instance, {})
103
+ ignored = set(entry.get("ignored_tests", []))
104
+ except Exception: # noqa: BLE001
105
+ ignored = set()
106
+
107
+ scored = [r for r in results if r.get("name") not in ignored]
108
+ passed = sum(1 for r in scored if r.get("status") == "passed")
109
+ total = len(scored)
110
+ resolved = total > 0 and passed == total
111
+
112
+ print(json.dumps({"passed": passed, "total": total, "resolved": resolved}))
113
+ finally:
114
+ import shutil
115
+
116
+ shutil.rmtree(run_dir, ignore_errors=True)
117
+
118
+
119
+ if __name__ == "__main__":
120
+ main()
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Render the gate results as clean, dependency-free SVG bar charts.
3
+ *
4
+ * One honest chart per benchmark: bars for each arm (baseline greys + the
5
+ * verifier-grounded method in the accent), an optional dashed oracle-ceiling line,
6
+ * and a single annotated delta. No chartjunk — value labels, one accent, lots of
7
+ * whitespace. Data is passed in (no hidden numbers) so the charts regenerate from
8
+ * the real run outputs.
9
+ *
10
+ * node bench/scripts/render-gate-chart.mjs # writes docs/assets/*.svg
11
+ * (then: cairosvg docs/assets/<f>.svg -o /tmp/<f>.png to preview)
12
+ */
13
+
14
+ import { mkdirSync, writeFileSync } from 'node:fs'
15
+ import { dirname, join } from 'node:path'
16
+ import { fileURLToPath } from 'node:url'
17
+
18
+ const here = dirname(fileURLToPath(import.meta.url))
19
+ const assetsDir = join(here, '..', '..', 'docs', 'assets')
20
+
21
+ const palette = {
22
+ bg: '#ffffff',
23
+ ink: '#0f172a', // slate-900 — titles, values
24
+ muted: '#64748b', // slate-500 — subtitles, labels
25
+ faint: '#e2e8f0', // slate-200 — baseline / axis
26
+ baseline: '#cbd5e1', // slate-300 — "blind" / weaker baselines
27
+ baseline2: '#94a3b8', // slate-400 — the competing selector
28
+ method: '#4f46e5', // indigo-600 — the verifier-grounded method (the hero)
29
+ methodSoft: '#eef2ff', // indigo-50 — method bar wash
30
+ ceiling: '#475569', // slate-600 — oracle ceiling line
31
+ good: '#059669', // emerald-600 — positive delta
32
+ }
33
+
34
+ const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
35
+ const f = 'system-ui, -apple-system, "Segoe UI", Inter, Roboto, sans-serif'
36
+
37
+ /**
38
+ * @param {{
39
+ * title: string, subtitle: string, footnote?: string,
40
+ * bars: {label: string, value: number, kind: 'blind'|'rival'|'method'}[],
41
+ * ceiling?: {value: number, label: string},
42
+ * delta?: {fromIdx: number, toIdx: number, label: string},
43
+ * yMax?: number, unit?: string,
44
+ * }} cfg
45
+ */
46
+ function chart(cfg) {
47
+ const W = 820
48
+ const H = 524
49
+ const m = { top: 152, right: 136, bottom: 120, left: 66 }
50
+ const plotW = W - m.left - m.right
51
+ const plotH = H - m.top - m.bottom
52
+ const yMax = cfg.yMax ?? 100
53
+ const unit = cfg.unit ?? '%'
54
+ const y = (v) => m.top + plotH * (1 - v / yMax)
55
+
56
+ const n = cfg.bars.length
57
+ const bandW = plotW / n
58
+ const barW = Math.min(112, bandW * 0.56)
59
+ const cx = (i) => m.left + bandW * (i + 0.5)
60
+ const fill = { blind: palette.baseline, rival: palette.baseline2, method: palette.method }
61
+
62
+ const parts = []
63
+ parts.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" font-family='${f}'>`)
64
+ parts.push(`<rect width="${W}" height="${H}" fill="${palette.bg}"/>`)
65
+
66
+ // title + subtitle
67
+ parts.push(`<text x="${m.left}" y="46" font-size="22" font-weight="700" fill="${palette.ink}">${esc(cfg.title)}</text>`)
68
+ parts.push(`<text x="${m.left}" y="72" font-size="13.5" fill="${palette.muted}">${esc(cfg.subtitle)}</text>`)
69
+
70
+ // baseline axis
71
+ parts.push(`<line x1="${m.left}" y1="${y(0)}" x2="${m.left + plotW}" y2="${y(0)}" stroke="${palette.faint}" stroke-width="1.5"/>`)
72
+ // light gridlines at 25/50/75/100
73
+ for (const g of [25, 50, 75, 100]) {
74
+ if (g > yMax) continue
75
+ parts.push(`<line x1="${m.left}" y1="${y(g)}" x2="${m.left + plotW}" y2="${y(g)}" stroke="${palette.faint}" stroke-width="1" stroke-dasharray="2 5"/>`)
76
+ parts.push(`<text x="${m.left - 12}" y="${y(g) + 4}" font-size="11" text-anchor="end" fill="${palette.muted}">${g}${unit}</text>`)
77
+ }
78
+
79
+ // oracle ceiling line
80
+ if (cfg.ceiling) {
81
+ const yc = y(cfg.ceiling.value)
82
+ parts.push(`<line x1="${m.left}" y1="${yc}" x2="${m.left + plotW}" y2="${yc}" stroke="${palette.ceiling}" stroke-width="1.5" stroke-dasharray="6 4"/>`)
83
+ parts.push(`<text x="${m.left + plotW + 8}" y="${yc - 5}" font-size="12" font-weight="600" fill="${palette.ceiling}">${esc(cfg.ceiling.label)}</text>`)
84
+ parts.push(`<text x="${m.left + plotW + 8}" y="${yc + 12}" font-size="12" fill="${palette.ceiling}">${cfg.ceiling.value}${unit}</text>`)
85
+ }
86
+
87
+ // bars
88
+ cfg.bars.forEach((b, i) => {
89
+ const x = cx(i) - barW / 2
90
+ const top = y(b.value)
91
+ const h = y(0) - top
92
+ const isMethod = b.kind === 'method'
93
+ if (isMethod) parts.push(`<rect x="${x - 3}" y="${top - 3}" width="${barW + 6}" height="${h + 3}" rx="7" fill="${palette.methodSoft}"/>`)
94
+ parts.push(`<rect x="${x}" y="${top}" width="${barW}" height="${h}" rx="5" fill="${fill[b.kind]}"/>`)
95
+ // value label
96
+ parts.push(`<text x="${cx(i)}" y="${top - 12}" font-size="16" font-weight="700" text-anchor="middle" fill="${isMethod ? palette.method : palette.ink}">${b.value}${unit}</text>`)
97
+ // x label (two lines if it contains a newline)
98
+ const lines = b.label.split('\n')
99
+ lines.forEach((ln, li) => {
100
+ parts.push(`<text x="${cx(i)}" y="${y(0) + 24 + li * 16}" font-size="12.5" font-weight="${isMethod ? 600 : 400}" text-anchor="middle" fill="${isMethod ? palette.method : palette.muted}">${esc(ln)}</text>`)
101
+ })
102
+ })
103
+
104
+ // delta annotation: a bracket from one bar's top to another's, labelled
105
+ if (cfg.delta) {
106
+ const a = cfg.delta.fromIdx
107
+ const b = cfg.delta.toIdx
108
+ const xa = cx(a)
109
+ const xb = cx(b)
110
+ const ya = y(cfg.bars[a].value)
111
+ const yb = y(cfg.bars[b].value)
112
+ const yBar = Math.min(ya, yb) - 34
113
+ const midx = (xa + xb) / 2
114
+ parts.push(`<line x1="${xa}" y1="${ya - 26}" x2="${xa}" y2="${yBar}" stroke="${palette.good}" stroke-width="1.25"/>`)
115
+ parts.push(`<line x1="${xb}" y1="${yb - 26}" x2="${xb}" y2="${yBar}" stroke="${palette.good}" stroke-width="1.25"/>`)
116
+ parts.push(`<line x1="${xa}" y1="${yBar}" x2="${xb}" y2="${yBar}" stroke="${palette.good}" stroke-width="1.25"/>`)
117
+ const lw = cfg.delta.label.length * 7.2 + 16
118
+ parts.push(`<rect x="${midx - lw / 2}" y="${yBar - 22}" width="${lw}" height="20" rx="10" fill="${palette.good}"/>`)
119
+ parts.push(`<text x="${midx}" y="${yBar - 8}" font-size="12.5" font-weight="700" text-anchor="middle" fill="#ffffff">${esc(cfg.delta.label)}</text>`)
120
+ }
121
+
122
+ if (cfg.footnote) {
123
+ // wrap to the plot width (~92 chars at 11.5px over the full canvas)
124
+ const words = cfg.footnote.split(' ')
125
+ const lines = []
126
+ let cur = ''
127
+ for (const w of words) {
128
+ if ((cur + ' ' + w).trim().length > 104) {
129
+ lines.push(cur.trim())
130
+ cur = w
131
+ } else cur = `${cur} ${w}`
132
+ }
133
+ if (cur.trim()) lines.push(cur.trim())
134
+ const startY = H - 20 - (lines.length - 1) * 15
135
+ lines.forEach((ln, i) => {
136
+ parts.push(`<text x="${m.left}" y="${startY + i * 15}" font-size="11.5" fill="${palette.muted}">${esc(ln)}</text>`)
137
+ })
138
+ }
139
+ parts.push('</svg>')
140
+ return parts.join('\n')
141
+ }
142
+
143
+ // ---- the charts (numbers are the real, verified run outputs) -------------------
144
+
145
+ const humaneval = chart({
146
+ title: 'Verifier-grounded selection recovers the oracle ceiling',
147
+ subtitle: 'HumanEval · gpt-3.5-turbo · k=4 · n=50 · paired bootstrap (B=10000) + Benjamini–Hochberg',
148
+ bars: [
149
+ { label: 'Blind\n(1 shot)', value: 80, kind: 'blind' },
150
+ { label: 'Self-consistency\n(pick the consensus)', value: 82, kind: 'rival' },
151
+ { label: 'Verifier-grounded\n(re-run the tests)', value: 92, kind: 'method' },
152
+ ],
153
+ ceiling: { value: 92, label: 'oracle ceiling', },
154
+ delta: { fromIdx: 1, toIdx: 2, label: '+10.0pp CI[+2, +18]' },
155
+ footnote: 'Deployable, non-oracle selector: ranks k attempts by re-running the task’s own unit tests (gold never shown). It captures the FULL ceiling (gap 0); self-consistency leaves 10pp on the table. Reproduced across two independent n=50 runs (+10pp / +12pp, both BH-positive).',
156
+ })
157
+
158
+ // commit0 — single-task Layer-1 VALIDATION (wcwidth, k=3). Regenerate with the powered
159
+ // n=10 cross-task gate when it lands. Honestly labelled as a per-task validation.
160
+ const commit0Validation = chart({
161
+ title: 'The selector signal survives real stateful coding rollouts',
162
+ subtitle: 'commit0 · gpt-4.1 · k=3 · single-task validation (wcwidth) — official pytest harness, graded pass-rate',
163
+ bars: [
164
+ { label: 'Random pick\n(blind, mean-of-k)', value: 67.5, kind: 'blind' },
165
+ { label: 'Verifier-grounded\n(highest test-pass)', value: 78.9, kind: 'method' },
166
+ ],
167
+ ceiling: { value: 78.9, label: 'best-of-k', },
168
+ delta: { fromIdx: 0, toIdx: 1, label: '+11.4pp' },
169
+ footnote: 'Layer-1: agent clones the repo, implements, runs pytest, iterates, emits a diff. Attempts score 60.5 / 78.9 / 63.2% — within-task variance the selector exploits. Powered n=10 cross-task gate in progress.',
170
+ })
171
+
172
+ mkdirSync(assetsDir, { recursive: true })
173
+ writeFileSync(join(assetsDir, 'gate-humaneval.svg'), humaneval)
174
+ writeFileSync(join(assetsDir, 'gate-commit0-validation.svg'), commit0Validation)
175
+ console.log(`wrote ${join(assetsDir, 'gate-humaneval.svg')}`)
176
+ console.log(`wrote ${join(assetsDir, 'gate-commit0-validation.svg')}`)
@@ -0,0 +1,56 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { access, readdir } from 'node:fs/promises'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { promisify } from 'node:util'
6
+
7
+ const execFileAsync = promisify(execFile)
8
+ const benchDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
9
+ const sourceDir = path.join(benchDir, 'src')
10
+
11
+ async function collectTests(dir) {
12
+ const files = []
13
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
14
+ const absolute = path.join(dir, entry.name)
15
+ if (entry.isDirectory()) files.push(...(await collectTests(absolute)))
16
+ else if (entry.isFile() && /\.test\.(?:mts|ts)$/.test(entry.name)) files.push(absolute)
17
+ }
18
+ return files.sort()
19
+ }
20
+
21
+ async function run(command, args, env = process.env) {
22
+ try {
23
+ await execFileAsync(command, args, {
24
+ cwd: benchDir,
25
+ env,
26
+ maxBuffer: 10 * 1024 * 1024,
27
+ timeout: 120_000,
28
+ })
29
+ } catch (error) {
30
+ if (error?.stdout) process.stdout.write(error.stdout)
31
+ if (error?.stderr) process.stderr.write(error.stderr)
32
+ const invocation = [command, ...args].join(' ')
33
+ const message = error instanceof Error ? error.message : String(error)
34
+ throw new Error(`${invocation} failed: ${message}`, { cause: error })
35
+ }
36
+ }
37
+
38
+ const python = path.join(benchDir, '.venv', 'bin', 'python')
39
+ try {
40
+ await access(python)
41
+ } catch {
42
+ await run('python3', ['-m', 'venv', '.venv'])
43
+ }
44
+
45
+ const tests = await collectTests(sourceDir)
46
+ const relativeTests = tests.map((file) => path.relative(benchDir, file))
47
+ if (relativeTests.length === 0) throw new Error('no package tests found under src/')
48
+
49
+ await run(process.execPath, ['--test', '--import', 'tsx', ...relativeTests], {
50
+ ...process.env,
51
+ TSX_TSCONFIG_PATH: 'tsconfig.public.json',
52
+ })
53
+
54
+ await run(python, ['-m', 'unittest', 'discover', '-s', 'pier_agents', '-p', '*_test.py'])
55
+
56
+ console.log(`package tests passed: ${tests.length}/${tests.length} TypeScript files + Pier bridge`)