@tangle-network/agent-bench 0.1.0 → 0.3.6

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 (317) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/HARNESS.md +302 -0
  3. package/README.md +39 -2
  4. package/dist/adapters.d.ts +15 -0
  5. package/dist/adapters.js +43 -0
  6. package/dist/adapters.js.map +1 -0
  7. package/dist/benchmarks/_harness.d.ts +125 -0
  8. package/dist/benchmarks/_harness.js +33 -0
  9. package/dist/benchmarks/_harness.js.map +1 -0
  10. package/dist/benchmarks/aec-bench.d.ts +27 -0
  11. package/dist/benchmarks/aec-bench.js +8 -0
  12. package/dist/benchmarks/aec-bench.js.map +1 -0
  13. package/dist/benchmarks/agentbench.d.ts +16 -0
  14. package/dist/benchmarks/agentbench.js +10 -0
  15. package/dist/benchmarks/agentbench.js.map +1 -0
  16. package/dist/benchmarks/appworld.d.ts +37 -0
  17. package/dist/benchmarks/appworld.js +14 -0
  18. package/dist/benchmarks/appworld.js.map +1 -0
  19. package/dist/benchmarks/bfcl.d.ts +18 -0
  20. package/dist/benchmarks/bfcl.js +10 -0
  21. package/dist/benchmarks/bfcl.js.map +1 -0
  22. package/dist/benchmarks/cad-design.d.ts +45 -0
  23. package/dist/benchmarks/cad-design.js +7 -0
  24. package/dist/benchmarks/cad-design.js.map +1 -0
  25. package/dist/benchmarks/cadbench.d.ts +19 -0
  26. package/dist/benchmarks/cadbench.js +8 -0
  27. package/dist/benchmarks/cadbench.js.map +1 -0
  28. package/dist/benchmarks/cadgenbench.d.ts +22 -0
  29. package/dist/benchmarks/cadgenbench.js +8 -0
  30. package/dist/benchmarks/cadgenbench.js.map +1 -0
  31. package/dist/benchmarks/commit0.d.ts +31 -0
  32. package/dist/benchmarks/commit0.js +10 -0
  33. package/dist/benchmarks/commit0.js.map +1 -0
  34. package/dist/benchmarks/crag.d.ts +14 -0
  35. package/dist/benchmarks/crag.js +9 -0
  36. package/dist/benchmarks/crag.js.map +1 -0
  37. package/dist/benchmarks/dabstep.d.ts +18 -0
  38. package/dist/benchmarks/dabstep.js +10 -0
  39. package/dist/benchmarks/dabstep.js.map +1 -0
  40. package/dist/benchmarks/enterpriseops-gym.d.ts +38 -0
  41. package/dist/benchmarks/enterpriseops-gym.js +10 -0
  42. package/dist/benchmarks/enterpriseops-gym.js.map +1 -0
  43. package/dist/benchmarks/finresearchbench.d.ts +15 -0
  44. package/dist/benchmarks/finresearchbench.js +8 -0
  45. package/dist/benchmarks/finresearchbench.js.map +1 -0
  46. package/dist/benchmarks/finsearchcomp.d.ts +49 -0
  47. package/dist/benchmarks/finsearchcomp.js +7 -0
  48. package/dist/benchmarks/finsearchcomp.js.map +1 -0
  49. package/dist/benchmarks/frames.d.ts +59 -0
  50. package/dist/benchmarks/frames.js +13 -0
  51. package/dist/benchmarks/frames.js.map +1 -0
  52. package/dist/benchmarks/hotpotqa.d.ts +48 -0
  53. package/dist/benchmarks/hotpotqa.js +15 -0
  54. package/dist/benchmarks/hotpotqa.js.map +1 -0
  55. package/dist/benchmarks/humaneval.d.ts +53 -0
  56. package/dist/benchmarks/humaneval.js +15 -0
  57. package/dist/benchmarks/humaneval.js.map +1 -0
  58. package/dist/benchmarks/mind2web.d.ts +41 -0
  59. package/dist/benchmarks/mind2web.js +9 -0
  60. package/dist/benchmarks/mind2web.js.map +1 -0
  61. package/dist/benchmarks/nomiracl.d.ts +15 -0
  62. package/dist/benchmarks/nomiracl.js +9 -0
  63. package/dist/benchmarks/nomiracl.js.map +1 -0
  64. package/dist/benchmarks/open-rag-bench.d.ts +14 -0
  65. package/dist/benchmarks/open-rag-bench.js +9 -0
  66. package/dist/benchmarks/open-rag-bench.js.map +1 -0
  67. package/dist/benchmarks/programbench.d.ts +38 -0
  68. package/dist/benchmarks/programbench.js +10 -0
  69. package/dist/benchmarks/programbench.js.map +1 -0
  70. package/dist/benchmarks/rag-shared.d.ts +42 -0
  71. package/dist/benchmarks/rag-shared.js +39 -0
  72. package/dist/benchmarks/rag-shared.js.map +1 -0
  73. package/dist/benchmarks/ragbench.d.ts +16 -0
  74. package/dist/benchmarks/ragbench.js +9 -0
  75. package/dist/benchmarks/ragbench.js.map +1 -0
  76. package/dist/benchmarks/simpleqa.d.ts +64 -0
  77. package/dist/benchmarks/simpleqa.js +11 -0
  78. package/dist/benchmarks/simpleqa.js.map +1 -0
  79. package/dist/benchmarks/swe-bench.d.ts +56 -0
  80. package/dist/benchmarks/swe-bench.js +14 -0
  81. package/dist/benchmarks/swe-bench.js.map +1 -0
  82. package/dist/benchmarks/t2-ragbench.d.ts +14 -0
  83. package/dist/benchmarks/t2-ragbench.js +9 -0
  84. package/dist/benchmarks/t2-ragbench.js.map +1 -0
  85. package/dist/benchmarks/tau-bench-shared.d.ts +26 -0
  86. package/dist/benchmarks/tau-bench-shared.js +10 -0
  87. package/dist/benchmarks/tau-bench-shared.js.map +1 -0
  88. package/dist/benchmarks/tau2-bench.d.ts +7 -0
  89. package/dist/benchmarks/tau2-bench.js +11 -0
  90. package/dist/benchmarks/tau2-bench.js.map +1 -0
  91. package/dist/benchmarks/tau3-banking.d.ts +15 -0
  92. package/dist/benchmarks/tau3-banking.js +9 -0
  93. package/dist/benchmarks/tau3-banking.js.map +1 -0
  94. package/dist/benchmarks/terminal-bench.d.ts +24 -0
  95. package/dist/benchmarks/terminal-bench.js +8 -0
  96. package/dist/benchmarks/terminal-bench.js.map +1 -0
  97. package/dist/benchmarks/toollm.d.ts +16 -0
  98. package/dist/benchmarks/toollm.js +10 -0
  99. package/dist/benchmarks/toollm.js.map +1 -0
  100. package/dist/benchmarks/trata-hedge.d.ts +32 -0
  101. package/dist/benchmarks/trata-hedge.js +7 -0
  102. package/dist/benchmarks/trata-hedge.js.map +1 -0
  103. package/dist/benchmarks/types.d.ts +107 -0
  104. package/dist/benchmarks/types.js +1 -0
  105. package/dist/benchmarks/types.js.map +1 -0
  106. package/dist/benchmarks/webarena-verified.d.ts +16 -0
  107. package/dist/benchmarks/webarena-verified.js +10 -0
  108. package/dist/benchmarks/webarena-verified.js.map +1 -0
  109. package/dist/chunk-2PVVP7GN.js +197 -0
  110. package/dist/chunk-2PVVP7GN.js.map +1 -0
  111. package/dist/chunk-2XU6OGEN.js +170 -0
  112. package/dist/chunk-2XU6OGEN.js.map +1 -0
  113. package/dist/chunk-53UPUNBZ.js +325 -0
  114. package/dist/chunk-53UPUNBZ.js.map +1 -0
  115. package/dist/chunk-5SBJCB6W.js +144 -0
  116. package/dist/chunk-5SBJCB6W.js.map +1 -0
  117. package/dist/chunk-7WSD27QQ.js +118 -0
  118. package/dist/chunk-7WSD27QQ.js.map +1 -0
  119. package/dist/chunk-C7T7WEK2.js +103 -0
  120. package/dist/chunk-C7T7WEK2.js.map +1 -0
  121. package/dist/chunk-CKUVRZ2T.js +251 -0
  122. package/dist/chunk-CKUVRZ2T.js.map +1 -0
  123. package/dist/chunk-HBSWHQNJ.js +30 -0
  124. package/dist/chunk-HBSWHQNJ.js.map +1 -0
  125. package/dist/chunk-HHXFIHXC.js +116 -0
  126. package/dist/chunk-HHXFIHXC.js.map +1 -0
  127. package/dist/chunk-IFAV6KEM.js +276 -0
  128. package/dist/chunk-IFAV6KEM.js.map +1 -0
  129. package/dist/chunk-INNOYXCP.js +387 -0
  130. package/dist/chunk-INNOYXCP.js.map +1 -0
  131. package/dist/chunk-J3KDJNX2.js +182 -0
  132. package/dist/chunk-J3KDJNX2.js.map +1 -0
  133. package/dist/chunk-JRWWGMK7.js +148 -0
  134. package/dist/chunk-JRWWGMK7.js.map +1 -0
  135. package/dist/chunk-JTHWEDEW.js +32 -0
  136. package/dist/chunk-JTHWEDEW.js.map +1 -0
  137. package/dist/chunk-KDIKRJGB.js +120 -0
  138. package/dist/chunk-KDIKRJGB.js.map +1 -0
  139. package/dist/chunk-LRRD7NAG.js +301 -0
  140. package/dist/chunk-LRRD7NAG.js.map +1 -0
  141. package/dist/chunk-ODT47UAY.js +221 -0
  142. package/dist/chunk-ODT47UAY.js.map +1 -0
  143. package/dist/chunk-PA2ZKHJC.js +230 -0
  144. package/dist/chunk-PA2ZKHJC.js.map +1 -0
  145. package/dist/chunk-PPYSEKFM.js +182 -0
  146. package/dist/chunk-PPYSEKFM.js.map +1 -0
  147. package/dist/chunk-PUIRNYI7.js +189 -0
  148. package/dist/chunk-PUIRNYI7.js.map +1 -0
  149. package/dist/chunk-R36V2VP7.js +169 -0
  150. package/dist/chunk-R36V2VP7.js.map +1 -0
  151. package/dist/chunk-R67DFVLO.js +142 -0
  152. package/dist/chunk-R67DFVLO.js.map +1 -0
  153. package/dist/chunk-SEVJPLZC.js +260 -0
  154. package/dist/chunk-SEVJPLZC.js.map +1 -0
  155. package/dist/chunk-SYDW647C.js +318 -0
  156. package/dist/chunk-SYDW647C.js.map +1 -0
  157. package/dist/chunk-TBKU5XQI.js +228 -0
  158. package/dist/chunk-TBKU5XQI.js.map +1 -0
  159. package/dist/chunk-TSWPNOYM.js +147 -0
  160. package/dist/chunk-TSWPNOYM.js.map +1 -0
  161. package/dist/chunk-UAIOHCUK.js +27 -0
  162. package/dist/chunk-UAIOHCUK.js.map +1 -0
  163. package/dist/chunk-UPAMRDX4.js +233 -0
  164. package/dist/chunk-UPAMRDX4.js.map +1 -0
  165. package/dist/chunk-VQRS7VUC.js +342 -0
  166. package/dist/chunk-VQRS7VUC.js.map +1 -0
  167. package/dist/chunk-X3BTXCJ4.js +262 -0
  168. package/dist/chunk-X3BTXCJ4.js.map +1 -0
  169. package/dist/chunk-X5YKXC6V.js +211 -0
  170. package/dist/chunk-X5YKXC6V.js.map +1 -0
  171. package/dist/chunk-Y6O2OCUO.js +130 -0
  172. package/dist/chunk-Y6O2OCUO.js.map +1 -0
  173. package/dist/chunk-YCGY7UIZ.js +208 -0
  174. package/dist/chunk-YCGY7UIZ.js.map +1 -0
  175. package/dist/chunk-Z7ML6L77.js +162 -0
  176. package/dist/chunk-Z7ML6L77.js.map +1 -0
  177. package/dist/chunk-ZEWMTR5M.js +136 -0
  178. package/dist/chunk-ZEWMTR5M.js.map +1 -0
  179. package/dist/index.d.ts +355 -0
  180. package/dist/index.js +1908 -0
  181. package/dist/index.js.map +1 -0
  182. package/fixtures/aec-bench.json +18 -0
  183. package/fixtures/agentbench-dbbench.json +22 -0
  184. package/fixtures/bfcl.json +45 -0
  185. package/fixtures/commit0.json +72 -0
  186. package/fixtures/crag.json +10 -0
  187. package/fixtures/dabstep.json +22 -0
  188. package/fixtures/enterpriseops-gym.json +103 -0
  189. package/fixtures/finresearchbench.json +21 -0
  190. package/fixtures/finsearchcomp.json +66 -0
  191. package/fixtures/frames.json +26 -0
  192. package/fixtures/hotpotqa.json +182 -0
  193. package/fixtures/nomiracl.json +26 -0
  194. package/fixtures/open-rag-bench.json +16 -0
  195. package/fixtures/pier-agent/no-model-task/environment/Dockerfile +16 -0
  196. package/fixtures/pier-agent/no-model-task/environment/seed/src/status.txt +1 -0
  197. package/fixtures/pier-agent/no-model-task/instruction.md +6 -0
  198. package/fixtures/pier-agent/no-model-task/pre_artifacts.sh +6 -0
  199. package/fixtures/pier-agent/no-model-task/task.toml +35 -0
  200. package/fixtures/pier-agent/no-model-task/tests/Dockerfile +17 -0
  201. package/fixtures/pier-agent/no-model-task/tests/seed/src/status.txt +1 -0
  202. package/fixtures/pier-agent/no-model-task/tests/test.sh +19 -0
  203. package/fixtures/programbench.json +17 -0
  204. package/fixtures/ragbench.json +21 -0
  205. package/fixtures/simpleqa.json +121 -0
  206. package/fixtures/t2-ragbench.json +13 -0
  207. package/fixtures/tau2-bench.json +16 -0
  208. package/fixtures/tau3-banking.json +16 -0
  209. package/fixtures/toollm.json +28 -0
  210. package/fixtures/webarena-verified.json +20 -0
  211. package/package.json +60 -20
  212. package/pier_agents/__init__.py +18 -0
  213. package/pier_agents/candidate_contract.py +755 -0
  214. package/pier_agents/process_boundary.py +321 -0
  215. package/pier_agents/tangle_candidate.py +907 -0
  216. package/pier_agents/workspace_boundary.py +368 -0
  217. package/scripts/appworld_driver.py +359 -0
  218. package/scripts/cadbench_prepare.py +22 -0
  219. package/scripts/cadgenbench_hard_parts.py +48 -0
  220. package/scripts/clbench_codebase_judge.py +73 -0
  221. package/scripts/commit0_judge.py +170 -0
  222. package/scripts/dabstep_judge.py +42 -0
  223. package/scripts/enterpriseops_gym_judge.py +281 -0
  224. package/scripts/programbench_judge.py +120 -0
  225. package/scripts/render-gate-chart.mjs +176 -0
  226. package/scripts/run-package-tests.mjs +56 -0
  227. package/scripts/terminate-pier-trial.mts +66 -0
  228. package/scripts/trata-hedge/README.md +56 -0
  229. package/scripts/trata-hedge/run.sh +60 -0
  230. package/scripts/trata-hedge/solve.py +83 -0
  231. package/scripts/verify-packed-consumer.mjs +235 -0
  232. package/scripts/verify-pier-agent.mts +715 -0
  233. package/scripts/verify-pier-pair.mts +74 -0
  234. package/scripts/verify-pier-recovery.mts +139 -0
  235. package/src/adapters.ts +26 -0
  236. package/src/benchmarks/_harness.test.mts +178 -0
  237. package/src/benchmarks/_harness.ts +239 -16
  238. package/src/benchmarks/agentbench.ts +163 -0
  239. package/src/benchmarks/appworld.test.mts +15 -9
  240. package/src/benchmarks/bfcl.ts +346 -0
  241. package/src/benchmarks/crag.ts +137 -0
  242. package/src/benchmarks/dabstep.test.mts +70 -0
  243. package/src/benchmarks/dabstep.ts +212 -0
  244. package/src/benchmarks/external-adapters.test.mts +150 -0
  245. package/src/benchmarks/finresearchbench.ts +269 -0
  246. package/src/benchmarks/humaneval.ts +20 -8
  247. package/src/benchmarks/nomiracl.ts +180 -0
  248. package/src/benchmarks/open-rag-bench.ts +153 -0
  249. package/src/benchmarks/rag-benchmarks.test.mts +138 -0
  250. package/src/benchmarks/rag-shared.ts +327 -0
  251. package/src/benchmarks/ragbench.ts +171 -0
  252. package/src/benchmarks/swe-bench.test.mts +61 -0
  253. package/src/benchmarks/swe-bench.ts +201 -19
  254. package/src/benchmarks/t2-ragbench.ts +166 -0
  255. package/src/benchmarks/tau-bench-shared.ts +214 -0
  256. package/src/benchmarks/tau2-bench.ts +30 -0
  257. package/src/benchmarks/tau3-banking.ts +29 -0
  258. package/src/benchmarks/terminal-bench.test.mts +33 -0
  259. package/src/benchmarks/terminal-bench.ts +23 -8
  260. package/src/benchmarks/toollm.ts +254 -0
  261. package/src/benchmarks/types.ts +42 -0
  262. package/src/benchmarks/webarena-verified.ts +200 -0
  263. package/src/commit0-prereqs.sh +0 -0
  264. package/src/coordination-mcp-container-reach.mts +181 -0
  265. package/src/decoder-live.mts +1 -1
  266. package/src/examples/README.md +103 -39
  267. package/src/examples/benchmark-matrix.mts +101 -0
  268. package/src/examples/lean-proof-gate.README.md +77 -0
  269. package/src/examples/lean-proof-gate.mts +162 -0
  270. package/src/examples/lean-verify.ts +95 -0
  271. package/src/examples/lean.Dockerfile +12 -0
  272. package/src/examples/math-demo.mts +9 -7
  273. package/src/examples/strategy-demo.mts +10 -12
  274. package/src/gate.ts +3 -2
  275. package/src/hev-eval.mts +69 -0
  276. package/src/hev-improve.mts +169 -0
  277. package/src/hev-structural.mts +688 -0
  278. package/src/index.ts +73 -0
  279. package/src/mbpp-structural.mts +662 -0
  280. package/src/pier-agent.test-fixtures.mts +19 -0
  281. package/src/pier-agent.test.mts +363 -0
  282. package/src/pier-agent.ts +657 -0
  283. package/src/pier-result-grader.mjs +30 -0
  284. package/src/pier-result-grader.test.mts +62 -0
  285. package/src/pier-result-grader.ts +108 -0
  286. package/src/pier-task-outcome.test.mts +117 -0
  287. package/src/pier-task-outcome.ts +240 -0
  288. package/src/pier-trial-controller.test.mts +412 -0
  289. package/src/pier-trial-controller.ts +858 -0
  290. package/src/pier-trial-supervisor.mjs +352 -0
  291. package/src/resolve-client.ts +25 -2
  292. package/src/run-benchmarks-cli.mts +72 -0
  293. package/src/run-benchmarks-report.ts +66 -0
  294. package/src/run-benchmarks.test.mts +231 -0
  295. package/src/run-benchmarks.ts +589 -0
  296. package/src/smoke-structural-rollout.mts +393 -0
  297. package/src/swe-bench-env.test.ts +207 -0
  298. package/src/swe-bench-env.ts +554 -0
  299. package/src/swe-jail.ts +293 -0
  300. package/src/swe-self-improve.mts +84 -0
  301. package/src/swe-structural-judge-policy.test.ts +117 -0
  302. package/src/swe-structural-judge-policy.ts +133 -0
  303. package/src/swe-structural-policy.test.ts +124 -0
  304. package/src/swe-structural-policy.ts +132 -0
  305. package/src/swe-structural-provenance.test.ts +93 -0
  306. package/src/swe-structural-provenance.ts +138 -0
  307. package/src/swe-structural.mts +1260 -0
  308. package/src/swe-temp.ts +14 -0
  309. package/src/tb-container-executor.mts +234 -0
  310. package/src/tb-container-executor.test.mts +99 -0
  311. package/src/tb-supervisor-sidecar.mts +222 -0
  312. package/src/trata-gepa.mts +1 -1
  313. package/steerers/eops-itsm-population.json +1 -0
  314. package/tb_agents/opencode_refine_agent.py +117 -0
  315. package/tb_agents/opencode_router_agent.py +406 -0
  316. package/tb_agents/opencode_supervisor_agent.py +239 -0
  317. package/tb_agents/script_agent.py +66 -0
