@tangle-network/agent-bench 0.1.0

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 (111) hide show
  1. package/README.md +15 -0
  2. package/package.json +35 -0
  3. package/src/adapters.ts +60 -0
  4. package/src/aec-gate.mts +217 -0
  5. package/src/atom-humaneval.mts +197 -0
  6. package/src/atom-mcp-e2e.mts +223 -0
  7. package/src/benchmarks/_harness.ts +206 -0
  8. package/src/benchmarks/aec-bench.test.mts +53 -0
  9. package/src/benchmarks/aec-bench.ts +319 -0
  10. package/src/benchmarks/appworld.test.mts +45 -0
  11. package/src/benchmarks/appworld.ts +426 -0
  12. package/src/benchmarks/cad-design.ts +429 -0
  13. package/src/benchmarks/cadbench.ts +135 -0
  14. package/src/benchmarks/cadgenbench.ts +121 -0
  15. package/src/benchmarks/commit0.test.mts +71 -0
  16. package/src/benchmarks/commit0.ts +266 -0
  17. package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
  18. package/src/benchmarks/enterpriseops-gym.ts +343 -0
  19. package/src/benchmarks/finsearchcomp.ts +371 -0
  20. package/src/benchmarks/frames.ts +520 -0
  21. package/src/benchmarks/hotpotqa.ts +320 -0
  22. package/src/benchmarks/humaneval.ts +251 -0
  23. package/src/benchmarks/mind2web.ts +311 -0
  24. package/src/benchmarks/programbench.test.mts +60 -0
  25. package/src/benchmarks/programbench.ts +210 -0
  26. package/src/benchmarks/simpleqa.ts +390 -0
  27. package/src/benchmarks/swe-bench.ts +152 -0
  28. package/src/benchmarks/terminal-bench.ts +182 -0
  29. package/src/benchmarks/trata-hedge.ts +496 -0
  30. package/src/benchmarks/types.ts +58 -0
  31. package/src/browser/adapters/bad-design-audit.ts +153 -0
  32. package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
  33. package/src/browser/adapters/bad.ts +165 -0
  34. package/src/browser/agent-adapter.ts +145 -0
  35. package/src/browser/process-adapter.ts +146 -0
  36. package/src/browser/run-to-spans.ts +105 -0
  37. package/src/browser/run-to-spans.verify.ts +89 -0
  38. package/src/browser/ui-reviewer.ts +200 -0
  39. package/src/browser/ui-reviewer.verify.ts +94 -0
  40. package/src/browser/verify.ts +96 -0
  41. package/src/clbench-codebase-gate.mts +314 -0
  42. package/src/clbench-context-gate.mts +305 -0
  43. package/src/cloud-loop.mts +138 -0
  44. package/src/coding-skills/minimal-diff.md +9 -0
  45. package/src/coding-skills/read-before-edit.md +9 -0
  46. package/src/coding-skills/reproduce-first.md +10 -0
  47. package/src/coding-skills/run-tests-after-edit.md +9 -0
  48. package/src/coding-skills/trace-the-failure.md +9 -0
  49. package/src/commit0-env-run.mts +59 -0
  50. package/src/commit0-env.ts +173 -0
  51. package/src/commit0-gate.mts +529 -0
  52. package/src/commit0-prereqs.sh +48 -0
  53. package/src/corpus-replay.mts +300 -0
  54. package/src/corpus-report.mts +504 -0
  55. package/src/corpus.test.mts +273 -0
  56. package/src/corpus.ts +325 -0
  57. package/src/decoder-live.mts +133 -0
  58. package/src/directives.ts +84 -0
  59. package/src/diverse-gate.mjs +112 -0
  60. package/src/egress-probe.mts +26 -0
  61. package/src/eops-skills/address-every-subtask.md +5 -0
  62. package/src/eops-skills/exact-tools-and-args.md +5 -0
  63. package/src/eops-skills/full-sequence-to-goal.md +5 -0
  64. package/src/eops-skills/ground-every-value.md +5 -0
  65. package/src/eops-skills/honor-the-policies.md +5 -0
  66. package/src/examples/README.md +58 -0
  67. package/src/examples/math-demo.mts +110 -0
  68. package/src/examples/strategy-demo.mts +119 -0
  69. package/src/fleet.mts +121 -0
  70. package/src/gate-cli.mts +101 -0
  71. package/src/gate.test.mts +129 -0
  72. package/src/gate.ts +460 -0
  73. package/src/generate-eval/certify.ts +178 -0
  74. package/src/generate-eval/schema.ts +78 -0
  75. package/src/humaneval-gate.mts +204 -0
  76. package/src/humaneval-repair-gate.mts +143 -0
  77. package/src/index.ts +19 -0
  78. package/src/mcp-mount-probe.mts +126 -0
  79. package/src/profile-coordinates.ts +134 -0
  80. package/src/profiles.ts +128 -0
  81. package/src/refine-loop.test.mts +106 -0
  82. package/src/refine-loop.ts +106 -0
  83. package/src/research-gate.mts +132 -0
  84. package/src/research-shot.ts +134 -0
  85. package/src/resolve-client.ts +58 -0
  86. package/src/router-executor.ts +51 -0
  87. package/src/run-pool.ts +48 -0
  88. package/src/runtime-hook-recorder.ts +137 -0
  89. package/src/sandbox-run.ts +125 -0
  90. package/src/search-bench/bridge.ts +124 -0
  91. package/src/search-bench/export.mts +0 -0
  92. package/src/search-bench/parametric-check.mts +63 -0
  93. package/src/search-bench/profiles.ts +98 -0
  94. package/src/search-bench/run.mts +287 -0
  95. package/src/search-bench/tasks-fresh.ts +688 -0
  96. package/src/search-bench/tasks.ts +129 -0
  97. package/src/search-tool.ts +95 -0
  98. package/src/selector.test.mts +189 -0
  99. package/src/selector.ts +366 -0
  100. package/src/skill-sandbox-smoke.mts +100 -0
  101. package/src/stats.mts +90 -0
  102. package/src/terminal-compare.ts +519 -0
  103. package/src/trajectory-assemble.mjs +130 -0
  104. package/src/trata-gate.mts +243 -0
  105. package/src/trata-gepa.mts +434 -0
  106. package/src/worker-blender.ts +230 -0
  107. package/src/worker-browser.ts +102 -0
  108. package/src/worker-build123d.ts +143 -0
  109. package/src/worker-cad.ts +451 -0
  110. package/src/worker.ts +136 -0
  111. package/src/workspace-loop.mts +133 -0
