@tangle-network/agent-bench 0.3.6 → 0.3.7

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 (91) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/adapters.js +2 -2
  3. package/dist/benchmarks/humaneval.d.ts +10 -1
  4. package/dist/benchmarks/humaneval.js +5 -3
  5. package/dist/{chunk-PPYSEKFM.js → chunk-5H5XV76F.js} +73 -15
  6. package/dist/chunk-5H5XV76F.js.map +1 -0
  7. package/dist/{chunk-5SBJCB6W.js → chunk-PWQVGAJB.js} +2 -2
  8. package/dist/index.js +2 -2
  9. package/package.json +5 -4
  10. package/scripts/run-package-tests.mjs +30 -8
  11. package/scripts/verify-pier-agent.mts +1 -0
  12. package/src/benchmarks/humaneval.test.mts +122 -0
  13. package/src/benchmarks/humaneval.ts +100 -27
  14. package/src/david-attribution.mts +78 -0
  15. package/src/david-goliath.mts +149 -0
  16. package/src/hev-improve.mts +25 -6
  17. package/src/humaneval-object-ablation.mts +201 -0
  18. package/src/live-improve-campaign-mbpp.mts +641 -0
  19. package/src/live-improve-campaign.mts +500 -0
  20. package/src/mbpp-structural.mts +12 -7
  21. package/src/stream-observe.py +45 -0
  22. package/src/stream-observe.tpl.html +247 -0
  23. package/src/supervisor-arena.mts +816 -0
  24. package/src/swe-arena/analyze.ts +211 -0
  25. package/src/swe-arena/arms.ts +788 -0
  26. package/src/swe-arena/bootstrap-meta.mts +188 -0
  27. package/src/swe-arena/bootstrap-meta.test.mts +51 -0
  28. package/src/swe-arena/calibrate.ts +116 -0
  29. package/src/swe-arena/capabilities.mts +76 -0
  30. package/src/swe-arena/capabilities.test.mts +57 -0
  31. package/src/swe-arena/capacity.ts +194 -0
  32. package/src/swe-arena/cell-evidence.mts +405 -0
  33. package/src/swe-arena/cell-evidence.test.mts +248 -0
  34. package/src/swe-arena/diagnosis-ensemble.test.mts +210 -0
  35. package/src/swe-arena/diagnosis-ensemble.ts +520 -0
  36. package/src/swe-arena/execution.test.mts +1170 -0
  37. package/src/swe-arena/fixtures/analyze.py +80 -0
  38. package/src/swe-arena/fixtures/excludes.txt +8 -0
  39. package/src/swe-arena/fixtures/gen1-salvage/README.md +45 -0
  40. package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +116 -0
  41. package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +293 -0
  42. package/src/swe-arena/fixtures/holdout-preregister.log +12 -0
  43. package/src/swe-arena/fixtures/holdout.json +44 -0
  44. package/src/swe-arena/fixtures/instances.json +146 -0
  45. package/src/swe-arena/fixtures/ledger.jsonl +12 -0
  46. package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +36 -0
  47. package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +33 -0
  48. package/src/swe-arena/fixtures/rejudge.jsonl +15 -0
  49. package/src/swe-arena/fixtures/rematch.jsonl +3 -0
  50. package/src/swe-arena/fixtures/rematch2.jsonl +3 -0
  51. package/src/swe-arena/fixtures/rematch3.jsonl +3 -0
  52. package/src/swe-arena/fixtures/sup-journal-true.json +19 -0
  53. package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +48 -0
  54. package/src/swe-arena/fixtures/verify/django__django-11532.sh +50 -0
  55. package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +76 -0
  56. package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +44 -0
  57. package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +32 -0
  58. package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +51 -0
  59. package/src/swe-arena/fixtures/worker-tokens.json +42 -0
  60. package/src/swe-arena/fixtures.ts +104 -0
  61. package/src/swe-arena/holdout-certify.mts +408 -0
  62. package/src/swe-arena/holdout-certify.test.mts +160 -0
  63. package/src/swe-arena/judge-child.mts +37 -0
  64. package/src/swe-arena/manifest.mts +293 -0
  65. package/src/swe-arena/manifest.test.mts +169 -0
  66. package/src/swe-arena/materialize.ts +142 -0
  67. package/src/swe-arena/outer-loop.mts +2145 -0
  68. package/src/swe-arena/outer-loop.test.mts +696 -0
  69. package/src/swe-arena/parity.test.mts +87 -0
  70. package/src/swe-arena/proc.test.mts +174 -0
  71. package/src/swe-arena/proc.ts +260 -0
  72. package/src/swe-arena/profiles/default-author.profile.json +4 -0
  73. package/src/swe-arena/proposer-fanout.mts +489 -0
  74. package/src/swe-arena/proposer-fanout.test.mts +372 -0
  75. package/src/swe-arena/reconcile.ts +0 -0
  76. package/src/swe-arena/replay.mts +183 -0
  77. package/src/swe-arena/replay.test.mts +300 -0
  78. package/src/swe-arena/run-experiment.mts +361 -0
  79. package/src/swe-arena/run-supervisor.mjs +297 -0
  80. package/src/swe-arena/run-supervisor.test.mts +498 -0
  81. package/src/swe-arena/serialized-judge.ts +414 -0
  82. package/src/swe-arena/types.ts +166 -0
  83. package/src/swe-code-improve.mts +328 -0
  84. package/src/swe-emit-patch.mts +104 -0
  85. package/src/swe-improve.mts +232 -0
  86. package/src/swe-jail.ts +2 -2
  87. package/src/swe-local-proof.mts +169 -0
  88. package/src/swe-repro-calibrate.mts +446 -0
  89. package/src/swe-stream.mts +1497 -0
  90. package/dist/chunk-PPYSEKFM.js.map +0 -1
  91. /package/dist/{chunk-5SBJCB6W.js.map → chunk-PWQVGAJB.js.map} +0 -0
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Serialized official judge — the typed port of the experiment's `judge.sh`,
3
+ * wrapping `adapter.judge` (via the tracked judge-child.mts) with the three
4
+ * protections the bash experiment had to learn the hard way:
5
+ *
6
+ * 1. ONE JUDGE AT A TIME. The stale-container pre-clean does `docker rm -f`
7
+ * by instance short-name; two overlapping judges of the same instance nuke
8
+ * each other's container mid-run and each records a spurious
9
+ * `resolved:false` (proven false-negative on psf__requests-1766). The lock
10
+ * here is two-layer: an in-process promise chain plus a kernel-backed
11
+ * `flock`, so the pre-clean can only ever remove a genuinely stale
12
+ * container — never a live grade.
13
+ *
14
+ * 2. CEILING ≥ 1800s. The original 700s ceiling caused 3 spurious timeouts on
15
+ * psf__requests-2317 (a successful grade took 1181s). The floor is
16
+ * enforced, not advisory: a shorter ceiling for the REAL judge is a
17
+ * protocol violation and throws.
18
+ *
19
+ * 3. ONE RETRY on empty/unparseable judge output (single-run judge flake was
20
+ * observed live: byte-identical patches split-verdicted). A second failure
21
+ * returns `resolved: null` — inconclusive, never a fabricated verdict —
22
+ * matching the RejudgeRow semantics M1 pinned.
23
+ */
24
+
25
+ import { spawn } from 'node:child_process'
26
+ import { open, readFile, type FileHandle } from 'node:fs/promises'
27
+ import { tmpdir } from 'node:os'
28
+ import { join } from 'node:path'
29
+ import { fileURLToPath } from 'node:url'
30
+ import { run } from './proc'
31
+
32
+ /** Verdict shape — field names match the experiment's JUDGE_RESULT/RejudgeRow rows. */
33
+ export interface JudgeVerdict {
34
+ iid: string
35
+ /** `null` = inconclusive (judge failed twice); never a guessed boolean. */
36
+ resolved: boolean | null
37
+ score?: number
38
+ secs?: number
39
+ patch_bytes?: number
40
+ note?: string
41
+ error?: string
42
+ /** First 200 chars of the failing output, for post-mortem (mirrors rejudge rows). */
43
+ raw?: string
44
+ /** How many judge child runs it took (1 = clean, 2 = retried). */
45
+ attempts?: number
46
+ }
47
+
48
+ export interface JudgeCommand {
49
+ bin: string
50
+ argv: string[]
51
+ cwd: string
52
+ }
53
+
54
+ export interface SerializedJudgeOptions {
55
+ /**
56
+ * Hard ceiling per judge child. Default 1_800_000 ms; values below the floor
57
+ * throw unless `unsafeAllowShortTimeout` (test hook) is set.
58
+ */
59
+ timeoutMs?: number
60
+ /** Maximum time spent waiting to acquire both judge locks. */
61
+ lockWaitTimeoutMs?: number
62
+ /** Hard ceiling for each stale-container pre-clean command. */
63
+ preCleanTimeoutMs?: number
64
+ /** Test-only escape hatch for the 1800s floor. NEVER set on a real judge. */
65
+ unsafeAllowShortTimeout?: boolean
66
+ /** Cross-process kernel lock file. One per docker daemon. */
67
+ lockFile?: string
68
+ /** SWEBENCH_CACHE_LEVEL for the child. `instance` = we manage image rotation. */
69
+ cacheLevel?: string
70
+ /** Override the judge child invocation (tests inject fakes here). */
71
+ command?: (iid: string, patchPath: string) => JudgeCommand
72
+ }
73
+
74
+ export const JUDGE_TIMEOUT_FLOOR_MS = 1_800_000
75
+ export const JUDGE_PRE_CLEAN_TIMEOUT_MS = 60_000
76
+ /** Process-group settlement and scheduler margin beyond all command ceilings. */
77
+ export const JUDGE_LOCK_SETTLEMENT_MARGIN_MS = 30_000
78
+ /** One live holder may use two attempts, each with docker ps + docker rm. */
79
+ export const JUDGE_LOCK_WAIT_TIMEOUT_MS =
80
+ 2 * (JUDGE_TIMEOUT_FLOOR_MS + 2 * JUDGE_PRE_CLEAN_TIMEOUT_MS) + JUDGE_LOCK_SETTLEMENT_MARGIN_MS
81
+
82
+ const benchRoot = fileURLToPath(new URL('../..', import.meta.url))
83
+ const judgeChild = fileURLToPath(new URL('./judge-child.mts', import.meta.url))
84
+
85
+ const defaultCommand = (iid: string, patchPath: string): JudgeCommand => ({
86
+ bin: 'node',
87
+ argv: ['--import', 'tsx', judgeChild, iid, patchPath],
88
+ cwd: benchRoot,
89
+ })
90
+
91
+ /** judge.sh's `sed 's/.*__//'` — the docker container filter for pre-clean. */
92
+ export function instanceShortName(iid: string): string {
93
+ return iid.replace(/.*__/, '')
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Locking: in-process promise chain + cross-process kernel flock.
98
+ // ---------------------------------------------------------------------------
99
+
100
+ let inProcessChain: Promise<unknown> = Promise.resolve()
101
+
102
+ export interface JudgeLockOptions {
103
+ /** Maximum total wait across the in-process queue and kernel lock. */
104
+ timeoutMs?: number
105
+ signal?: AbortSignal
106
+ }
107
+
108
+ function abortReason(signal: AbortSignal): Error {
109
+ if (signal.reason instanceof Error) return signal.reason
110
+ const error = new Error('serialized-judge: aborted while waiting for the judge lock')
111
+ error.name = 'AbortError'
112
+ return error
113
+ }
114
+
115
+ function throwIfAborted(signal?: AbortSignal): void {
116
+ if (signal?.aborted) throw abortReason(signal)
117
+ }
118
+
119
+ function lockTimeoutError(timeoutMs: number): Error {
120
+ return new Error(`serialized-judge: lock wait exceeded ${timeoutMs}ms`)
121
+ }
122
+
123
+ function waitFor<T>(promise: Promise<T>, deadline: number, timeoutMs: number, signal?: AbortSignal): Promise<T> {
124
+ throwIfAborted(signal)
125
+ const remainingMs = deadline - Date.now()
126
+ if (remainingMs <= 0) return Promise.reject(lockTimeoutError(timeoutMs))
127
+
128
+ return new Promise<T>((resolve, reject) => {
129
+ let settled = false
130
+ const finish = (callback: () => void) => {
131
+ if (settled) return
132
+ settled = true
133
+ clearTimeout(timer)
134
+ signal?.removeEventListener('abort', onAbort)
135
+ callback()
136
+ }
137
+ const onAbort = () => finish(() => reject(abortReason(signal!)))
138
+ const timer = setTimeout(() => finish(() => reject(lockTimeoutError(timeoutMs))), remainingMs)
139
+ signal?.addEventListener('abort', onAbort, { once: true })
140
+ if (signal?.aborted) onAbort()
141
+ promise.then(
142
+ (value) => finish(() => resolve(value)),
143
+ (error: unknown) => finish(() => reject(error)),
144
+ )
145
+ })
146
+ }
147
+
148
+ /**
149
+ * Ask util-linux `flock` to lock an fd inherited from this process. Linux
150
+ * associates a flock with the open file description, so the lock remains held
151
+ * by `handle` after the short helper exits and is released atomically on close.
152
+ * There is no owner record to initialize, inspect, or unlink: process death and
153
+ * fd close are the only stale-owner recovery path, both enforced by the kernel.
154
+ */
155
+ function tryAcquireFlock(
156
+ handle: FileHandle,
157
+ deadline: number,
158
+ timeoutMs: number,
159
+ signal?: AbortSignal,
160
+ ): Promise<boolean> {
161
+ throwIfAborted(signal)
162
+ const remainingMs = deadline - Date.now()
163
+ if (remainingMs <= 0) return Promise.reject(lockTimeoutError(timeoutMs))
164
+
165
+ return new Promise<boolean>((resolve, reject) => {
166
+ const waitSeconds = Math.max(0.001, remainingMs / 1_000).toFixed(3)
167
+ const child = spawn('flock', ['--exclusive', '--wait', waitSeconds, '3'], {
168
+ stdio: ['ignore', 'ignore', 'pipe', handle.fd],
169
+ })
170
+ let stderr = ''
171
+ let pendingError: Error | undefined
172
+ let settled = false
173
+ const finish = (callback: () => void) => {
174
+ if (settled) return
175
+ settled = true
176
+ clearTimeout(timer)
177
+ signal?.removeEventListener('abort', onAbort)
178
+ callback()
179
+ }
180
+ const stopWith = (error: Error) => {
181
+ if (pendingError || settled) return
182
+ pendingError = error
183
+ child.kill('SIGKILL')
184
+ }
185
+ const onAbort = () => stopWith(abortReason(signal!))
186
+ const timer = setTimeout(() => stopWith(lockTimeoutError(timeoutMs)), remainingMs)
187
+
188
+ signal?.addEventListener('abort', onAbort, { once: true })
189
+ if (signal?.aborted) onAbort()
190
+ child.stderr?.on('data', (chunk: Buffer) => {
191
+ if (stderr.length < 2_000) stderr += chunk.toString('utf8')
192
+ })
193
+ child.on('error', (error) => finish(() => reject(pendingError ?? error)))
194
+ child.on('close', (code, closeSignal) => {
195
+ if (pendingError) {
196
+ finish(() => reject(pendingError!))
197
+ return
198
+ }
199
+ if (code === 0) {
200
+ finish(() => resolve(true))
201
+ return
202
+ }
203
+ // util-linux flock uses rc=1 when --wait expires without ownership.
204
+ if (code === 1) {
205
+ finish(() => resolve(false))
206
+ return
207
+ }
208
+ const detail = stderr.trim()
209
+ finish(() => reject(new Error(
210
+ `serialized-judge: flock failed with ${closeSignal ?? `exit ${code ?? 'unknown'}`}${detail ? `: ${detail}` : ''}`,
211
+ )))
212
+ })
213
+ })
214
+ }
215
+
216
+ async function acquireFlock(
217
+ lockFile: string,
218
+ deadline: number,
219
+ timeoutMs: number,
220
+ signal?: AbortSignal,
221
+ ): Promise<() => Promise<void>> {
222
+ for (;;) {
223
+ throwIfAborted(signal)
224
+ if (Date.now() >= deadline) throw lockTimeoutError(timeoutMs)
225
+ const handle = await open(lockFile, 'a')
226
+ try {
227
+ if (await tryAcquireFlock(handle, deadline, timeoutMs, signal)) {
228
+ let released = false
229
+ return async () => {
230
+ if (released) return
231
+ released = true
232
+ await handle.close()
233
+ }
234
+ }
235
+ } catch (error) {
236
+ await handle.close().catch(() => {})
237
+ throw error
238
+ }
239
+ await handle.close()
240
+ if (Date.now() >= deadline) throw lockTimeoutError(timeoutMs)
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Run `fn` holding BOTH locks. Exported so calibrate/parity paths can pin any
246
+ * docker-touching critical section to the same mutex the judge uses.
247
+ */
248
+ export function withJudgeLock<T>(
249
+ lockFile: string,
250
+ fn: () => Promise<T>,
251
+ opts: JudgeLockOptions = {},
252
+ ): Promise<T> {
253
+ const timeoutMs = opts.timeoutMs ?? JUDGE_LOCK_WAIT_TIMEOUT_MS
254
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
255
+ return Promise.reject(new Error('serialized-judge: lock timeoutMs must be a positive finite number'))
256
+ }
257
+ const deadline = Date.now() + timeoutMs
258
+ const predecessor = inProcessChain.catch(() => {})
259
+ const task = (async () => {
260
+ // Cancellation stops this waiter, not the predecessor. A later waiter
261
+ // still has to acquire the kernel lock, so the live holder is never bypassed.
262
+ await waitFor(predecessor, deadline, timeoutMs, opts.signal)
263
+ const release = await acquireFlock(lockFile, deadline, timeoutMs, opts.signal)
264
+ try {
265
+ throwIfAborted(opts.signal)
266
+ return await fn()
267
+ } finally {
268
+ await release()
269
+ }
270
+ })()
271
+ inProcessChain = task.catch(() => {})
272
+ return task
273
+ }
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // The judge itself.
277
+ // ---------------------------------------------------------------------------
278
+
279
+ export interface SerializedJudge {
280
+ judge(iid: string, patchPath: string, tag?: string, signal?: AbortSignal): Promise<JudgeVerdict>
281
+ readonly lockFile: string
282
+ }
283
+
284
+ function parseJudgeResult(stdout: string): Omit<JudgeVerdict, 'attempts'> | undefined {
285
+ const line = stdout.split('\n').find((l) => l.includes('JUDGE_RESULT'))
286
+ if (!line) return undefined
287
+ try {
288
+ const parsed = JSON.parse(line.slice(line.indexOf('JUDGE_RESULT') + 'JUDGE_RESULT'.length).trim())
289
+ if (typeof parsed !== 'object' || parsed === null || typeof parsed.resolved !== 'boolean') return undefined
290
+ return parsed as Omit<JudgeVerdict, 'attempts'>
291
+ } catch {
292
+ return undefined
293
+ }
294
+ }
295
+
296
+ export function createSerializedJudge(opts: SerializedJudgeOptions = {}): SerializedJudge {
297
+ const timeoutMs = opts.timeoutMs ?? JUDGE_TIMEOUT_FLOOR_MS
298
+ const usingRealJudge = opts.command === undefined
299
+ if (timeoutMs < JUDGE_TIMEOUT_FLOOR_MS && (usingRealJudge || !opts.unsafeAllowShortTimeout)) {
300
+ throw new Error(
301
+ `serialized-judge: timeoutMs=${timeoutMs} is below the ${JUDGE_TIMEOUT_FLOOR_MS}ms floor ` +
302
+ `(700s provably caused 3 spurious timeouts on psf__requests-2317)`,
303
+ )
304
+ }
305
+ const lockFile = opts.lockFile ?? join(tmpdir(), 'swe-arena-judge.lock')
306
+ const preCleanTimeoutMs = opts.preCleanTimeoutMs ?? JUDGE_PRE_CLEAN_TIMEOUT_MS
307
+ const lockWaitTimeoutMs =
308
+ opts.lockWaitTimeoutMs ??
309
+ 2 * (timeoutMs + 2 * preCleanTimeoutMs) + JUDGE_LOCK_SETTLEMENT_MARGIN_MS
310
+ if (!Number.isFinite(lockWaitTimeoutMs) || lockWaitTimeoutMs <= 0) {
311
+ throw new Error('serialized-judge: lockWaitTimeoutMs must be a positive finite number')
312
+ }
313
+ if (!Number.isFinite(preCleanTimeoutMs) || preCleanTimeoutMs <= 0) {
314
+ throw new Error('serialized-judge: preCleanTimeoutMs must be a positive finite number')
315
+ }
316
+ const cacheLevel = opts.cacheLevel ?? 'instance'
317
+ const command = opts.command ?? defaultCommand
318
+ // A command override is a hermetic test hook and does not touch Docker.
319
+ // Tests that exercise cleanup opt in by setting preCleanTimeoutMs.
320
+ const shouldPreClean = usingRealJudge || opts.preCleanTimeoutMs !== undefined
321
+
322
+ async function runDockerCleanup(argv: string[], signal?: AbortSignal): Promise<Awaited<ReturnType<typeof run>>> {
323
+ let result: Awaited<ReturnType<typeof run>>
324
+ try {
325
+ result = await run('docker', argv, { timeoutMs: preCleanTimeoutMs, signal })
326
+ } catch (error) {
327
+ throw new Error(`serialized-judge: docker pre-clean could not start: ${(error as Error).message}`, {
328
+ cause: error,
329
+ })
330
+ }
331
+ throwIfAborted(signal)
332
+ if (result.timedOut) {
333
+ throw new Error(`serialized-judge: docker ${argv[0]} pre-clean timed out after ${preCleanTimeoutMs}ms`)
334
+ }
335
+ if (result.code !== 0) {
336
+ const detail = (result.stderr || result.stdout).trim().slice(0, 1_500)
337
+ throw new Error(
338
+ `serialized-judge: docker ${argv[0]} pre-clean exited ${result.code}${detail ? `: ${detail}` : ''}`,
339
+ )
340
+ }
341
+ return result
342
+ }
343
+
344
+ async function preClean(iid: string, signal?: AbortSignal): Promise<void> {
345
+ throwIfAborted(signal)
346
+ const short = instanceShortName(iid)
347
+ const ps = await runDockerCleanup(['ps', '-aq', '--filter', `name=${short}`], signal)
348
+ throwIfAborted(signal)
349
+ const ids = ps.stdout.split('\n').map((s) => s.trim()).filter(Boolean)
350
+ if (ids.length > 0) {
351
+ await runDockerCleanup(['rm', '-f', ...ids], signal)
352
+ throwIfAborted(signal)
353
+ }
354
+ }
355
+
356
+ async function attempt(
357
+ iid: string,
358
+ patchPath: string,
359
+ signal?: AbortSignal,
360
+ ): Promise<{ verdict?: Omit<JudgeVerdict, 'attempts'>; raw: string }> {
361
+ // Pre-clean INSIDE the mutex: with judging serialized this can only ever
362
+ // remove a stale container, never a live grade.
363
+ if (shouldPreClean) await preClean(iid, signal)
364
+ throwIfAborted(signal)
365
+ const { bin, argv, cwd } = command(iid, patchPath)
366
+ const res = await run(bin, argv, {
367
+ cwd,
368
+ timeoutMs,
369
+ signal,
370
+ env: { ...process.env, SWEBENCH_CACHE_LEVEL: cacheLevel },
371
+ })
372
+ throwIfAborted(signal)
373
+ const raw = res.timedOut
374
+ ? `timeout after ${timeoutMs}ms`
375
+ : res.code === 0
376
+ ? res.stdout || res.stderr
377
+ : `exit ${res.code}: ${res.stdout || res.stderr}`
378
+ return {
379
+ verdict: !res.timedOut && res.code === 0 ? parseJudgeResult(res.stdout) : undefined,
380
+ raw,
381
+ }
382
+ }
383
+
384
+ return {
385
+ lockFile,
386
+ async judge(iid, patchPath, _tag = 'x', signal) {
387
+ throwIfAborted(signal)
388
+ // Empty patch never reaches docker — same short-circuit as judge.sh.
389
+ const patch = await readFile(patchPath, 'utf8')
390
+ throwIfAborted(signal)
391
+ if (patch.trim().length === 0) {
392
+ return { iid, resolved: false, score: 0, note: 'empty-patch', attempts: 0 }
393
+ }
394
+ return withJudgeLock(
395
+ lockFile,
396
+ async () => {
397
+ const first = await attempt(iid, patchPath, signal)
398
+ if (first.verdict) return { ...first.verdict, attempts: 1 }
399
+ throwIfAborted(signal)
400
+ const second = await attempt(iid, patchPath, signal)
401
+ if (second.verdict) return { ...second.verdict, attempts: 2 }
402
+ return {
403
+ iid,
404
+ resolved: null,
405
+ error: 'parse-or-timeout',
406
+ raw: second.raw.slice(0, 200),
407
+ attempts: 2,
408
+ }
409
+ },
410
+ { timeoutMs: lockWaitTimeoutMs, signal },
411
+ )
412
+ },
413
+ }
414
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * swe-arena — typed replay of the committed SOLO-vs-SUPERVISOR head-to-head
3
+ * artifacts (SWE-bench Verified, glm-5.2 both arms).
4
+ *
5
+ * MILESTONE 1: these types mirror the fixture files byte-for-byte semantics.
6
+ * They are the proof-of-faithfulness layer: `reconcile.ts` + `analyze.ts`
7
+ * must reproduce the reference `fixtures/analyze.py` output exactly (pinned in
8
+ * `replay.test.mts`) before any typed execution path is built on top.
9
+ */
10
+
11
+ /**
12
+ * The fields of one SWE-bench Verified instance we actually consume from
13
+ * `task-meta.json` (generated by the experiment's `load_meta.py` from
14
+ * princeton-nlp/SWE-bench_Verified). `FAIL_TO_PASS` / `PASS_TO_PASS` are
15
+ * JSON-encoded string arrays as shipped by the HF dataset — kept as raw
16
+ * strings here; decode at the point of use.
17
+ */
18
+ export interface SweInstance {
19
+ instance_id: string
20
+ repo: string
21
+ base_commit: string
22
+ problem_statement: string
23
+ patch: string
24
+ test_patch: string
25
+ /** JSON-encoded string[] (raw HF dataset encoding). */
26
+ FAIL_TO_PASS: string
27
+ /** JSON-encoded string[] (raw HF dataset encoding). */
28
+ PASS_TO_PASS: string
29
+ version: string | null
30
+ environment_setup_commit: string | null
31
+ }
32
+
33
+ /** Per-run opencode usage breakdown captured on the SOLO arm. */
34
+ export interface SoloUsage {
35
+ steps: number
36
+ in: number
37
+ out: number
38
+ reasoning: number
39
+ cache_w: number
40
+ cache_r: number
41
+ max_ctx: number
42
+ oc_cost: number
43
+ total_io: number
44
+ }
45
+
46
+ /**
47
+ * One paired row of `ledger.jsonl` — exactly the schema the experiment wrote.
48
+ * `null` values are real telemetry gaps (e.g. `sup_spentTokens: null` when the
49
+ * supervisor driver exited rc=3 mid-run), not absent data to be defaulted.
50
+ */
51
+ export interface LedgerRow {
52
+ iid: string
53
+ solo_resolved: boolean
54
+ sup_resolved: boolean
55
+ solo_verify_pass: boolean
56
+ sup_verify_pass: boolean
57
+ solo_patch_lines: number
58
+ sup_patch_lines: number
59
+ solo_wall_s: number
60
+ sup_wall_s: number
61
+ solo_tokens: number
62
+ solo_usage: SoloUsage
63
+ sup_spentTokens: number | null
64
+ sup_spentUsd: number | null
65
+ sup_spawned: number
66
+ /** Missing on rows written before the field was added (pallets__flask-5014). */
67
+ sup_workers?: number
68
+ sup_settled: number
69
+ sup_subtasks: string[]
70
+ sup_delivered: boolean | null
71
+ /**
72
+ * The fixture ledger observed only completed/running; the driver can also
73
+ * settle failed/cancelled (M2 widened the union — the typed execution path
74
+ * records those honestly instead of coercing them).
75
+ */
76
+ sup_status: 'completed' | 'running' | 'failed' | 'cancelled' | null
77
+ sup_verdict: 'delivered' | 'no-winner' | 'best-effort' | null
78
+ solo_oc_rc: number
79
+ sup_driver_rc: number
80
+ solo_patch: string
81
+ sup_patch: string
82
+ /** Free-form correction note (e.g. psf__requests-1766 sup re-judge). */
83
+ _note?: string
84
+ }
85
+
86
+ /**
87
+ * Tags carried by `rejudge.jsonl` rows. Gold-family tags grade the OFFICIAL
88
+ * gold patch (calibrating the judge itself); final/my-reverify tags are the
89
+ * personal authoritative re-judgements of arm patches that override the
90
+ * automated ledger verdicts.
91
+ */
92
+ export type RejudgeTag =
93
+ | 'gold-control'
94
+ | 'gold'
95
+ | 'gold2'
96
+ | 'solo-final'
97
+ | 'sup-final'
98
+ | 'solo-final2'
99
+ | 'sup-final2'
100
+ | 'my-reverify'
101
+
102
+ /** One row of `rejudge.jsonl`. `resolved: null` = the judge run failed to parse (inconclusive). */
103
+ export interface RejudgeRow {
104
+ iid: string
105
+ tag: RejudgeTag
106
+ patch: string
107
+ resolved: boolean | null
108
+ score?: number
109
+ secs?: number
110
+ patch_bytes?: number
111
+ error?: string
112
+ raw?: string
113
+ }
114
+
115
+ /** Evolution-round labels: SUP2/3/4 = rematch.jsonl / rematch2.jsonl / rematch3.jsonl. */
116
+ export type RematchArm = 'SUP2' | 'SUP3' | 'SUP4'
117
+
118
+ /** One row of a rematch*.jsonl evolution round. */
119
+ export interface RematchRow {
120
+ iid: string
121
+ arm: RematchArm
122
+ resolved: boolean
123
+ verify_pass: boolean
124
+ patch_lines: number
125
+ wall_s: number
126
+ spawned: number
127
+ workers: number
128
+ sup_status: 'completed' | 'running'
129
+ sup_verdict: 'delivered' | 'no-winner' | 'best-effort' | null
130
+ delivered: boolean | null
131
+ spentTokens: number | null
132
+ }
133
+
134
+ /** Declarative arm identity for typed execution paths built on this module. */
135
+ export interface ArmSpec {
136
+ name: string
137
+ kind: 'solo' | 'supervisor'
138
+ env: Record<string, string>
139
+ provenance: { repo: string; commit: string }
140
+ }
141
+
142
+ /** One pre-registered holdout instance from `holdout.json`. */
143
+ export interface HoldoutEntry {
144
+ iid: string
145
+ repo: string
146
+ gold_official_resolved: boolean
147
+ verify_calibrated: boolean
148
+ selected_at_commit: string
149
+ }
150
+
151
+ /**
152
+ * The holdout registry: instances selected BEFORE any arm ran (see
153
+ * `holdout-preregister.log`), each with its gold patch verified against the
154
+ * calibrated judge. Only entries passing both checks are usable.
155
+ */
156
+ export interface HoldoutRegistry {
157
+ entries: HoldoutEntry[]
158
+ /** Commit of the supervisor runtime (`loops`) at selection time. */
159
+ selectedAtCommit: string
160
+ }
161
+
162
+ /** Per-instance supervisor worker-session spend from `worker-tokens.json`. */
163
+ export interface WorkerTokens {
164
+ worker_sessions: number
165
+ worker_tok: number
166
+ }