@@ -0,0 +1,554 @@
1
+ /**
2
+ * SWE-bench Verified as an `AgenticSurface` — the PROPER, no-cheating way to run a coding agent on real
3
+ * GitHub bugs through the substrate (`runAgentic`/`runBenchmark`/`runStrategyEvolution` drive the loop;
4
+ * we only provide tools + a deployable score). The agent clones the repo at base_commit, explores +
5
+ * edits SOURCE via tools (never tests — path-jailed), and `score()` grades the resulting `git diff`
6
+ * with the OFFICIAL swebench Docker harness (apply patch → FAIL_TO_PASS + PASS_TO_PASS → resolved).
7
+ *
8
+ * No cheating by construction: the agent never sees the hidden tests or the gold patch (the adapter's
9
+ * prompt is the issue only); `edit_file` refuses test files; the score is a real test run, not a judge.
10
+ *
11
+ * CONTAMINATION CAVEAT: SWE-bench bugs are public GitHub fixes a frontier model may have MEMORIZED.
12
+ * A clean train→holdout split (disjoint instances) rules out adaptive-reuse, but NOT training-data
13
+ * memorization. Always report this; never claim a "clean" frontier number from this arena alone.
14
+ */
15
+ import { execFile } from 'node:child_process'
16
+ import { cpSync, existsSync, lstatSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
17
+ import { join, sep } from 'node:path'
18
+ import { promisify } from 'node:util'
19
+ import type { AgenticSurface, AgenticTask, AgenticTool, ArtifactHandle, SurfaceScore } from '@tangle-network/agent-runtime/loops'
20
+ import { runVenvPython } from './benchmarks/_harness'
21
+ import { createSweBenchAdapter, type SweBenchAdapterOptions } from './benchmarks/swe-bench'
22
+ import type { BenchTask } from './benchmarks/types'
23
+ import { absoluteSweTempDir } from './swe-temp'
24
+
25
+ const exec = promisify(execFile)
26
+ export const isTestPath = (p: string) => /(^|\/)(tests?)\//.test(p) || /test_.*\.py$|_test\.py$|conftest\.py$/.test(p)
27
+
28
+ /** Copy a cached git checkout without rewriting repository-relative symlinks, then prove that the
29
+ * copy is byte-for-byte clean from git's perspective before a worker can observe it. */
30
+ export async function copyPristineGitCheckout(sourceDir: string, destinationDir: string): Promise<void> {
31
+ cpSync(sourceDir, destinationDir, { recursive: true, verbatimSymlinks: true })
32
+ let status: string
33
+ try {
34
+ const result = await exec('git', ['-C', destinationDir, 'status', '--porcelain'], {
35
+ timeout: 60_000,
36
+ maxBuffer: 20_000_000,
37
+ })
38
+ status = result.stdout
39
+ } catch (error) {
40
+ throw new Error(`could not verify cached checkout copy: ${(error as Error).message}`)
41
+ }
42
+ if (status.length > 0) {
43
+ throw new Error(`cached checkout copy is not pristine:\n${status.slice(0, 4_000)}`)
44
+ }
45
+ }
46
+
47
+ /**
48
+ * The read/edit-only SWE agent system prompt — the ESTABLISHED baseline surface (glm-5.2 raw = 7/12,
49
+ * glm-4.6 = 3/12). Exported as the single source of truth so `tasks()` here and the improve() seed in
50
+ * swe-improve.mts stay byte-identical (the baseline denominator depends on this — no drift). Do NOT
51
+ * edit this constant to add run-tool guidance; that lives in SWE_SEED_PROMPT_WITH_RUN so the read/edit
52
+ * baseline arm is reproducible unchanged.
53
+ */
54
+ export const SWE_SEED_PROMPT =
55
+ 'You are a senior engineer fixing a real bug in the checked-out repository. Work PERSISTENTLY and do not ' +
56
+ 'stop early: use list_files + read_file to explore BROADLY (read many candidate files — the bug is rarely in ' +
57
+ 'the first file you open), trace the issue to its root cause, then fix it with edit_file. You MUST make at ' +
58
+ 'least one edit_file call — never finish with prose alone or without attempting a fix. Make a MINIMAL surgical ' +
59
+ 'change (a few lines, like a real PR), source only (test files are rejected). If an edit_file fails (old_string ' +
60
+ 'not unique/found), read the file again and retry with exact text. Keep going until you have made your best fix.'
61
+
62
+ /**
63
+ * The WITH-TOOLS arm: the baseline prompt PLUS the run-tool workflow (write a failing repro, edit, re-run
64
+ * until it passes). Only used when the environment is built with `enableRun`. Kept as a separate constant
65
+ * so enabling the run tool never mutates the read/edit-only baseline. Also reconciles the now-active
66
+ * confusion from loadTasks' userPrompt (which says the repo is "at /work, cd /work" — the container path
67
+ * from the sandbox path; here the run tool's cwd is the repo root and there is nothing to cd into).
68
+ */
69
+ // NOTE (WAF): the router sits behind a Cloudflare WAF whose RCE ruleset 403s any request body that
70
+ // contains BACKTICK-wrapped command-like text (backticks are shell command-substitution syntax). The
71
+ // SAME commands without backticks pass. So this prompt and the run tool description deliberately write
72
+ // example commands WITHOUT backticks. Verified: backtick form -> 403, plain form -> 200.
73
+ export const SWE_SEED_PROMPT_WITH_RUN =
74
+ `${SWE_SEED_PROMPT} ` +
75
+ 'You ALSO have a run tool: execute a shell command in the repo checkout (cwd is already the repo root — do ' +
76
+ 'NOT cd, and ignore any instruction that says the repo is at /work). Use run to VERIFY your fix, not to ' +
77
+ 'stall. DISCIPLINE: read a few files to locate the bug, then MAKE YOUR EDIT with edit_file EARLY — do not ' +
78
+ 'spend many turns running commands before your first edit. After you edit, use run to check the fix: a ' +
79
+ 'one-line python -c inline check, or python -m pytest on the nearest existing test file (use -k to select ' +
80
+ 'the case, plus -rA and -p no:cacheprovider). If it still fails, read the output, refine the edit, and ' +
81
+ 're-run — iterate EDIT then run until it passes, and run the nearest existing tests to catch regressions. ' +
82
+ 'The network is DISABLED and the hidden grading tests are NOT present, so verify with local, network-free ' +
83
+ 'checks. You MUST make at least one edit_file — a turn budget spent running with no edit is a failure.'
84
+
85
+ /** Hard wall-clock cap (seconds) for a single `run` command, enforced BOTH in-container (`timeout`) and
86
+ * on the host (execFile timeout + SIGKILL). Env-overridable for slow suites; default fail-closed at 120s. */
87
+ const RUN_TIMEOUT_S = Number(process.env.SWE_RUN_TIMEOUT ?? 120)
88
+ /** Combined stdout+stderr budget returned to the model: head + tail so the failure summary (which pytest
89
+ * prints at the tail) survives truncation. */
90
+ const RUN_OUTPUT_LIMIT = Number(process.env.SWE_RUN_OUTPUT_LIMIT ?? 10_000)
91
+ /** Exact run-tool settings stamped into structural-experiment receipts. */
92
+ export const SWE_RUN_TOOL_CONFIG = Object.freeze({
93
+ timeoutS: RUN_TIMEOUT_S,
94
+ outputLimit: RUN_OUTPUT_LIMIT,
95
+ })
96
+ /** Monotonic suffix so concurrent `run` calls get distinct container names (for reap-on-timeout). */
97
+ let runNameCounter = 0
98
+
99
+ /** Truncate a long string keeping a head and a (larger) tail, with a marker between — the tail carries the
100
+ * test-failure summary, so it gets the bigger share. Pure. */
101
+ function truncateHeadTail(s: string, limit: number): string {
102
+ if (s.length <= limit) return s
103
+ const head = Math.floor(limit * 0.35)
104
+ const tail = limit - head
105
+ return `${s.slice(0, head)}\n...[truncated ${s.length - limit} chars]...\n${s.slice(s.length - tail)}`
106
+ }
107
+
108
+ /** Inline venv script: given the instance metadata row (argv[1] JSON), print the candidate Docker image
109
+ * tags swebench itself would use — the remote/namespaced tag (what a `swebench` pull caches, e.g.
110
+ * `swebench/sweb.eval.x86_64.psf_1776_requests-1142:latest`) FIRST, then the local-build tag
111
+ * (`sweb.eval.x86_64.psf__requests-1142:latest`). Delegated to `make_test_spec` so the naming stays
112
+ * correct across swebench versions instead of being hand-built. No network (it reads a dict, not the HF split). */
113
+ const IMAGE_KEY_SCRIPT = `
114
+ import json, sys
115
+ from swebench.harness.test_spec.test_spec import make_test_spec
116
+ row = json.loads(sys.argv[1])
117
+ candidates = []
118
+ for namespace in ("swebench", None):
119
+ try:
120
+ candidates.append({
121
+ "tag": make_test_spec(row, namespace=namespace).instance_image_key,
122
+ "namespace": "swebench" if namespace == "swebench" else "none",
123
+ })
124
+ except Exception:
125
+ pass
126
+ print(json.dumps(candidates))
127
+ `
128
+
129
+ const SWEBENCH_VERSION_SCRIPT = `
130
+ import importlib.metadata
131
+ print(importlib.metadata.version("swebench"))
132
+ `
133
+
134
+ /** Installed official scorer package stamped into experiment execution receipts. */
135
+ export async function resolveSweBenchScorerVersion(): Promise<string> {
136
+ const output = await runVenvPython(SWEBENCH_VERSION_SCRIPT, [], 60_000)
137
+ const version = output.trim().split('\n').filter(Boolean).pop() ?? ''
138
+ if (!version) throw new Error('could not resolve installed swebench scorer version')
139
+ return version
140
+ }
141
+
142
+ /**
143
+ * Cheap string pre-filter for an agent-supplied repo-relative path, applied before the path is
144
+ * joined to a workspace root: rejects absolute paths and any `..` segment, strips a leading `./`.
145
+ * Returns the cleaned relative path, or `null` if it must be refused. Pure and side-effect-free —
146
+ * `root` is unused here (the symlink-following boundary is the realpath jail, not this filter) but
147
+ * is taken so call sites read symmetrically with the realpath check.
148
+ */
149
+ export const jailPath = (_root: string, p: string): string | null => {
150
+ if (p.startsWith('/') || p.includes('..')) return null
151
+ return p.replace(/^\.\//, '')
152
+ }
153
+
154
+ /**
155
+ * Containment predicate for the realpath jail: true iff `real` (an already-resolved absolute path)
156
+ * is `jailRoot` itself or lies strictly inside it. The `+ sep` guard stops a sibling like
157
+ * `/tmp/swe-x-evil` from matching the root `/tmp/swe-x`. Pure and side-effect-free.
158
+ */
159
+ export const isInsideJail = (jailRoot: string, real: string): boolean => real === jailRoot || real.startsWith(jailRoot + sep)
160
+
161
+ export interface SweImageIdentity {
162
+ id: string
163
+ repoDigests: string[]
164
+ }
165
+
166
+ export interface SweImageCandidate {
167
+ tag: string
168
+ namespace: 'swebench' | 'none'
169
+ }
170
+
171
+ export type SweImageResolution =
172
+ | { ok: true; tag: string; namespace: SweImageCandidate['namespace']; identity: SweImageIdentity }
173
+ | { ok: false; reason: string }
174
+
175
+ export function parseSweImageCandidates(stdout: string): SweImageCandidate[] {
176
+ const parsed = JSON.parse(stdout) as Array<{ tag?: unknown; namespace?: unknown }>
177
+ return parsed.map((candidate, index) => {
178
+ if (
179
+ typeof candidate.tag !== 'string' ||
180
+ (candidate.namespace !== 'swebench' && candidate.namespace !== 'none')
181
+ ) {
182
+ throw new Error(`invalid SWE image candidate at index ${index}`)
183
+ }
184
+ return { tag: candidate.tag, namespace: candidate.namespace }
185
+ })
186
+ }
187
+
188
+ export function firstAvailableSweImageCandidate(
189
+ candidates: readonly SweImageCandidate[],
190
+ identities: ReadonlyMap<string, SweImageIdentity>,
191
+ ): { candidate: SweImageCandidate; identity: SweImageIdentity } | null {
192
+ for (const candidate of candidates) {
193
+ const identity = identities.get(candidate.tag)
194
+ if (identity) return { candidate, identity }
195
+ }
196
+ return null
197
+ }
198
+
199
+ /** Parse the immutable image identity returned by `docker image inspect`. */
200
+ export function parseSweImageIdentity(stdout: string): SweImageIdentity {
201
+ const rows = JSON.parse(stdout) as Array<{ Id?: unknown; RepoDigests?: unknown }>
202
+ const row = rows[0]
203
+ const id = typeof row?.Id === 'string' ? row.Id : ''
204
+ if (!id) throw new Error('docker image inspect returned no image ID')
205
+ const repoDigests = Array.isArray(row?.RepoDigests)
206
+ ? row.RepoDigests.filter((value): value is string => typeof value === 'string').sort()
207
+ : []
208
+ return { id, repoDigests }
209
+ }
210
+
211
+ interface Ws {
212
+ dir: string
213
+ task: BenchTask
214
+ /** Memoized `run`-tool image resolution: the local Docker tag to exec in, or a fail-closed reason. */
215
+ image?: SweImageResolution
216
+ }
217
+
218
+ /** Resolve the locally-present Docker image for an instance's METADATA row (no workspace needed).
219
+ * Asks swebench for the candidate tags, then picks the first that `docker image inspect` finds locally.
220
+ * Fail-closed: docker down / no cached image / resolver error → `{ ok:false }`. Exported so script-side
221
+ * calibrators (swe-repro-calibrate) hard-assert image presence through the SAME resolution the `run`
222
+ * tool uses instead of hand-building tags. */
223
+ export async function resolveImageForMetadata(
224
+ metadata: Record<string, unknown>,
225
+ ): Promise<SweImageResolution> {
226
+ let candidates: SweImageCandidate[]
227
+ try {
228
+ const out = await runVenvPython(IMAGE_KEY_SCRIPT, [JSON.stringify(metadata)], 60_000)
229
+ const lastLine = out.trim().split('\n').filter(Boolean).pop() ?? '[]'
230
+ candidates = parseSweImageCandidates(lastLine)
231
+ } catch (e) {
232
+ return { ok: false, reason: `image-key resolution failed: ${(e as Error).message.slice(0, 160)}` }
233
+ }
234
+ if (!candidates.length) return { ok: false, reason: 'swebench produced no image key for this instance' }
235
+ const identities = new Map<string, SweImageIdentity>()
236
+ for (const { tag } of candidates) {
237
+ try {
238
+ const inspected = await exec('docker', ['image', 'inspect', tag], { timeout: 20_000 })
239
+ identities.set(tag, parseSweImageIdentity(inspected.stdout))
240
+ } catch (e) {
241
+ const m = (e as { stderr?: string }).stderr ?? (e as Error).message ?? ''
242
+ if (/Cannot connect to the Docker daemon|Is the docker daemon running/i.test(m)) {
243
+ return { ok: false, reason: 'docker daemon unavailable' }
244
+ }
245
+ // otherwise: this tag is just not cached locally — try the next candidate
246
+ }
247
+ }
248
+ const selected = firstAvailableSweImageCandidate(candidates, identities)
249
+ if (selected) {
250
+ return {
251
+ ok: true,
252
+ tag: selected.candidate.tag,
253
+ namespace: selected.candidate.namespace,
254
+ identity: selected.identity,
255
+ }
256
+ }
257
+ return { ok: false, reason: `no cached image (tried: ${candidates.map(({ tag }) => tag).join(', ')})` }
258
+ }
259
+
260
+ /** Per-workspace memoization of `resolveImageForMetadata` for the `run` tool (degrades to an
261
+ * `ERROR:` string so the agent falls back to read/edit-only instead of crashing). */
262
+ async function resolveInstanceImage(ws: Ws, expected?: SweImageIdentity): Promise<SweImageResolution> {
263
+ if (ws.image) {
264
+ if (ws.image.ok && expected && ws.image.identity.id !== expected.id) {
265
+ return { ok: false, reason: `image identity changed (${expected.id} -> ${ws.image.identity.id})` }
266
+ }
267
+ return ws.image
268
+ }
269
+ const resolved = await resolveImageForMetadata(ws.task.metadata ?? {})
270
+ if (resolved.ok && expected && resolved.identity.id !== expected.id) {
271
+ return (ws.image = {
272
+ ok: false,
273
+ reason: `image identity changed (${expected.id} -> ${resolved.identity.id})`,
274
+ })
275
+ }
276
+ return (ws.image = resolved)
277
+ }
278
+
279
+ /** Build the SWE-bench Environment + a DISJOINT-slice task supplier over the Verified split. The
280
+ * supplier keys tasks by dataset offset so `runStrategyEvolution`'s train [0,trainN) and holdout
281
+ * [trainN+off,…) never overlap. Verified is loaded once; instances carry their repo/base_commit. */
282
+ export async function createSweBenchEnvironment(
283
+ poolN = 80,
284
+ opts: {
285
+ ids?: readonly string[]
286
+ enableRun?: boolean
287
+ cloneCache?: boolean
288
+ expectedImageIdentities?: ReadonlyMap<string, SweImageIdentity>
289
+ adapterOptions?: SweBenchAdapterOptions
290
+ } = {},
291
+ ): Promise<{
292
+ environment: AgenticSurface
293
+ tasks: (offset: number, n: number) => Promise<AgenticTask[]>
294
+ adapter: ReturnType<typeof createSweBenchAdapter>
295
+ }> {
296
+ const adapter = createSweBenchAdapter(opts.adapterOptions)
297
+ // WITH-TOOLS arm: expose the jailed `run` tool + use the run-aware seed prompt. Default OFF keeps
298
+ // the established read/edit-only baseline byte-identical.
299
+ const enableRun = opts.enableRun ?? false
300
+ const pool = opts.ids?.length
301
+ ? await adapter.loadTasks({ ids: [...opts.ids], split: 'test' })
302
+ : await adapter.loadTasks({ limit: poolN, split: 'test' })
303
+ const byId = new Map(pool.map((t) => [t.id, t]))
304
+ // Each environment owns its workspace registry so concurrent environments don't share state.
305
+ const workspaces = new Map<string, Ws>()
306
+ // Opt-in per-instance clone cache: clone each instance from GitHub once, then copy the pristine
307
+ // checkout for later sessions. This removes a mid-run network dependency without sharing edits.
308
+ const cloneCache = opts.cloneCache ?? false
309
+ const pristine = new Map<string, Promise<string>>()
310
+ const clonedAt = async (md: Record<string, string>, dir: string): Promise<void> => {
311
+ await exec('git', ['clone', '--filter=blob:none', '--no-checkout', '--quiet', `https://github.com/${md.repo}.git`, dir], { timeout: 420_000 })
312
+ await exec('git', ['-C', dir, 'checkout', '--quiet', md.base_commit], { timeout: 300_000 })
313
+ }
314
+ const pristineClone = (id: string, md: Record<string, string>): Promise<string> => {
315
+ let pending = pristine.get(id)
316
+ if (!pending) {
317
+ pending = (async () => {
318
+ const dir = mkdtempSync(join(absoluteSweTempDir(), 'swe-cache-'))
319
+ try {
320
+ await clonedAt(md, dir)
321
+ return dir
322
+ } catch (error) {
323
+ rmSync(dir, { recursive: true, force: true })
324
+ pristine.delete(id)
325
+ throw error
326
+ }
327
+ })()
328
+ pristine.set(id, pending)
329
+ }
330
+ return pending
331
+ }
332
+
333
+ const environment: AgenticSurface = {
334
+ name: 'swe-bench-verified',
335
+ async open(task) {
336
+ const bt = byId.get(task.id)
337
+ if (!bt) throw new Error(`swe-bench-env: unknown task ${task.id}`)
338
+ const md = bt.metadata as Record<string, string>
339
+ const dir = mkdtempSync(join(absoluteSweTempDir(), 'swe-'))
340
+ try {
341
+ if (cloneCache) await copyPristineGitCheckout(await pristineClone(task.id, md), dir)
342
+ else await clonedAt(md, dir)
343
+ const handle: ArtifactHandle = { id: dir, surface: 'swe-bench-verified' }
344
+ workspaces.set(dir, { dir, task: bt })
345
+ return handle
346
+ } catch (error) {
347
+ rmSync(dir, { recursive: true, force: true })
348
+ throw error
349
+ }
350
+ },
351
+ async tools() {
352
+ const tools: AgenticTool[] = [
353
+ { type: 'function', function: { name: 'list_files', description: 'List source files under a repo subdirectory (recursive, bounded). "" = repo root.', parameters: { type: 'object', properties: { dir: { type: 'string' } }, required: ['dir'] } } },
354
+ { type: 'function', function: { name: 'read_file', description: 'Read a repo file by path.', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } } },
355
+ { type: 'function', function: { name: 'edit_file', description: 'Surgical fix: replace the EXACT old_string (must occur once — copy whitespace precisely) with new_string in a SOURCE file. Minimal changes, never whole-file rewrites. Test files are rejected.', parameters: { type: 'object', properties: { path: { type: 'string' }, old_string: { type: 'string' }, new_string: { type: 'string' } }, required: ['path', 'old_string', 'new_string'] } } },
356
+ ]
357
+ if (enableRun) {
358
+ tools.push({
359
+ type: 'function',
360
+ function: {
361
+ name: 'run',
362
+ description:
363
+ 'Run a shell command in the repo checkout to REPRODUCE the bug and VERIFY your fix. cwd is the ' +
364
+ 'repo root (do NOT cd). Use a one-line python -c inline check for a quick offline reproduction, ' +
365
+ 'or python -m pytest on an existing test file (add -k to select a case, plus -rA and ' +
366
+ '-p no:cacheprovider) to run tests near your change. Returns the exit code then the combined ' +
367
+ 'stdout+stderr. NETWORK IS DISABLED and hidden grading tests are absent. A non-zero exit is a ' +
368
+ 'normal failing-test signal, not a tool error.',
369
+ parameters: { type: 'object', properties: { cmd: { type: 'string' } }, required: ['cmd'] },
370
+ },
371
+ })
372
+ }
373
+ return tools
374
+ },
375
+ async call(handle, name, args) {
376
+ const ws = workspaces.get(handle.id)
377
+ if (!ws) return 'ERROR: workspace closed'
378
+ // Cheap pre-filter: reject absolute paths and `..` traversal, strip a leading `./`. The real
379
+ // boundary is the realpath jail check below (resolveInJail) — `safe` only normalizes the string
380
+ // form. `ws.dir` is passed for signature symmetry; the filter itself is root-independent.
381
+ const safe = (p: string): string | null => jailPath(ws.dir, p)
382
+ // Resolve `relPath` to an absolute path and assert it stays inside the workspace AFTER following
383
+ // symlinks (a repo symlink targeting /etc/passwd would otherwise escape the string-only jail).
384
+ // The target must exist (both callers read it first); a missing path throws and the caller
385
+ // surfaces the error message, matching the previous read-then-fail behavior.
386
+ const jailRoot = realpathSync(ws.dir)
387
+ const resolveInJail = (relPath: string): string | null => {
388
+ const real = realpathSync(join(ws.dir, relPath))
389
+ return isInsideJail(jailRoot, real) ? real : null
390
+ }
391
+ if (name === 'list_files') {
392
+ const sub = safe(String(args.dir ?? '')) ?? ''
393
+ const root = join(ws.dir, sub)
394
+ if (!existsSync(root)) return `(no such path: ${sub})`
395
+ const out: string[] = []
396
+ const walk = (d: string, depth: number) => {
397
+ if (depth > 2 || out.length > 240) return
398
+ let entries: string[] = []
399
+ try {
400
+ entries = readdirSync(d)
401
+ } catch {
402
+ return
403
+ }
404
+ for (const e of entries) {
405
+ if (e.startsWith('.') || e === 'node_modules' || e === '__pycache__') continue
406
+ const p = join(d, e)
407
+ let isDir = false
408
+ try {
409
+ isDir = lstatSync(p).isDirectory()
410
+ } catch {
411
+ continue
412
+ }
413
+ out.push(p.slice(ws.dir.length + 1) + (isDir ? '/' : ''))
414
+ if (isDir) walk(p, depth + 1)
415
+ }
416
+ }
417
+ walk(root, 0)
418
+ return out.slice(0, 240).join('\n') || '(empty)'
419
+ }
420
+ if (name === 'read_file') {
421
+ const p = safe(String(args.path ?? ''))
422
+ if (!p) return 'ERROR: invalid path'
423
+ let real: string | null
424
+ try {
425
+ real = resolveInJail(p)
426
+ } catch (e) {
427
+ return `(error: ${(e as Error).message})`
428
+ }
429
+ if (!real) return `ERROR: path ${p} escapes the workspace`
430
+ try {
431
+ const c = readFileSync(real, 'utf8')
432
+ return c.length > 24_000 ? `${c.slice(0, 24_000)}\n...[truncated]` : c
433
+ } catch (e) {
434
+ return `(error: ${(e as Error).message})`
435
+ }
436
+ }
437
+ if (name === 'edit_file') {
438
+ const p = safe(String(args.path ?? ''))
439
+ if (!p) return 'ERROR: invalid path'
440
+ if (isTestPath(p)) return 'REJECTED: editing test files is forbidden (the evaluation runs hidden tests).'
441
+ const oldStr = String(args.old_string ?? '')
442
+ const newStr = String(args.new_string ?? '')
443
+ let real: string | null
444
+ try {
445
+ real = resolveInJail(p)
446
+ } catch (e) {
447
+ return `(cannot read ${p}: ${(e as Error).message})`
448
+ }
449
+ if (!real) return `ERROR: path ${p} escapes the workspace`
450
+ let content: string
451
+ try {
452
+ content = readFileSync(real, 'utf8')
453
+ } catch (e) {
454
+ return `(cannot read ${p}: ${(e as Error).message})`
455
+ }
456
+ if (!oldStr) return 'ERROR: old_string is empty.'
457
+ const count = content.split(oldStr).length - 1
458
+ if (count === 0) return `ERROR: old_string not found in ${p}. read_file it and copy EXACT text.`
459
+ if (count > 1) return `ERROR: old_string appears ${count}× in ${p} — add surrounding context to make it unique.`
460
+ writeFileSync(real, content.replace(oldStr, newStr))
461
+ return `edited ${p}: replaced 1 occurrence`
462
+ }
463
+ if (enableRun && name === 'run') {
464
+ const cmd = String(args.cmd ?? '').trim()
465
+ if (!cmd) return 'ERROR: run requires a non-empty cmd'
466
+ const img = await resolveInstanceImage(ws, opts.expectedImageIdentities?.get(ws.task.id))
467
+ if (!img.ok) return `ERROR: run unavailable (${img.reason}) — continue with read_file/edit_file only`
468
+ const T = RUN_TIMEOUT_S
469
+ const containerName = `swe-run-${process.pid}-${Date.now()}-${runNameCounter++}`
470
+ // The whole toolchain is interpreted ONLY by the container's bash — the host never sees a shell
471
+ // (execFile + args array). The agent cmd rides in as $SWE_CMD (an env var, no host-quoting hazard).
472
+ // `{ …; } 2>&1` merges stderr into stdout preserving order; the group's exit = the last command's.
473
+ // conda.sh only DEFINES `conda` (unlike bin/activate, which would consume our positional args).
474
+ const script =
475
+ '{ source /opt/miniconda3/etc/profile.d/conda.sh && conda activate testbed && cd /testbed && ' +
476
+ 'timeout -s KILL "$SWE_T"s bash -c "$SWE_CMD"; } 2>&1'
477
+ // READ-ONLY mount ⇒ the container physically cannot mutate the graded checkout (`run` is purely
478
+ // observational; the only writer stays edit_file). `--network none` + `--rm` + the dual timeout.
479
+ const dockerArgs = [
480
+ 'run', '--rm', '--name', containerName, '--network', 'none',
481
+ '-v', `${ws.dir}:/testbed:ro`, '-w', '/testbed',
482
+ '-e', 'PYTHONDONTWRITEBYTECODE=1', '-e', `SWE_T=${T}`, '-e', `SWE_CMD=${cmd}`,
483
+ img.identity.id, 'bash', '-lc', script,
484
+ ]
485
+ let code = 0
486
+ let out = ''
487
+ try {
488
+ const r = await exec('docker', dockerArgs, { timeout: (T + 20) * 1000, killSignal: 'SIGKILL', maxBuffer: 20_000_000 })
489
+ out = r.stdout
490
+ } catch (e) {
491
+ const err = e as { code?: number; killed?: boolean; stdout?: string; message?: string }
492
+ if (typeof err.code === 'number') {
493
+ code = err.code
494
+ out = err.stdout ?? ''
495
+ } else {
496
+ // Host-level kill (execFile timeout) or docker failed to spawn: reap any orphaned container.
497
+ exec('docker', ['rm', '-f', containerName], { timeout: 20_000 }).catch(() => {})
498
+ if (err.killed) {
499
+ code = 124
500
+ out = `${err.stdout ?? ''}\n[host timeout: docker run exceeded ${T + 20}s and was killed]`
501
+ } else {
502
+ return `ERROR: run failed to execute (${String(err.message ?? e).slice(0, 200)})`
503
+ }
504
+ }
505
+ }
506
+ // Docker infra failures (container couldn't start / daemon vanished) are tool-misuse, not a test
507
+ // result → keep the ERROR: prefix so they don't pollute the failing-test signal.
508
+ if (code === 125 || /Cannot connect to the Docker daemon/i.test(out)) {
509
+ return `ERROR: run unavailable (docker: ${truncateHeadTail(out, 400)})`
510
+ }
511
+ const note = code === 124 || code === 137 ? `\n[command hit the ${T}s time/kill limit]` : ''
512
+ return `exit=${code}${note}\n${truncateHeadTail(out, RUN_OUTPUT_LIMIT)}`
513
+ }
514
+ return `ERROR: unknown tool ${name}`
515
+ },
516
+ async score(_task, handle): Promise<SurfaceScore> {
517
+ const ws = workspaces.get(handle.id)
518
+ if (!ws) return { passes: 0, total: 1, errored: 1 }
519
+ let patch = ''
520
+ try {
521
+ const r = await exec('git', ['-C', ws.dir, 'diff'], { maxBuffer: 20_000_000, timeout: 60_000 })
522
+ patch = r.stdout
523
+ } catch {
524
+ patch = ''
525
+ }
526
+ if (!patch.trim()) return { passes: 0, total: 1, errored: 0 }
527
+ try {
528
+ const s = await adapter.judge(ws.task, patch)
529
+ return { passes: s.resolved ? 1 : 0, total: 1, errored: 0 }
530
+ } catch {
531
+ return { passes: 0, total: 1, errored: 1 }
532
+ }
533
+ },
534
+ async close(handle) {
535
+ const ws = workspaces.get(handle.id)
536
+ if (!ws) return
537
+ workspaces.delete(handle.id)
538
+ rmSync(ws.dir, { recursive: true, force: true })
539
+ },
540
+ }
541
+
542
+ const tasks = async (offset: number, n: number): Promise<AgenticTask[]> => {
543
+ const slice = pool.slice(offset, offset + n)
544
+ if (slice.length < n) throw new Error(`swe-bench-env: pool exhausted at offset ${offset} (need ${n}, have ${slice.length}; raise poolN)`)
545
+ return slice.map((bt) => ({
546
+ id: bt.id,
547
+ systemPrompt: enableRun ? SWE_SEED_PROMPT_WITH_RUN : SWE_SEED_PROMPT,
548
+ userPrompt: bt.prompt,
549
+ meta: { instanceId: bt.id },
550
+ }))
551
+ }
552
+
553
+ return { environment, tasks, adapter }
554
+ }