@@ -0,0 +1,223 @@
1
+ /**
2
+ * THE WHOLE REAL THING, end to end. No mock, no stub.
3
+ *
4
+ * An opencode SUPERVISOR (via the cli-bridge) mounts the coordination MCP over a live Scope and
5
+ * carries the real `supervise` SKILL.md (a file in its cwd's skill dir — loaded natively by the
6
+ * harness, not stapled into a prompt). It authors a worker profile and calls spawn_worker.
7
+ * Each WORKER is an opencode coding session in its OWN cwd that edits files and is graded by a
8
+ * REAL test it must pass (the deployable check = `valid`). The supervisor settles only on a
9
+ * delivered worker. Real models (bridge non-Claude), real check, real driver↔worker transcripts.
10
+ *
11
+ * ROUTER_BASE=http://127.0.0.1:3355/v1 TANGLE_API_KEY=<bridge-bearer> \
12
+ * WORKER_MODEL=opencode/zai-coding-plan/glm-5-turbo npx tsx bench/src/atom-mcp-e2e.mts
13
+ */
14
+
15
+ import { execFileSync } from 'node:child_process'
16
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
17
+ import { tmpdir } from 'node:os'
18
+ import { dirname, join } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+ import {
21
+ type Agent,
22
+ type AgentProfile,
23
+ type AgentSpec,
24
+ contentAddress,
25
+ createExecutorRegistry,
26
+ createSupervisor,
27
+ type Executor,
28
+ type ExecutorResult,
29
+ gitWorkspace,
30
+ InMemoryResultBlobStore,
31
+ InMemorySpawnJournal,
32
+ runInWorkspace,
33
+ type Scope,
34
+ type Workspace,
35
+ } from '../../src/runtime/index'
36
+ import { asAuthoredProfile } from '../../src/runtime/supervise/authoring'
37
+ import { serveCoordinationMcp } from '../../src/runtime/supervise/coordination-mcp'
38
+
39
+ const BRIDGE = (process.env.ROUTER_BASE ?? 'http://127.0.0.1:3355/v1').replace(/\/$/, '')
40
+ const BEARER = process.env.TANGLE_API_KEY ?? ''
41
+ const MODEL = process.env.WORKER_MODEL ?? 'opencode/zai-coding-plan/glm-5-turbo'
42
+ const REPO = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
43
+ const SKILL_MD = readFileSync(join(REPO, 'skills', 'supervise', 'SKILL.md'), 'utf8')
44
+
45
+ const TASK = 'In solution.py, implement add(a, b) so it returns the sum a + b and test_solution.py passes.'
46
+
47
+ /** Seed a bare git repo with the failing task — the SHARED workspace ref every worker clones. */
48
+ function seedWorkspaceRepo(): string {
49
+ const git = (args: string[], cwd?: string): void => {
50
+ execFileSync('git', ['-c', 'core.hooksPath=/dev/null', '-c', 'user.email=t@t', '-c', 'user.name=t', ...args], {
51
+ cwd,
52
+ stdio: 'pipe',
53
+ })
54
+ }
55
+ const bare = `${mkdtempSync(join(tmpdir(), 'e2e-ws-'))}.git`
56
+ git(['init', '--bare', '-b', 'main', bare])
57
+ const seed = mkdtempSync(join(tmpdir(), 'e2e-seed-'))
58
+ git(['clone', bare, seed])
59
+ writeFileSync(join(seed, 'solution.py'), 'def add(a, b):\n raise NotImplementedError\n')
60
+ writeFileSync(
61
+ join(seed, 'test_solution.py'),
62
+ 'from solution import add\nassert add(2, 3) == 5\nassert add(-1, 1) == 0\nassert add(0, 0) == 0\nprint("PASS")\n',
63
+ )
64
+ git(['add', '-A'], seed)
65
+ git(['commit', '-m', 'task'], seed)
66
+ git(['push', 'origin', 'main'], seed)
67
+ rmSync(seed, { recursive: true, force: true })
68
+ return bare
69
+ }
70
+
71
+ /** The deployable check: run the test in the worker's cwd. Exit 0 = delivered. No LLM judge. */
72
+ function checkPasses(cwd: string): boolean {
73
+ try {
74
+ execFileSync('python3', ['test_solution.py'], { cwd, stdio: 'pipe', timeout: 30_000 })
75
+ return true
76
+ } catch {
77
+ return false
78
+ }
79
+ }
80
+
81
+ async function bridgeChat(opts: {
82
+ messages: Array<{ role: string; content: string }>
83
+ cwd?: string
84
+ mcpUrl?: string
85
+ }): Promise<string> {
86
+ const r = await fetch(`${BRIDGE}/chat/completions`, {
87
+ method: 'POST',
88
+ headers: { authorization: `Bearer ${BEARER}`, 'content-type': 'application/json' },
89
+ body: JSON.stringify({
90
+ model: MODEL,
91
+ messages: opts.messages,
92
+ ...(opts.cwd ? { cwd: opts.cwd } : {}),
93
+ ...(opts.mcpUrl ? { mcp: { mcpServers: { coordination: { type: 'http', url: opts.mcpUrl } } } } : {}),
94
+ }),
95
+ })
96
+ if (!r.ok) return `(bridge HTTP ${r.status}: ${(await r.text()).slice(0, 200)})`
97
+ const j = (await r.json()) as { choices?: Array<{ message?: { content?: string } }> }
98
+ return j.choices?.[0]?.message?.content ?? ''
99
+ }
100
+
101
+ const transcripts: Array<{ who: string; said: string; delivered?: boolean }> = []
102
+
103
+ /** A WORKER = a real opencode coding session in a clone of the SHARED workspace, graded by the
104
+ * real test; its delivery is committed back so the next worker builds on it (not isolated). */
105
+ function makeWorker(rawProfile: unknown, ws: Workspace, n: number): Agent<unknown, unknown> {
106
+ const p = asAuthoredProfile(rawProfile)
107
+ const name = p?.name ?? `worker-${n}`
108
+ let artifact: ExecutorResult<unknown> | undefined
109
+ const inner: Executor<unknown> = {
110
+ runtime: 'router',
111
+ async execute() {
112
+ const sys = p?.systemPrompt ?? TASK
113
+ const run = await runInWorkspace(
114
+ ws,
115
+ async (cwd) => {
116
+ const said = await bridgeChat({
117
+ messages: [
118
+ {
119
+ role: 'user',
120
+ content: `${sys}\n\nYou are working in the current directory (it already holds prior workers' committed progress). Edit the files so that running \`python3 test_solution.py\` prints PASS. Do it now.`,
121
+ },
122
+ ],
123
+ cwd,
124
+ })
125
+ const valid = checkPasses(cwd)
126
+ transcripts.push({ who: name, said: said.slice(0, 300), delivered: valid })
127
+ return { valid, value: said.slice(0, 120), message: `${name}: ${valid ? 'delivered' : 'wip'}` }
128
+ },
129
+ { tmpPrefix: 'e2e-worker-', commitOnInvalid: true },
130
+ )
131
+ const delivered = run.valid
132
+ artifact = {
133
+ outRef: contentAddress(`${name}:${delivered}`),
134
+ out: { worker: name, delivered, rev: run.commit?.ok ? run.commit.rev : undefined, profileSystemPrompt: sys.slice(0, 120) },
135
+ verdict: { valid: delivered, score: delivered ? 1 : 0 },
136
+ spent: { iterations: 1, tokens: { input: 0, output: 0 }, usd: 0, ms: 0 },
137
+ }
138
+ return artifact
139
+ },
140
+ teardown: () => Promise.resolve({ destroyed: true }),
141
+ resultArtifact: () => {
142
+ if (!artifact) throw new Error('worker resultArtifact before execute')
143
+ return artifact
144
+ },
145
+ }
146
+ const spec: AgentSpec = { profile: { name } as AgentProfile, harness: null, executor: inner }
147
+ return { name, act: async () => '', executorSpec: spec } as Agent<unknown, unknown> & { executorSpec: AgentSpec }
148
+ }
149
+
150
+ async function main(): Promise<void> {
151
+ console.log(`atom-mcp-e2e: model=${MODEL} (real boxes, real MCP, real test, shared workspace)`)
152
+ const bareRef = seedWorkspaceRepo()
153
+ const ws = gitWorkspace({ ref: bareRef })
154
+ const blobs = new InMemoryResultBlobStore()
155
+ let n = 0
156
+
157
+ const root: Agent<unknown, unknown> = {
158
+ name: 'supervisor',
159
+ async act(_t, scope: Scope<unknown>) {
160
+ const mcp = await serveCoordinationMcp({
161
+ scope,
162
+ blobs,
163
+ makeWorkerAgent: (raw) => makeWorker(raw, ws, n++),
164
+ perWorker: { maxIterations: 2, maxTokens: 200_000 },
165
+ })
166
+ // The supervisor's cwd carries the REAL skill file (opencode loads it from the cwd skill dirs).
167
+ const supCwd = mkdtempSync(join(tmpdir(), 'e2e-sup-'))
168
+ for (const d of ['.opencode/skills/supervise', '.claude/skills/supervise']) {
169
+ mkdirSync(join(supCwd, d), { recursive: true })
170
+ writeFileSync(join(supCwd, d, 'SKILL.md'), SKILL_MD)
171
+ }
172
+ try {
173
+ console.error(`[e2e] coordination MCP at ${mcp.url}; supervisor cwd=${supCwd}`)
174
+ const said = await bridgeChat({
175
+ messages: [
176
+ {
177
+ role: 'user',
178
+ content: `${TASK}\n\nYou are a SUPERVISOR. You have the "supervise" skill and a "coordination" MCP with tools spawn_worker, await_event, stop. Do NOT write code yourself. Author a worker profile (a JSON object with name + a rich systemPrompt telling the worker exactly what to implement) and call spawn_worker with it, then await_event, and stop once a worker delivered (valid:true).`,
179
+ },
180
+ ],
181
+ cwd: supCwd,
182
+ mcpUrl: mcp.url,
183
+ })
184
+ transcripts.push({ who: 'supervisor', said: said.slice(0, 400) })
185
+ const settled = mcp.settled()
186
+ const delivered = settled.filter((w) => w.status === 'done' && w.valid === true)
187
+ console.error(`[e2e] supervisor spawned ${settled.length} worker(s), ${delivered.length} delivered`)
188
+ return delivered[0]?.outRef ? await blobs.get(delivered[0].outRef) : undefined
189
+ } finally {
190
+ await mcp.close()
191
+ rmSync(supCwd, { recursive: true, force: true })
192
+ }
193
+ },
194
+ }
195
+
196
+ const result = await createSupervisor<unknown, unknown>().run(root, TASK, {
197
+ budget: { maxIterations: 100, maxTokens: 2_000_000 },
198
+ runId: 'e2e',
199
+ journal: new InMemorySpawnJournal(),
200
+ blobs,
201
+ executors: createExecutorRegistry(),
202
+ maxDepth: 4,
203
+ now: () => Date.now(),
204
+ })
205
+ rmSync(bareRef, { recursive: true, force: true })
206
+
207
+ console.log('\n── transcripts (real driver↔worker) ──')
208
+ for (const t of transcripts) {
209
+ console.log(`\n[${t.who}${t.delivered === undefined ? '' : t.delivered ? ' · DELIVERED' : ' · failed'}]`)
210
+ console.log(` ${t.said.replace(/\n/g, '\n ')}`)
211
+ }
212
+ console.log('\n── verdict ──')
213
+ console.log(
214
+ result.kind === 'winner'
215
+ ? `✅ REAL E2E DELIVERED — supervisor (via MCP + skill) drove an in-box worker that coded + passed the real test. out=${JSON.stringify(result.out)}`
216
+ : `❌ no delivery (result=${result.kind}) — see transcripts above`,
217
+ )
218
+ }
219
+
220
+ main().catch((e) => {
221
+ console.error(e instanceof Error ? (e.stack ?? e.message) : String(e))
222
+ process.exit(1)
223
+ })
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Shared code-bench harness. The "stage the artifact → run the benchmark's own
3
+ * evaluator in an external process (mkdtemp / execFile / .venv python / Docker)
4
+ * → read its JSON report → { resolved, score }" spine that swe-bench,
5
+ * terminal-bench, commit0, programbench and aec-bench all need. Factored out so
6
+ * the Docker/venv/report-reading logic lives in ONE place instead of being
7
+ * copy-pasted per adapter.
8
+ *
9
+ * It owns NO benchmark policy: each adapter passes the argv for its evaluator
10
+ * and a `parseReport` that maps that evaluator's report JSON → a BenchScore. The
11
+ * harness owns process spawning, temp-dir lifecycle, large-buffer/timeout config
12
+ * and fail-loud diagnostics.
13
+ *
14
+ * Fail-loud: an absent .venv / harness / Docker daemon THROWS from `preflight`
15
+ * (the adapter passes the import line + the exact fix). A staged run that exits
16
+ * nonzero throws with the captured stderr — never a fabricated score.
17
+ */
18
+
19
+ import { execFile, spawn } from 'node:child_process'
20
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
21
+ import { tmpdir } from 'node:os'
22
+ import { join } from 'node:path'
23
+ import { fileURLToPath } from 'node:url'
24
+ import { promisify } from 'node:util'
25
+ import type { BenchScore } from './types'
26
+
27
+ const execFileAsync = promisify(execFile)
28
+
29
+ /** Repo root for the bench package (…/bench), so `.venv` and `fixtures` resolve. */
30
+ export const benchRoot = fileURLToPath(new URL('../..', import.meta.url))
31
+ /** The bench venv interpreter every python-backed evaluator runs through. */
32
+ export const venvPython = join(benchRoot, '.venv', 'bin', 'python')
33
+
34
+ /** Interpreter for a NAMED isolated venv (e.g. `.venv-commit0`). Benches whose pip
35
+ * deps conflict with the shared `.venv` (commit0 downgrades pydantic/sqlalchemy)
36
+ * get their own venv and pass its python explicitly — keeping the shared one clean. */
37
+ export const venvPythonAt = (venvDir: string): string => join(benchRoot, venvDir, 'bin', 'python')
38
+ /** Report/transcript reads are large; 256 MiB matches the SWE harness budget. */
39
+ export const bigBuffer = 1024 * 1024 * 256
40
+
41
+ /** Path to a named executable inside the bench venv (e.g. `venvBin('tb')`). */
42
+ export function venvBin(name: string): string {
43
+ return join(benchRoot, '.venv', 'bin', name)
44
+ }
45
+
46
+ /**
47
+ * Run the bench venv python with an inline script (`-c`); return stdout. Throws
48
+ * (with stderr) on a nonzero exit — the loaders rely on this to fail loud rather
49
+ * than parse a partial dump.
50
+ */
51
+ export async function runVenvPython(
52
+ script: string,
53
+ args: string[] = [],
54
+ timeoutMs = 0,
55
+ python: string = venvPython,
56
+ ): Promise<string> {
57
+ const { stdout } = await execFileAsync(python, ['-c', script, ...args], {
58
+ maxBuffer: bigBuffer,
59
+ timeout: timeoutMs,
60
+ })
61
+ return stdout
62
+ }
63
+
64
+ /**
65
+ * Preflight a python-backed harness: import the module(s) and (optionally) ping
66
+ * Docker, all inside the bench venv. On failure THROWS the captured error joined
67
+ * to the adapter's `fix` guidance — the contract every code-bench preflight wants.
68
+ */
69
+ export async function preflightVenvImports(opts: {
70
+ /** Module names to `import` (e.g. ['swebench']); '' entries are ignored. */
71
+ modules: string[]
72
+ /** Also `docker.from_env().ping()` — true for Docker-backed evaluators. */
73
+ requireDocker?: boolean
74
+ /** Actionable remediation appended to the thrown message. */
75
+ fix: string
76
+ /** Override the interpreter (e.g. an isolated `.venv-commit0`). Default: shared `.venv`. */
77
+ python?: string
78
+ }): Promise<void> {
79
+ const imports = opts.modules.filter((m) => m.length > 0)
80
+ const lines = [...imports.map((m) => `import ${m}`)]
81
+ if (opts.requireDocker) lines.push('import docker', 'docker.from_env().ping()')
82
+ lines.push("print('ok')")
83
+ try {
84
+ await runVenvPython(lines.join('\n'), [], 0, opts.python ?? venvPython)
85
+ } catch (err) {
86
+ const msg = err instanceof Error ? err.message : String(err)
87
+ throw new Error(`${msg}\n${opts.fix}`)
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Run a bench-local python driver script (in the bench venv) while piping
93
+ * `input` to its stdin, returning stdout. The driver's verdict JSON is its LAST
94
+ * stdout line; callers parse that and inspect an `error` field (fail loud).
95
+ *
96
+ * Uses spawn + an explicit `stdin.end(input)` rather than promisify(execFile)'s
97
+ * `input` option, because that option is NOT honored by async execFile — stdin
98
+ * is left open and a driver that does `sys.stdin.read()` blocks forever. The
99
+ * artifact-piping judges (commit0, appworld) MUST go through this.
100
+ */
101
+ export function runVenvScriptStdin(
102
+ scriptPath: string,
103
+ args: string[],
104
+ input: string,
105
+ opts: { cwd?: string; timeoutMs?: number; python?: string } = {},
106
+ ): Promise<string> {
107
+ return new Promise<string>((resolve, reject) => {
108
+ const child = spawn(opts.python ?? venvPython, [scriptPath, ...args], {
109
+ cwd: opts.cwd ?? benchRoot,
110
+ ...(opts.timeoutMs ? { timeout: opts.timeoutMs } : {}),
111
+ })
112
+ let stdout = ''
113
+ let stderr = ''
114
+ let bytes = 0
115
+ child.stdout.on('data', (c: Buffer) => {
116
+ bytes += c.length
117
+ if (bytes <= bigBuffer) stdout += c.toString('utf8')
118
+ })
119
+ child.stderr.on('data', (c: Buffer) => {
120
+ stderr += c.toString('utf8')
121
+ })
122
+ child.on('error', (err) => reject(err))
123
+ child.on('close', (code) => {
124
+ if (code === 0) resolve(stdout)
125
+ else reject(new Error((stderr || stdout || `exit ${code}`).slice(0, 1500)))
126
+ })
127
+ child.stdin.end(input)
128
+ })
129
+ }
130
+
131
+ export interface StagedRunSpec {
132
+ /** mkdtemp prefix, e.g. 'swebench-' / 'commit0-'. */
133
+ tmpPrefix: string
134
+ /**
135
+ * Write the artifact + any harness inputs into the temp dir. Returns nothing;
136
+ * `argv`/`cwd` consume `dir` to point the evaluator at what was written.
137
+ */
138
+ stage(dir: string): Promise<void>
139
+ /** The external evaluator to spawn. `bin` defaults to the bench venv python. */
140
+ bin?: string
141
+ /** argv for the evaluator, computed from the temp `dir`. */
142
+ argv(dir: string): string[]
143
+ /** Working directory for the evaluator. Defaults to the temp `dir`. */
144
+ cwd?(dir: string): string
145
+ /** Hard timeout for the evaluator (ms); 0 = none. */
146
+ timeoutMs?: number
147
+ /**
148
+ * Read the evaluator's report(s) out of `dir` and map to a BenchScore. Throws
149
+ * if the expected report is absent/malformed (fail loud — no default score).
150
+ */
151
+ parseReport(dir: string): Promise<BenchScore>
152
+ /** Keep the temp dir on disk (debugging). Default false → always cleaned up. */
153
+ keepTmp?: boolean
154
+ }
155
+
156
+ /**
157
+ * The shared judge body: mkdtemp → stage → spawn evaluator → parseReport →
158
+ * cleanup. The evaluator's stdout/stderr is surfaced on failure; the temp dir is
159
+ * always removed in `finally` unless `keepTmp`.
160
+ */
161
+ export async function runStagedJudge(spec: StagedRunSpec): Promise<BenchScore> {
162
+ const dir = await mkdtemp(join(tmpdir(), spec.tmpPrefix))
163
+ try {
164
+ await spec.stage(dir)
165
+ const bin = spec.bin ?? venvPython
166
+ try {
167
+ await execFileAsync(bin, spec.argv(dir), {
168
+ cwd: spec.cwd ? spec.cwd(dir) : dir,
169
+ maxBuffer: bigBuffer,
170
+ ...(spec.timeoutMs ? { timeout: spec.timeoutMs } : {}),
171
+ })
172
+ } catch (err) {
173
+ const e = err as { stderr?: string; stdout?: string; message?: string }
174
+ const detail = (e.stderr || e.stdout || e.message || String(err)).slice(0, 2000)
175
+ throw new Error(`${spec.tmpPrefix.replace(/-$/, '')} evaluator failed (${bin} ${spec.argv(dir).join(' ')}):\n${detail}`)
176
+ }
177
+ return await spec.parseReport(dir)
178
+ } finally {
179
+ if (!spec.keepTmp) await rm(dir, { recursive: true, force: true }).catch(() => {})
180
+ }
181
+ }
182
+
183
+ /** Read + JSON.parse a report file from a staged run; throws with the path on failure. */
184
+ export async function readJsonReport<T>(path: string): Promise<T> {
185
+ let raw: string
186
+ try {
187
+ raw = await readFile(path, 'utf8')
188
+ } catch (err) {
189
+ throw new Error(`expected report not written: ${path} (${err instanceof Error ? err.message : err})`)
190
+ }
191
+ try {
192
+ return JSON.parse(raw) as T
193
+ } catch (err) {
194
+ throw new Error(`report not valid JSON at ${path}: ${err instanceof Error ? err.message : err}`)
195
+ }
196
+ }
197
+
198
+ /** Write a UTF-8 file into a staged dir (artifact / preds.json / attempt.sh / …). */
199
+ export async function stageFile(path: string, content: string): Promise<void> {
200
+ await writeFile(path, content)
201
+ }
202
+
203
+ /** Sanitize an instance id into a filesystem/run-id-safe token. */
204
+ export function safeRunId(prefix: string, id: string): string {
205
+ return `${prefix}-${id}`.replace(/[^a-zA-Z0-9_.-]/g, '_')
206
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Offline AEC-Bench adapter test. AEC's judge runs the task's own verify.py with
3
+ * python3 (stdlib only, no Docker, no pip), so the FULL judge is exercised here:
4
+ * gold → score 1, empty → score 0, with per-field partial credit in detail. Run:
5
+ * AEC_FIXTURES=1 npx tsx --test src/benchmarks/aec-bench.test.mts
6
+ */
7
+ import assert from 'node:assert/strict'
8
+ import { test } from 'node:test'
9
+ import { createAecBenchAdapter } from './aec-bench'
10
+
11
+ process.env.AEC_FIXTURES = '1'
12
+
13
+ const id = 'electrical/catenary-sag'
14
+
15
+ test('loadTasks (fixtures) yields self-contained tasks with verifier metadata', async () => {
16
+ const a = createAecBenchAdapter()
17
+ const tasks = await a.loadTasks({ ids: [id] })
18
+ assert.equal(tasks.length, 1)
19
+ const t = tasks[0]
20
+ assert.equal(t.id, id)
21
+ assert.equal(t.split, 'electrical')
22
+ assert.ok(t.prompt.length > 0, 'prompt is the instruction.md')
23
+ const md = t.metadata as Record<string, unknown>
24
+ assert.equal(typeof md.verifyPy, 'string')
25
+ assert.ok((md.verifyPy as string).length > 0)
26
+ assert.equal(typeof md.goldenPassMd, 'string')
27
+ })
28
+
29
+ test('loadTasks limit slices the fixture set', async () => {
30
+ const a = createAecBenchAdapter()
31
+ const tasks = await a.loadTasks({ limit: 1 })
32
+ assert.equal(tasks.length, 1)
33
+ })
34
+
35
+ test('judge: REAL verify.py — gold resolves with full per-field credit', async () => {
36
+ const a = createAecBenchAdapter()
37
+ const [t] = await a.loadTasks({ ids: [id] })
38
+ const gold = await a.goldArtifact(t)
39
+ assert.equal(typeof gold, 'string')
40
+ const score = await a.judge(t, gold as string)
41
+ assert.equal(score.resolved, true)
42
+ assert.equal(score.score, 1)
43
+ const detail = JSON.parse(score.detail as string) as { fields: Record<string, number> }
44
+ for (const v of Object.values(detail.fields)) assert.equal(v, 1)
45
+ })
46
+
47
+ test('judge: REAL verify.py — empty artifact fails closed to 0 (no fabricated score)', async () => {
48
+ const a = createAecBenchAdapter()
49
+ const [t] = await a.loadTasks({ ids: [id] })
50
+ const score = await a.judge(t, '')
51
+ assert.equal(score.resolved, false)
52
+ assert.equal(score.score, 0)
53
+ })