@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,529 @@
1
+ /**
2
+ * commit0 Layer-1 gate runner — the verifier-grounded selector AND the
3
+ * observe→steer efficacy experiment on REAL stateful coding rollouts. Each shot is
4
+ * a fresh sandbox where an opencode agent clones the stubbed repo at base_commit,
5
+ * implements the source, and WRITES its diff to a file we read back over the
6
+ * sandbox FS — NOT pasted in the final message (a large diff truncates there →
7
+ * `git apply` "corrupt patch", the failure that made the generic stream-parse path
8
+ * unusable). The official commit0 pytest harness then grades each diff to a
9
+ * continuous (passed+xfail)/total reward — the within-task variance the
10
+ * verifier-grounded selector needs (unlike aec's per-task-deterministic scores).
11
+ *
12
+ * ARMS (comma-separated, default `random`):
13
+ * - random — K independent blind shots (the equal-compute control).
14
+ * - refineAudit — shot 0 blind, then for each later shot a TRACE-ONLY analyst
15
+ * (ANALYST_MODEL, default deepseek-v4-pro, off-box via the router) reads the
16
+ * prior shot's diff + stream-trace tail and its findings are appended to a
17
+ * FRESH sandbox's prompt. The analyst never sees a judge score — judging is
18
+ * phase 2, after every rollout, so selector≠judge holds by construction.
19
+ * Both arms run K shots per task: equal compute is the non-negotiable invariant
20
+ * (the analyst call is the steering overhead, one cheap router completion).
21
+ *
22
+ * Two phases, deliberately split:
23
+ * 1. ROLLOUTS run concurrently (sandbox-bound; CONCURRENCY units in flight; a
24
+ * steered arm's shots chain sequentially inside one unit).
25
+ * 2. JUDGES run SEQUENTIALLY (Docker-bound). commit0 keys its report dir on
26
+ * hash(test_ids), shared across a repo's attempts, so concurrent judging of the
27
+ * same repo races/overwrites report.json — the `None`-score bug.
28
+ *
29
+ * Judge prerequisite: the per-repo Docker image MUST exist before judging — the
30
+ * local Docker backend ignores `rebuild_image` (only the Modal context honors it),
31
+ * so run `./src/commit0-prereqs.sh <repo…>` (pulls wentingzhao/<repo>:v0, else
32
+ * builds via commit0.harness.build) for every repo in the task list first.
33
+ *
34
+ * Fail loud: an attempt whose rollout errored, whose diff never materialized, or
35
+ * whose judge harness failed is recorded as an INFRA attempt (error on the
36
+ * AttemptRecord, no score) and its record is marked infraError — counted +
37
+ * reported, excluded by corpus-report, never a silent 0.
38
+ *
39
+ * Writes one corpus RunRecord per (task, arm) (conditions `random@K`,
40
+ * `refineAudit@K`) the existing `corpus-replay --selector=verifier` +
41
+ * `corpus-report` consume unchanged.
42
+ *
43
+ * ./src/commit0-prereqs.sh wcwidth tinydb … # once per repo set
44
+ * dotenvx run -f … -- env N=8 K=2 ARMS=random,refineAudit \
45
+ * WORKER_MODEL=deepseek-v4-pro CONCURRENCY=3 CORPUS=/tmp/commit0.jsonl \
46
+ * tsx src/commit0-gate.mts
47
+ * tsx src/corpus-report.mts /tmp/commit0.jsonl # paired steering verdict
48
+ * tsx src/corpus-replay.mts /tmp/commit0.jsonl --selector=verifier
49
+ */
50
+
51
+ import { spawn } from 'node:child_process'
52
+ import { mkdtemp, rm } from 'node:fs/promises'
53
+ import { tmpdir } from 'node:os'
54
+ import { join } from 'node:path'
55
+ import {
56
+ type AgentRunSpec,
57
+ type Deliverable,
58
+ openSandboxRun,
59
+ type SandboxRun,
60
+ } from '@tangle-network/agent-runtime/loops'
61
+ import { Sandbox } from '@tangle-network/sandbox'
62
+ import { createCommit0Adapter } from './benchmarks/commit0'
63
+ import type { BenchTask } from './benchmarks/types'
64
+ import { type AttemptRecord, appendRunRecord, buildRunRecordFromAttempts } from './corpus'
65
+ import { type AnalystFn, llmAnalyst } from './sandbox-run'
66
+ import {
67
+ type BenchRuntimeDecisionPoint,
68
+ type BenchRuntimeHookEvent,
69
+ createRuntimeHookRecorder,
70
+ } from './runtime-hook-recorder'
71
+ import { pool } from './stats.mts'
72
+
73
+ function must(name: string): string {
74
+ const v = process.env[name]
75
+ if (!v) throw new Error(`env ${name} is required`)
76
+ return v
77
+ }
78
+
79
+ const PATCH_PATH = '/tmp/solution.patch'
80
+ const randomSuffix = () => Math.random().toString(36).slice(2, 10)
81
+
82
+ interface Commit0Meta {
83
+ repo: string
84
+ baseCommit: string
85
+ srcDir: string
86
+ testDir: string
87
+ specification: string
88
+ }
89
+
90
+ interface Shot {
91
+ task: BenchTask
92
+ attempt: number
93
+ diff: string
94
+ ok: boolean
95
+ detail?: string
96
+ wallMs: number
97
+ /** measured count of stream events from the rollout (0 if it errored before streaming) */
98
+ events: number
99
+ /** bounded tail of the raw stream events — the trace the analyst reads (trace-only). */
100
+ traceEvents?: unknown[]
101
+ /** the analyst findings appended to this shot's prompt (steered arm, round ≥ 1). */
102
+ steer?: string
103
+ runtimeEvents?: BenchRuntimeHookEvent[]
104
+ runtimeDecisionPoints?: BenchRuntimeDecisionPoint[]
105
+ }
106
+
107
+ /** Last events of a rollout stream, kept for the trace-only analyst. */
108
+ const TRACE_EVENTS_TAIL = 12
109
+ /** Diff prefix shown to the analyst (it names files + hunks; the full diff can be 100KB+). */
110
+ const ANALYST_DIFF_MAX = 6_000
111
+ /** Findings ceiling appended to a steered prompt (the analyst is told 1-3 sentences). */
112
+ const STEER_MAX = 1_500
113
+
114
+ /** Append the analyst's findings to the base rollout prompt (fresh box — the
115
+ * agent has no memory of the prior attempt, so the framing names it). */
116
+ function steeredPrompt(base: string, steer: string): string {
117
+ return `${base}\n\n--- Analysis of a previous attempt at this task ---\n${steer}\n\nApply this correction while you implement.`
118
+ }
119
+
120
+ /** Build the rollout prompt: clone the stub, implement the source, write the diff to
121
+ * a FILE (the robust deliverable). Mirrors solveShot's file-read contract. */
122
+ function rolloutPrompt(meta: Commit0Meta): string {
123
+ return [
124
+ `Clone https://github.com/${meta.repo} into /work, then \`cd /work && git checkout ${meta.baseCommit}\`.`,
125
+ `The public functions/classes under \`${meta.srcDir}\` are stubbed (empty \`pass\`/\`...\` bodies). Your job is to`,
126
+ `implement COMPLETE, CORRECT bodies under \`${meta.srcDir}\` so the existing test suite under \`${meta.testDir}\` passes.`,
127
+ '',
128
+ 'Work iteratively — do NOT stop at a first draft:',
129
+ `1. Read the spec (${meta.specification}) and the tests under \`${meta.testDir}\` to learn the exact required behavior.`,
130
+ `2. Install the package editable so imports resolve: \`pip install -e .\` (use the repo's setup if it differs).`,
131
+ `3. Implement ALL stubbed bodies under \`${meta.srcDir}\` — every function/class, not just the easy ones.`,
132
+ `4. RUN the suite: \`python -m pytest ${meta.testDir} -q\`. Read failures and FIX them. Repeat until as many tests`,
133
+ ` pass as you can get — keep iterating; a partial implementation that fails most tests is not done.`,
134
+ '5. Do NOT edit the tests — the evaluation re-runs them on a fresh clone.',
135
+ '',
136
+ `When the suite is green (or you have maximized passing tests), from /work run EXACTLY:`,
137
+ ` git add -A && git diff --cached -- ${meta.srcDir} > ${PATCH_PATH}`,
138
+ `Then stop. The patch file is the only deliverable — do NOT paste the diff in your reply.`,
139
+ ].join('\n')
140
+ }
141
+
142
+ interface ShotCfg {
143
+ sandboxBaseUrl: string
144
+ sandboxKey: string
145
+ routerBaseUrl: string
146
+ /** Host-side opencode (`COMMIT0_BACKEND=local`) only — routes `openai/*` models
147
+ * through the router from THIS machine. The sandbox path never sends it into
148
+ * the box: in-box model auth is the box-provisioned `OPENCODE_MODEL_API_KEY`. */
149
+ routerKey: string
150
+ model: string
151
+ /** in-box opencode provider. `openai-compat` (default) is the generic passthrough,
152
+ * so router-served cheap models resolve in-box; `openai` only accepts its
153
+ * registered model names (e.g. gpt-4.1). Override via WORKER_PROVIDER. */
154
+ provider: string
155
+ timeoutMs: number
156
+ /** local-backend: the opencode CLI binary (cli-bridge fallback when the sandbox is down). */
157
+ opencodeBin: string
158
+ }
159
+
160
+ /** The diff the in-box agent produces, read back off the box FS (+ any stream error). */
161
+ interface RolloutDeliverable {
162
+ diff: string
163
+ lastErr?: string
164
+ }
165
+
166
+ /** Reads the patch FILE the agent wrote (the robust deliverable — a large diff
167
+ * truncates in the chat stream → `git apply` "corrupt patch"), folding any in-box
168
+ * error event into `lastErr` so a failed rollout still surfaces on an empty patch. */
169
+ const commit0Deliverable: Deliverable<RolloutDeliverable> = {
170
+ kind: 'artifact',
171
+ path: PATCH_PATH,
172
+ fromArtifact: (raw, events) => {
173
+ let lastErr: string | undefined
174
+ for (const ev of events) {
175
+ if ((ev as { type?: string }).type === 'error') lastErr = JSON.stringify((ev as { data?: unknown }).data).slice(0, 300)
176
+ }
177
+ return { diff: raw, ...(lastErr ? { lastErr } : {}) }
178
+ },
179
+ }
180
+
181
+ async function runShot(task: BenchTask, attempt: number, cfg: ShotCfg, steer?: string): Promise<Shot> {
182
+ const meta = task.metadata as unknown as Commit0Meta
183
+ const startedAt = Date.now()
184
+ const client = new Sandbox({ baseUrl: cfg.sandboxBaseUrl, apiKey: cfg.sandboxKey })
185
+ // A stream/transport ceiling for the flaky sandbox path (0 ⇒ untimed); the run
186
+ // tears its own box down in `close()`. The whole rollout is fault-isolated: ANY
187
+ // error (502 / stream drop / provision fail / abort) becomes a recorded NO-DIFF
188
+ // attempt — it MUST NOT throw, or one flaky box aborts the pool and loses every
189
+ // other rollout (the powered-run crash).
190
+ const controller = new AbortController()
191
+ const timer = cfg.timeoutMs > 0 ? setTimeout(() => controller.abort(), cfg.timeoutMs) : undefined
192
+ // backend.model pins provider/model/baseUrl only; the platform writes the in-box
193
+ // provider config keyed to the box's own OPENCODE_MODEL_API_KEY. The inline
194
+ // profile + backend override is the same generic AgentRunSpec the runLoop kernel
195
+ // boots. Never inject an external key — the egress proxy 403s foreign credentials.
196
+ const agentRun: AgentRunSpec<string> = {
197
+ profile: { name: 'commit0-worker', metadata: { backendType: 'opencode' } },
198
+ name: 'commit0-worker',
199
+ taskToPrompt: () => '', // unused — the prompt is streamed directly by openSandboxRun
200
+ sandboxOverrides: {
201
+ name: `commit0-${task.id}-${attempt}-${randomSuffix()}`.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 60),
202
+ environment: 'universal',
203
+ backend: {
204
+ type: 'opencode',
205
+ model: { provider: cfg.provider, model: cfg.model, baseUrl: cfg.routerBaseUrl },
206
+ },
207
+ },
208
+ }
209
+ let run: SandboxRun<RolloutDeliverable> | undefined
210
+ const runtime = createRuntimeHookRecorder()
211
+ try {
212
+ run = await openSandboxRun(
213
+ client,
214
+ {
215
+ agentRun,
216
+ signal: controller.signal,
217
+ hooks: runtime.hooks,
218
+ runId: `commit0:${task.id}:${attempt}`,
219
+ scenarioId: task.id,
220
+ },
221
+ commit0Deliverable,
222
+ )
223
+ const prompt = steer ? steeredPrompt(rolloutPrompt(meta), steer) : rolloutPrompt(meta)
224
+ const turn = await run.start(prompt)
225
+ const ok = turn.out.diff.trim().length > 0
226
+ return {
227
+ task,
228
+ attempt,
229
+ diff: turn.out.diff,
230
+ ok,
231
+ events: turn.events.length,
232
+ traceEvents: turn.events.slice(-TRACE_EVENTS_TAIL),
233
+ ...(steer ? { steer } : {}),
234
+ runtimeEvents: runtime.events,
235
+ runtimeDecisionPoints: runtime.decisionPoints,
236
+ wallMs: Date.now() - startedAt,
237
+ ...(ok ? {} : { detail: `empty patch${turn.readError ? ` (read failed: ${turn.readError.slice(0, 120)})` : ''}${turn.out.lastErr ? `; lastError=${turn.out.lastErr}` : ''}` }),
238
+ }
239
+ } catch (err) {
240
+ const msg = err instanceof Error ? err.message : String(err)
241
+ return {
242
+ task,
243
+ attempt,
244
+ diff: '',
245
+ ok: false,
246
+ events: 0,
247
+ ...(steer ? { steer } : {}),
248
+ runtimeEvents: runtime.events,
249
+ runtimeDecisionPoints: runtime.decisionPoints,
250
+ wallMs: Date.now() - startedAt,
251
+ detail: `rollout error: ${msg.slice(0, 200)}`,
252
+ }
253
+ } finally {
254
+ if (timer) clearTimeout(timer)
255
+ if (run) await run.close()
256
+ }
257
+ }
258
+
259
+ /** Run a subprocess, capturing combined stdout+stderr; never throws (returns rc). */
260
+ function sh(cmd: string, args: string[], opts: { cwd?: string; timeoutMs?: number; env?: NodeJS.ProcessEnv } = {}): Promise<{ code: number; out: string }> {
261
+ return new Promise((resolve) => {
262
+ const child = spawn(cmd, args, {
263
+ ...(opts.cwd ? { cwd: opts.cwd } : {}),
264
+ env: opts.env ?? process.env,
265
+ ...(opts.timeoutMs ? { timeout: opts.timeoutMs } : {}),
266
+ })
267
+ let out = ''
268
+ child.stdout?.on('data', (c: Buffer) => { out += c.toString() })
269
+ child.stderr?.on('data', (c: Buffer) => { out += c.toString() })
270
+ child.on('error', (e) => resolve({ code: -1, out: `${out}\n${e}` }))
271
+ child.on('close', (code) => resolve({ code: code ?? -1, out }))
272
+ })
273
+ }
274
+
275
+ /** Local-rollout prompt: the repo is ALREADY cloned + checked out into the cwd, so
276
+ * (unlike the sandbox prompt) the agent implements + test-iterates in place; the diff
277
+ * is read from git afterward (no chat-message truncation). */
278
+ function localRolloutPrompt(meta: Commit0Meta): string {
279
+ return [
280
+ `The current directory is the stubbed Python library \`${meta.repo}\`, checked out at its base commit.`,
281
+ `The public functions/classes under \`${meta.srcDir}\` are stubbed (empty \`pass\`/\`...\`). Implement COMPLETE, CORRECT bodies so the tests under \`${meta.testDir}\` pass.`,
282
+ 'Work iteratively, do NOT stop at a first draft:',
283
+ `1. Read the spec (${meta.specification}) and the tests under \`${meta.testDir}\`.`,
284
+ '2. Set up an ISOLATED venv so imports + pytest resolve: `python3 -m venv .venv && .venv/bin/pip install -e .` (use the repo setup if it differs).',
285
+ `3. Implement ALL stubbed bodies under \`${meta.srcDir}\`.`,
286
+ `4. Run \`.venv/bin/python -m pytest ${meta.testDir} -q\`, read failures, FIX them, and repeat until as many tests pass as you can.`,
287
+ '5. Do NOT edit the test files.',
288
+ 'When done, just stop — do NOT print the diff; it is collected from git.',
289
+ ].join('\n')
290
+ }
291
+
292
+ /**
293
+ * LOCAL rollout backend (cli-bridge fallback for when the sandbox gateway is down):
294
+ * clone + checkout the stub into a tmpdir, run local opencode (its own kimi/zai
295
+ * coding-plan auth, or the router when model is `openai/*`) to implement + test-iterate
296
+ * in place, then read the diff straight from git (complete — no message truncation).
297
+ * Fault-isolated like runShot: any failure → a recorded NO-DIFF attempt, never a throw.
298
+ */
299
+ async function runShotLocal(task: BenchTask, attempt: number, cfg: ShotCfg, steer?: string): Promise<Shot> {
300
+ const meta = task.metadata as unknown as Commit0Meta
301
+ const startedAt = Date.now()
302
+ let dir: string | undefined
303
+ try {
304
+ dir = await mkdtemp(join(tmpdir(), 'commit0-local-'))
305
+ const clone = await sh('git', ['clone', '--quiet', `https://github.com/${meta.repo}`, dir], { timeoutMs: 180_000 })
306
+ if (clone.code !== 0) {
307
+ return { task, attempt, diff: '', ok: false, events: 0, wallMs: Date.now() - startedAt, detail: `git clone failed: ${clone.out.trim().slice(-180)}` }
308
+ }
309
+ const co = await sh('git', ['-C', dir, 'checkout', '--quiet', meta.baseCommit], { timeoutMs: 60_000 })
310
+ if (co.code !== 0) {
311
+ return { task, attempt, diff: '', ok: false, events: 0, wallMs: Date.now() - startedAt, detail: `git checkout ${meta.baseCommit} failed: ${co.out.trim().slice(-180)}` }
312
+ }
313
+ // openai/* → route through the router (OPENAI_* env); anything else → opencode's
314
+ // OWN configured auth (kimi-for-coding / zai coding-plan subscriptions).
315
+ const env = cfg.model.startsWith('openai/')
316
+ ? { ...process.env, OPENAI_API_KEY: cfg.routerKey, OPENAI_BASE_URL: cfg.routerBaseUrl }
317
+ : process.env
318
+ const prompt = steer ? steeredPrompt(localRolloutPrompt(meta), steer) : localRolloutPrompt(meta)
319
+ const oc = await sh(cfg.opencodeBin, ['run', prompt, '-m', cfg.model, '--dir', dir], { timeoutMs: cfg.timeoutMs, env })
320
+ const lines = oc.out.split('\n')
321
+ const events = lines.length
322
+ // Read the diff straight from git, scoped to src_dir (excludes the .venv the agent made).
323
+ const diffRes = await sh('bash', ['-c', `cd ${JSON.stringify(dir)} && git add -- ${JSON.stringify(meta.srcDir)} && git diff --cached -- ${JSON.stringify(meta.srcDir)}`], { timeoutMs: 60_000 })
324
+ const diff = diffRes.out
325
+ const ok = diff.trim().length > 0
326
+ return {
327
+ task,
328
+ attempt,
329
+ diff,
330
+ ok,
331
+ events,
332
+ traceEvents: lines.slice(-TRACE_EVENTS_TAIL),
333
+ ...(steer ? { steer } : {}),
334
+ wallMs: Date.now() - startedAt,
335
+ ...(ok ? {} : { detail: `no diff (opencode rc=${oc.code}): ${oc.out.trim().slice(-160)}` }),
336
+ }
337
+ } catch (err) {
338
+ return { task, attempt, diff: '', ok: false, events: 0, ...(steer ? { steer } : {}), wallMs: Date.now() - startedAt, detail: `local rollout error: ${(err instanceof Error ? err.message : String(err)).slice(0, 180)}` }
339
+ } finally {
340
+ if (dir) await rm(dir, { recursive: true, force: true }).catch(() => {})
341
+ }
342
+ }
343
+
344
+ async function main(): Promise<void> {
345
+ // BACKEND=local → cli-bridge fallback (local opencode, no remote sandbox); needs a
346
+ // sandbox-down workaround. Default 'sandbox' (the remote gateway). Local uses opencode's
347
+ // OWN auth (kimi-for-coding / zai coding-plan), so TANGLE_API_KEY is only required for
348
+ // the sandbox backend or an `openai/*` local model (router).
349
+ const backend = process.env.COMMIT0_BACKEND === 'local' ? 'local' : 'sandbox'
350
+ const n = Number(process.env.N ?? 8)
351
+ const k = Number(process.env.K ?? 4)
352
+ const model = process.env.WORKER_MODEL ?? (backend === 'local' ? 'kimi-for-coding/kimi-k2-thinking' : 'gpt-4.1')
353
+ const routerBaseUrl = process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1'
354
+ // The arms under test. `random` = K independent blind shots (the equal-compute
355
+ // control); `refineAudit` = blind shot 0, then trace-only-analyst-steered shots.
356
+ const armNames = (process.env.ARMS ?? 'random').split(',').map((s) => s.trim()).filter(Boolean)
357
+ for (const a of armNames) {
358
+ if (a !== 'random' && a !== 'refineAudit') throw new Error(`unknown arm ${a} (have: random, refineAudit)`)
359
+ }
360
+ // The analyst is an OFF-BOX router call (host-side), so it needs the router key
361
+ // even when the worker runs on its own auth.
362
+ const analystModel = process.env.ANALYST_MODEL ?? 'deepseek-v4-pro'
363
+ const needsRouterKey = backend === 'sandbox' || model.startsWith('openai/') || armNames.includes('refineAudit')
364
+ const routerKey = needsRouterKey ? must('TANGLE_API_KEY') : (process.env.TANGLE_API_KEY ?? '')
365
+ const sandboxBaseUrl = process.env.SANDBOX_BASE_URL ?? 'https://sandbox.tangle.tools'
366
+ const opencodeBin = process.env.OPENCODE_BIN ?? join(process.env.HOME ?? '', '.local/bin/opencode')
367
+ // openai-compat = generic passthrough so cheap router models resolve in-box;
368
+ // `openai` rejects non-registered model names. Override via WORKER_PROVIDER.
369
+ const provider = process.env.WORKER_PROVIDER ?? 'openai-compat'
370
+ const concurrency = Number(process.env.CONCURRENCY ?? 3)
371
+ // No tight cap on the agentic rollout — it runs until the agent finishes (the clone→
372
+ // implement→pytest-iterate loop genuinely takes a while). 0 = untimed. Only set
373
+ // SHOT_TIMEOUT_MS to impose a deliberate ceiling. Sandbox keeps a stream cap (flaky transport).
374
+ const timeoutMs = process.env.SHOT_TIMEOUT_MS ? Number(process.env.SHOT_TIMEOUT_MS) : backend === 'local' ? 0 : 900_000
375
+ const corpusPath = process.env.CORPUS ?? '/tmp/commit0.jsonl'
376
+ if (!Number.isInteger(n) || n < 1) throw new Error(`N must be a positive integer, got ${process.env.N}`)
377
+ if (!Number.isInteger(k) || k < 1) throw new Error(`K must be a positive integer, got ${process.env.K}`)
378
+
379
+ const adapter = createCommit0Adapter()
380
+ console.log(`=== commit0 Layer-1 gate · backend=${backend} · N=${n} K=${k} arms=${armNames.join(',')} model=${model} analyst=${analystModel} rolloutConc=${concurrency} ===`)
381
+ await adapter.preflight()
382
+ const tasks = await adapter.loadTasks({ limit: n })
383
+ console.log(`loaded ${tasks.length} task(s): ${tasks.map((t) => t.id).join(', ')}`)
384
+
385
+ // Phase 1 — rollouts, concurrent. sandbox = remote box; local = cli-bridge (opencode
386
+ // in a tmpdir, diff read from git). Both fault-isolated → a failure is a NO-DIFF, never a throw.
387
+ const cfg: ShotCfg = { sandboxBaseUrl, sandboxKey: routerKey, routerBaseUrl, routerKey, model, provider, timeoutMs, opencodeBin }
388
+ const runRollout = backend === 'local' ? runShotLocal : runShot
389
+ const analyze: AnalystFn = llmAnalyst({ routerBaseUrl, routerKey, model: analystModel })
390
+ const logShot = (armName: string, s: Shot) =>
391
+ console.log(` rollout ${s.task.id}[${armName}]#${s.attempt}: ${s.ok ? `diff ${s.diff.length}B` : `NO DIFF (${s.detail})`}${s.steer ? ' [steered]' : ''} (${(s.wallMs / 1000) | 0}s)`)
392
+
393
+ interface TaggedShot {
394
+ arm: string
395
+ shot: Shot
396
+ }
397
+ // A unit is the pool's schedulable atom: one blind shot for the control arm
398
+ // (max packing), the WHOLE blind→analyst→steered chain for the steered arm
399
+ // (shot i+1 depends on shot i's trace). Units never throw — every failure is a
400
+ // recorded NO-DIFF/INFRA shot, so one flaky box cannot abort the pool.
401
+ const units: Array<() => Promise<TaggedShot[]>> = []
402
+ for (const task of tasks) {
403
+ for (const armName of armNames) {
404
+ if (armName === 'random') {
405
+ for (let attempt = 0; attempt < k; attempt += 1) {
406
+ units.push(async () => {
407
+ const s = await runRollout(task, attempt, cfg)
408
+ logShot(armName, s)
409
+ return [{ arm: armName, shot: s }]
410
+ })
411
+ }
412
+ continue
413
+ }
414
+ units.push(async () => {
415
+ const out: TaggedShot[] = []
416
+ let prev: Shot | undefined
417
+ for (let attempt = 0; attempt < k; attempt += 1) {
418
+ let steer: string | undefined
419
+ if (prev) {
420
+ // Trace-only: the analyst sees the prior shot's diff + stream tail,
421
+ // never a judge score (judging is phase 2 — selector≠judge by
422
+ // construction). "no change needed" is the policy's informed no-op.
423
+ try {
424
+ const events = prev.traceEvents?.length ? prev.traceEvents : prev.detail ? [prev.detail] : []
425
+ const feedback = (await analyze([{ output: prev.diff.slice(0, ANALYST_DIFF_MAX), events }])).trim()
426
+ if (feedback && !/^no change needed/i.test(feedback)) steer = feedback.slice(0, STEER_MAX)
427
+ } catch (err) {
428
+ // A steered arm whose analyst died is no longer a steered arm —
429
+ // record the attempt as INFRA rather than degrading to blind.
430
+ const msg = (err instanceof Error ? err.message : String(err)).slice(0, 200)
431
+ const s: Shot = { task, attempt, diff: '', ok: false, events: 0, wallMs: 0, detail: `analyst error: ${msg}` }
432
+ logShot(armName, s)
433
+ out.push({ arm: armName, shot: s })
434
+ break
435
+ }
436
+ }
437
+ const s = await runRollout(task, attempt, cfg, steer)
438
+ logShot(armName, s)
439
+ out.push({ arm: armName, shot: s })
440
+ prev = s
441
+ }
442
+ return out
443
+ })
444
+ }
445
+ }
446
+ const where = backend === 'local' ? 'local opencode (cli-bridge)' : `in-box (${PATCH_PATH})`
447
+ console.log(`\n▶ phase 1: ${tasks.length * armNames.length * k} rollouts in ${units.length} units (conc=${concurrency}) via ${where}`)
448
+ const tagged = (await pool(units, concurrency, (u) => u())).flat()
449
+
450
+ // Phase 2 — judging, SEQUENTIAL (Docker-bound; commit0 keys its report dir on
451
+ // hash(test_ids), shared across a repo's attempts → concurrent judging of one repo
452
+ // races/overwrites report.json). Judged PER (TASK, ARM), writing each RunRecord
453
+ // immediately so a mid-run crash keeps completed records. Fail loud: an attempt
454
+ // with no diff or a failed judge becomes an INFRA attempt (error recorded, no
455
+ // score) and the whole record is infra-excluded — never a silent 0.
456
+ console.log(`\n▶ phase 2: judging sequentially per task (official commit0 pytest harness) → ${corpusPath}`)
457
+ let scoredRecords = 0
458
+ let infraRecords = 0
459
+ let infraAttempts = 0
460
+ for (const task of tasks) {
461
+ let imageTouched = false
462
+ for (const armName of armNames) {
463
+ const armShots = tagged.filter((t) => t.arm === armName && t.shot.task.id === task.id).map((t) => t.shot)
464
+ const attempts: AttemptRecord[] = []
465
+ let recordInfra = false
466
+ for (let i = 0; i < k; i += 1) {
467
+ const s = armShots.find((x) => x.attempt === i)
468
+ let sc: { score: number; resolved: boolean } | undefined
469
+ let attemptError: string | undefined
470
+ if (s?.ok) {
471
+ // Local Docker backend: images must pre-exist (src/commit0-prereqs.sh);
472
+ // rebuild_image is honored by the Modal backend only, where the first
473
+ // judged attempt of a repo force-builds and the rest reuse.
474
+ process.env.COMMIT0_REBUILD_IMAGE = imageTouched ? '0' : '1'
475
+ imageTouched = true
476
+ try {
477
+ const v = await adapter.judge(s.task, s.diff)
478
+ sc = { score: v.score, resolved: v.resolved }
479
+ console.log(` judge ${task.id}[${armName}]#${i}: score=${(v.score * 100).toFixed(1)}% resolved=${v.resolved}`)
480
+ } catch (err) {
481
+ attemptError = `judge harness failed: ${(err instanceof Error ? err.message : String(err)).slice(0, 300)}`
482
+ console.log(` judge ${task.id}[${armName}]#${i}: INFRA ${attemptError.slice(0, 200)}`)
483
+ }
484
+ } else {
485
+ attemptError = s ? `no diff: ${s.detail ?? 'unknown'}` : 'missing shot'
486
+ console.log(` judge ${task.id}[${armName}]#${i}: INFRA (${attemptError.slice(0, 160)})`)
487
+ }
488
+ if (attemptError) {
489
+ recordInfra = true
490
+ infraAttempts += 1
491
+ }
492
+ attempts.push({
493
+ round: i,
494
+ prompt: s?.steer ? `commit0-rollout + steer:\n${s.steer}` : 'commit0-rollout',
495
+ output: s?.diff ?? '',
496
+ ...(sc ? { valid: sc.resolved, score: sc.score } : {}),
497
+ ...(attemptError ? { error: attemptError } : {}),
498
+ wallMs: s?.wallMs ?? 0,
499
+ eventCount: s?.events ?? 0,
500
+ eventTypes: { 'sandbox.stream': s?.events ?? 0 },
501
+ traceTail: (s?.diff ?? '').slice(-600),
502
+ })
503
+ }
504
+ if (recordInfra) infraRecords += 1
505
+ else if (attempts.some((a) => a.score !== undefined)) scoredRecords += 1
506
+ const record = buildRunRecordFromAttempts(attempts, {
507
+ benchmark: adapter.name,
508
+ instanceId: task.id,
509
+ condition: `${armName}@${k}`,
510
+ model,
511
+ infraError: recordInfra,
512
+ runtimeEvents: armShots.flatMap((x) => x.runtimeEvents ?? []),
513
+ runtimeDecisionPoints: armShots.flatMap((x) => x.runtimeDecisionPoints ?? []),
514
+ })
515
+ await appendRunRecord(corpusPath, record) // incremental: partial progress survives a crash
516
+ }
517
+ }
518
+
519
+ console.log(
520
+ `\n=== wrote ${tasks.length * armNames.length} record(s) (${scoredRecords} fully scored, ${infraRecords} infra-excluded; ${infraAttempts} infra attempts) → ${corpusPath} ===\n` +
521
+ ` STEERING VERDICT: tsx src/corpus-report.mts ${corpusPath}\n` +
522
+ ` SELECTOR GATE: tsx src/corpus-replay.mts ${corpusPath} --selector=verifier`,
523
+ )
524
+ }
525
+
526
+ main().catch((err) => {
527
+ console.error(`commit0-gate: ${err instanceof Error ? err.message : String(err)}`)
528
+ process.exit(1)
529
+ })
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ # commit0 judge prerequisites: ensure the per-repo Docker image exists for each
3
+ # argument — a repo short name (`wcwidth`, `tinydb`, …) or a split (`lite`, `all`).
4
+ #
5
+ # The judge (scripts/commit0_judge.py) grades on commit0's LOCAL Docker backend,
6
+ # whose execution context only CREATES containers from an existing image —
7
+ # `rebuild_image` is honored by the Modal backend only. Without the image every
8
+ # judge call fails with ImageNotFound ("commit0 harness failed"), so images must
9
+ # exist BEFORE a gate run.
10
+ #
11
+ # Fast path: pull the official prebuilt image (docker.io/wentingzhao/<repo>:v0 —
12
+ # the exact tag commit0's spec.repo_image_tag resolves). Fallback: build locally
13
+ # via commit0.harness.build (minutes + multi-GB per repo; skips existing images).
14
+ #
15
+ # ./src/commit0-prereqs.sh wcwidth tinydb # individual repos
16
+ # ./src/commit0-prereqs.sh lite # a whole split
17
+ # COMMIT0_VENV=.venv-commit0 ./src/commit0-prereqs.sh wcwidth
18
+ set -euo pipefail
19
+ cd "$(dirname "$0")/.." # bench/
20
+
21
+ venv="${COMMIT0_VENV:-.venv-commit0}"
22
+ dataset="${COMMIT0_DATASET:-wentingzhao/commit0_combined}"
23
+ split="${COMMIT0_SPLIT:-test}"
24
+
25
+ [ $# -ge 1 ] || { echo "usage: $0 <repo-or-split> [more…]" >&2; exit 2; }
26
+ [ -x "$venv/bin/python" ] || { echo "missing $venv/bin/python — create it per benchmarks/commit0.ts preflight (pip install commit0 datasets)" >&2; exit 2; }
27
+ docker info >/dev/null 2>&1 || { echo "Docker daemon unavailable — commit0 judges on --backend local" >&2; exit 2; }
28
+
29
+ for target in "$@"; do
30
+ image="wentingzhao/$(printf '%s' "$target" | tr '[:upper:]' '[:lower:]'):v0"
31
+ if docker image inspect "$image" >/dev/null 2>&1; then
32
+ echo "ok: $image already present"
33
+ continue
34
+ fi
35
+ if docker pull "$image" >/dev/null 2>&1; then
36
+ echo "ok: pulled $image"
37
+ continue
38
+ fi
39
+ # No hub image (or the target is a split name): build through the harness,
40
+ # which skips repos whose image already exists.
41
+ echo "building $target via commit0.harness.build (this takes minutes per repo)…"
42
+ "$venv/bin/python" -c '
43
+ import sys
44
+ from commit0.harness.build import main
45
+ main(sys.argv[1], sys.argv[2], sys.argv[3], 1, 1)
46
+ ' "$dataset" "$split" "$target"
47
+ done
48
+ echo "prereqs done."