@tangle-network/agent-bench 0.8.23 → 0.9.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.
- package/CHANGELOG.md +69 -0
- package/HARNESS.md +9 -2
- package/dist/index.d.ts +27 -10
- package/dist/index.js +140 -40
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/scripts/verify-pier-agent.mts +1 -1
- package/scripts/verify-pier-recovery.mts +2 -1
- package/src/atom-mcp-e2e.mts +1 -0
- package/src/benchmarks/humaneval.test.mts +2 -2
- package/src/coordination-mcp-container-reach.mts +4 -3
- package/src/index.ts +1 -0
- package/src/mcp-mount-probe.mts +1 -0
- package/src/pier-agent.test.mts +1 -1
- package/src/pier-agent.ts +9 -4
- package/src/pier-trial-controller.test.mts +7 -5
- package/src/pier-trial-controller.ts +10 -2
- package/src/run-benchmarks.test.mts +86 -4
- package/src/run-benchmarks.ts +128 -28
- package/src/sandbox-run.ts +2 -1
- package/src/swe-arena/activation.test.mts +1 -0
- package/src/swe-arena/briefing.test.mts +1 -0
- package/src/swe-arena/execution.test.mts +3 -2
- package/src/swe-arena/factory.test.mts +5 -3
- package/src/swe-arena/gepa-seat.test.mts +2 -0
- package/src/swe-arena/premeasured-from-cells.test.mts +1 -0
- package/src/swe-arena/proc.test.mts +3 -5
- package/src/swe-arena/proposer-fanout.test.mts +1 -0
- package/src/swe-arena/run-experiment.mts +2 -0
- package/src/swe-arena/scratch-worktree.test.mts +2 -1
- package/src/swe-bench-env.test.ts +9 -1
- package/src/tb-container-executor.test.mts +4 -1
- package/src/tb-supervisor-sidecar.mts +1 -0
- package/tb_agents/opencode_supervisor_agent.py +2 -2
package/src/run-benchmarks.ts
CHANGED
|
@@ -35,7 +35,7 @@ import type {
|
|
|
35
35
|
Deliverable,
|
|
36
36
|
OpenSandboxRunOptions,
|
|
37
37
|
} from '@tangle-network/agent-runtime/kernel'
|
|
38
|
-
import { openSandboxRun } from '@tangle-network/agent-runtime/kernel'
|
|
38
|
+
import { openSandboxRun, SandboxRunAbortError, sumSandboxUsage } from '@tangle-network/agent-runtime/kernel'
|
|
39
39
|
import type { SandboxEvent } from '@tangle-network/sandbox'
|
|
40
40
|
import { resolveAdapter } from './adapters'
|
|
41
41
|
import type { BenchmarkAdapter, BenchScore, BenchTask } from './benchmarks/types'
|
|
@@ -60,8 +60,17 @@ export interface BenchCell {
|
|
|
60
60
|
readonly profile?: AgentProfile
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
63
|
+
/** A worker's artifact and observed execution evidence, before external grading. */
|
|
64
|
+
export interface BenchShotResult {
|
|
65
|
+
readonly artifact: string
|
|
66
|
+
readonly ok: boolean
|
|
67
|
+
readonly detail?: string
|
|
68
|
+
/** Provider observations, including explicit unknown counters. Omitted when the shot reports none. */
|
|
69
|
+
readonly usage?: ReturnType<typeof sumSandboxUsage>
|
|
70
|
+
readonly events?: readonly SandboxEvent[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Runs one (adapter, task, cell) shot. Defaults to `openSandboxRun`. */
|
|
65
74
|
export type BenchShot = (input: {
|
|
66
75
|
readonly adapter: BenchmarkAdapter
|
|
67
76
|
readonly task: BenchTask
|
|
@@ -72,12 +81,15 @@ export type BenchShot = (input: {
|
|
|
72
81
|
readonly attempt?: number
|
|
73
82
|
readonly routerBaseUrl: string
|
|
74
83
|
readonly routerKey: string
|
|
84
|
+
/** Optional inference credential for the box; routerKey continues to authorize sandbox control. */
|
|
85
|
+
readonly modelApiKey?: string
|
|
75
86
|
readonly bridgeUrl?: string
|
|
76
87
|
readonly bridgeBearer?: string
|
|
77
88
|
readonly sandboxBaseUrl?: string
|
|
78
89
|
readonly timeoutMs?: number
|
|
90
|
+
readonly signal?: AbortSignal
|
|
79
91
|
readonly resolveClient?: typeof resolveBenchClient
|
|
80
|
-
}) => Promise<
|
|
92
|
+
}) => Promise<BenchShotResult>
|
|
81
93
|
|
|
82
94
|
export interface RunBenchmarksOptions {
|
|
83
95
|
/** Registry keys (`resolveAdapter`) — the benchmark subset to run. */
|
|
@@ -86,6 +98,8 @@ export interface RunBenchmarksOptions {
|
|
|
86
98
|
readonly cells: readonly BenchCell[]
|
|
87
99
|
readonly routerBaseUrl: string
|
|
88
100
|
readonly routerKey: string
|
|
101
|
+
/** Optional inference credential for the box; never used for sandbox creation or deletion. */
|
|
102
|
+
readonly modelApiKey?: string
|
|
89
103
|
readonly bridgeUrl?: string
|
|
90
104
|
readonly bridgeBearer?: string
|
|
91
105
|
readonly sandboxBaseUrl?: string
|
|
@@ -99,6 +113,8 @@ export interface RunBenchmarksOptions {
|
|
|
99
113
|
readonly concurrency?: number
|
|
100
114
|
/** Per-shot wall-clock (ms). */
|
|
101
115
|
readonly timeoutMs?: number
|
|
116
|
+
/** Cancels active shots and prevents queued shots from starting. */
|
|
117
|
+
readonly signal?: AbortSignal
|
|
102
118
|
/** Test seam: resolve the runtime transport. Defaults to `resolveBenchClient`. */
|
|
103
119
|
readonly resolveClient?: typeof resolveBenchClient
|
|
104
120
|
/** Max attempts per (benchmark × cell × task). Default 1. Attempts after the first receive
|
|
@@ -126,6 +142,11 @@ export interface BenchCellTaskResult {
|
|
|
126
142
|
readonly ok: boolean
|
|
127
143
|
readonly detail?: string
|
|
128
144
|
readonly wallMs: number
|
|
145
|
+
/** Exact bytes given to the benchmark judge, retained even when judging fails. */
|
|
146
|
+
readonly artifact?: string
|
|
147
|
+
readonly usage?: ReturnType<typeof sumSandboxUsage>
|
|
148
|
+
/** Worker events only; benchmark grading remains outside this trace. */
|
|
149
|
+
readonly events?: readonly SandboxEvent[]
|
|
129
150
|
}
|
|
130
151
|
|
|
131
152
|
export interface BenchLeaderboardRow {
|
|
@@ -173,7 +194,8 @@ function finalText(events: readonly SandboxEvent[]): string {
|
|
|
173
194
|
|
|
174
195
|
/** The default real-agent shot: one `openSandboxRun` over the cell's harness+model, deliverable
|
|
175
196
|
* extracted by the adapter's parser (or final text), abortable on `timeoutMs`. */
|
|
176
|
-
const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerBaseUrl, routerKey, bridgeUrl, bridgeBearer, sandboxBaseUrl, timeoutMs, resolveClient }) => {
|
|
197
|
+
const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerBaseUrl, routerKey, modelApiKey, bridgeUrl, bridgeBearer, sandboxBaseUrl, timeoutMs, signal, resolveClient }) => {
|
|
198
|
+
signal?.throwIfAborted()
|
|
177
199
|
const client = (resolveClient ?? resolveBenchClient)({
|
|
178
200
|
backend: cell.backend ?? 'router',
|
|
179
201
|
routerBaseUrl,
|
|
@@ -206,7 +228,7 @@ const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerB
|
|
|
206
228
|
sandboxOverrides: {
|
|
207
229
|
name: `bench-${adapter.name}-${task.id}-${uniq}`.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 60),
|
|
208
230
|
environment: 'universal',
|
|
209
|
-
backend: { type: harness as never, model: { provider: profileProvider, model: cell.model, baseUrl: routerBaseUrl } },
|
|
231
|
+
backend: { type: harness as never, model: { provider: profileProvider, model: cell.model, baseUrl: routerBaseUrl, ...(modelApiKey === undefined ? {} : { apiKey: modelApiKey }) } },
|
|
210
232
|
},
|
|
211
233
|
}
|
|
212
234
|
const deliverable: Deliverable<string> = {
|
|
@@ -217,7 +239,7 @@ const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerB
|
|
|
217
239
|
const timer = timeoutMs ? setTimeout(() => controller.abort(), timeoutMs) : undefined
|
|
218
240
|
const runOptions: OpenSandboxRunOptions = {
|
|
219
241
|
agentRun,
|
|
220
|
-
signal: controller.signal,
|
|
242
|
+
signal: signal ? AbortSignal.any([controller.signal, signal]) : controller.signal,
|
|
221
243
|
runId: `bench:${adapter.name}:${task.id}:${uniq}`,
|
|
222
244
|
scenarioId: task.id,
|
|
223
245
|
}
|
|
@@ -236,9 +258,12 @@ const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerB
|
|
|
236
258
|
)
|
|
237
259
|
}
|
|
238
260
|
}
|
|
239
|
-
|
|
261
|
+
let run: Awaited<ReturnType<typeof openSandboxRun<string>>> | undefined
|
|
262
|
+
let result: BenchShotResult = { artifact: '', ok: false }
|
|
240
263
|
try {
|
|
264
|
+
run = await openSandboxRun(client, runOptions, deliverable)
|
|
241
265
|
const turn = await run.start(prompt ?? task.prompt)
|
|
266
|
+
result = { artifact: '', ok: false, usage: sumSandboxUsage(turn.events), events: turn.events }
|
|
242
267
|
// Event-stream deliverable (adapter.output ?? finalText) — the FALLBACK.
|
|
243
268
|
let artifact = (turn.out ?? '').trim()
|
|
244
269
|
let boxExtractError: string | undefined
|
|
@@ -289,7 +314,9 @@ const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerB
|
|
|
289
314
|
}
|
|
290
315
|
}
|
|
291
316
|
const detail =
|
|
292
|
-
turn.
|
|
317
|
+
!turn.outcome.success
|
|
318
|
+
? turn.outcome.error ?? `agent ended with status ${turn.outcome.status}`
|
|
319
|
+
: turn.readError !== undefined
|
|
293
320
|
? `read: ${turn.readError.slice(0, 160)}`
|
|
294
321
|
: boxExtractError !== undefined
|
|
295
322
|
? `boxExtract: ${boxExtractError}`
|
|
@@ -306,15 +333,30 @@ const openSandboxShot: BenchShot = async ({ adapter, task, cell, prompt, routerB
|
|
|
306
333
|
// debug-only; never fail the shot on a dump error
|
|
307
334
|
}
|
|
308
335
|
}
|
|
309
|
-
|
|
336
|
+
result = {
|
|
310
337
|
artifact,
|
|
311
|
-
ok: artifact.length > 0,
|
|
338
|
+
ok: turn.outcome.success && artifact.length > 0 && turn.readError === undefined && boxExtractError === undefined,
|
|
339
|
+
usage: result.usage,
|
|
340
|
+
events: turn.events,
|
|
312
341
|
...(detail ? { detail } : {}),
|
|
313
342
|
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
result = {
|
|
345
|
+
...result,
|
|
346
|
+
ok: false,
|
|
347
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
348
|
+
...(err instanceof SandboxRunAbortError ? { usage: sumSandboxUsage(err.events), events: err.events } : {}),
|
|
349
|
+
}
|
|
314
350
|
} finally {
|
|
315
351
|
if (timer) clearTimeout(timer)
|
|
316
|
-
|
|
352
|
+
try {
|
|
353
|
+
await run?.close()
|
|
354
|
+
} catch (err) {
|
|
355
|
+
const cleanup = `cleanup: ${err instanceof Error ? err.message : String(err)}`
|
|
356
|
+
result = { ...result, detail: combineDetails(result.detail, cleanup) }
|
|
357
|
+
}
|
|
317
358
|
}
|
|
359
|
+
return result
|
|
318
360
|
}
|
|
319
361
|
|
|
320
362
|
function parseMaybeJson(value: string): unknown {
|
|
@@ -380,23 +422,44 @@ async function loopedShot(
|
|
|
380
422
|
input: Parameters<BenchShot>[0],
|
|
381
423
|
shot: BenchShot,
|
|
382
424
|
attempts: number,
|
|
383
|
-
): Promise<
|
|
425
|
+
): Promise<BenchShotResult> {
|
|
384
426
|
const scores = new Map<number, BenchScore>()
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
427
|
+
const shots = new Map<number, BenchShotResult>()
|
|
428
|
+
let pendingShot = false
|
|
429
|
+
let result: Awaited<ReturnType<typeof runRefineLoop<string>>>
|
|
430
|
+
try {
|
|
431
|
+
result = await runRefineLoop<string>({
|
|
432
|
+
rounds: attempts,
|
|
433
|
+
prompt: (round, history) => (round === 1 ? input.task.prompt : retryPrompt(input.task, history, scores)),
|
|
434
|
+
runShot: async (prompt, round) => {
|
|
435
|
+
input.signal?.throwIfAborted()
|
|
436
|
+
pendingShot = true
|
|
437
|
+
const out = await shot({ ...input, prompt, attempt: round })
|
|
438
|
+
shots.set(round, out)
|
|
439
|
+
pendingShot = false
|
|
440
|
+
return { artifact: out.artifact, note: out.detail }
|
|
441
|
+
},
|
|
442
|
+
judge: async (artifact, round) => {
|
|
443
|
+
const score = await input.adapter.judge(input.task, artifact)
|
|
444
|
+
scores.set(round, score)
|
|
445
|
+
const succeeded = shots.get(round)?.ok === true
|
|
446
|
+
return { valid: succeeded && score.resolved, score: succeeded ? score.score : 0 }
|
|
447
|
+
},
|
|
448
|
+
})
|
|
449
|
+
} catch (err) {
|
|
450
|
+
const completed = [...shots.values()]
|
|
451
|
+
return {
|
|
452
|
+
artifact: completed.at(-1)?.artifact ?? '',
|
|
453
|
+
ok: false,
|
|
454
|
+
usage: combinedUsage(pendingShot ? [...completed, { artifact: '', ok: false }] : completed),
|
|
455
|
+
events: completed.flatMap((shot) => shot.events ?? []),
|
|
456
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
457
|
+
}
|
|
458
|
+
}
|
|
398
459
|
|
|
399
460
|
const best = result.rounds.reduce((winner, candidate) => {
|
|
461
|
+
if (shots.get(candidate.round)?.ok !== true) return winner
|
|
462
|
+
if (shots.get(winner.round)?.ok !== true) return candidate
|
|
400
463
|
const a = scores.get(winner.round)
|
|
401
464
|
const b = scores.get(candidate.round)
|
|
402
465
|
if (!a) return candidate
|
|
@@ -408,7 +471,9 @@ async function loopedShot(
|
|
|
408
471
|
const bestScore = scores.get(best.round)
|
|
409
472
|
return {
|
|
410
473
|
artifact: best.artifact,
|
|
411
|
-
ok: best.artifact.trim().length > 0,
|
|
474
|
+
ok: shots.get(best.round)?.ok === true && best.artifact.trim().length > 0,
|
|
475
|
+
usage: combinedUsage([...shots.values()]),
|
|
476
|
+
events: [...shots.values()].flatMap((shot) => shot.events ?? []),
|
|
412
477
|
detail: JSON.stringify({
|
|
413
478
|
mode: 'refine-loop',
|
|
414
479
|
attempts: result.rounds.length,
|
|
@@ -425,6 +490,30 @@ async function loopedShot(
|
|
|
425
490
|
}
|
|
426
491
|
}
|
|
427
492
|
|
|
493
|
+
function combinedUsage(shots: readonly BenchShotResult[]): ReturnType<typeof sumSandboxUsage> {
|
|
494
|
+
const usage = { input: 0, output: 0, costUsd: 0 }
|
|
495
|
+
let tokensKnown = shots.length > 0
|
|
496
|
+
let usdKnown = shots.length > 0
|
|
497
|
+
let estimate: number | undefined
|
|
498
|
+
let unknownReason: string | undefined
|
|
499
|
+
for (const shot of shots) {
|
|
500
|
+
usage.input += shot.usage?.input ?? 0
|
|
501
|
+
usage.output += shot.usage?.output ?? 0
|
|
502
|
+
usage.costUsd += shot.usage?.costUsd ?? 0
|
|
503
|
+
tokensKnown &&= shot.usage !== undefined && shot.usage.tokensKnown !== false
|
|
504
|
+
usdKnown &&= shot.usage !== undefined && shot.usage.usdKnown !== false
|
|
505
|
+
unknownReason ??= shot.usage?.tokensUnknownReason
|
|
506
|
+
if (shot.usage?.estimatedCostUsd !== undefined) estimate = (estimate ?? 0) + shot.usage.estimatedCostUsd
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
...usage,
|
|
510
|
+
...(tokensKnown ? {} : { tokensKnown: false as const }),
|
|
511
|
+
...(usdKnown ? {} : { usdKnown: false as const }),
|
|
512
|
+
...(estimate === undefined ? {} : { estimatedCostUsd: estimate }),
|
|
513
|
+
...(unknownReason === undefined ? {} : { tokensUnknownReason: unknownReason }),
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
428
517
|
function combineDetails(runDetail: string | undefined, scoreDetail: string | undefined): string | undefined {
|
|
429
518
|
if (runDetail && scoreDetail) {
|
|
430
519
|
return JSON.stringify({ run: parseMaybeJson(runDetail), score: parseMaybeJson(scoreDetail) })
|
|
@@ -481,6 +570,7 @@ async function prepareBenchmarks(
|
|
|
481
570
|
}
|
|
482
571
|
|
|
483
572
|
export async function runBenchmarks(opts: RunBenchmarksOptions): Promise<RunBenchmarksReport> {
|
|
573
|
+
opts.signal?.throwIfAborted()
|
|
484
574
|
if (opts.benchmarks.length === 0) throw new Error('runBenchmarks: no benchmarks selected')
|
|
485
575
|
if (opts.cells.length === 0) throw new Error('runBenchmarks: no cells to run')
|
|
486
576
|
const reps = Math.max(1, opts.reps ?? 1)
|
|
@@ -497,20 +587,24 @@ export async function runBenchmarks(opts: RunBenchmarksOptions): Promise<RunBenc
|
|
|
497
587
|
await runPool(jobs, Math.max(1, opts.concurrency ?? 4), async (job, index) => {
|
|
498
588
|
const startedAt = Date.now()
|
|
499
589
|
let result: BenchCellTaskResult
|
|
590
|
+
let out: BenchShotResult | undefined
|
|
500
591
|
try {
|
|
592
|
+
opts.signal?.throwIfAborted()
|
|
501
593
|
const shotInput = {
|
|
502
594
|
adapter: job.adapter,
|
|
503
595
|
task: job.task,
|
|
504
596
|
cell: job.cell,
|
|
505
597
|
routerBaseUrl: opts.routerBaseUrl,
|
|
506
598
|
routerKey: opts.routerKey,
|
|
599
|
+
...(opts.modelApiKey === undefined ? {} : { modelApiKey: opts.modelApiKey }),
|
|
507
600
|
...(opts.bridgeUrl ? { bridgeUrl: opts.bridgeUrl } : {}),
|
|
508
601
|
...(opts.bridgeBearer ? { bridgeBearer: opts.bridgeBearer } : {}),
|
|
509
602
|
...(opts.sandboxBaseUrl ? { sandboxBaseUrl: opts.sandboxBaseUrl } : {}),
|
|
510
603
|
...(opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}),
|
|
604
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
511
605
|
...(opts.resolveClient ? { resolveClient: opts.resolveClient } : {}),
|
|
512
606
|
}
|
|
513
|
-
|
|
607
|
+
out = loopAttempts > 1 ? await loopedShot(shotInput, shot, loopAttempts) : await shot(shotInput)
|
|
514
608
|
const score: BenchScore = await job.adapter.judge(job.task, out.artifact)
|
|
515
609
|
result = {
|
|
516
610
|
benchmark: job.benchmark,
|
|
@@ -522,6 +616,9 @@ export async function runBenchmarks(opts: RunBenchmarksOptions): Promise<RunBenc
|
|
|
522
616
|
ok: out.ok,
|
|
523
617
|
...(out.detail ?? score.detail ? { detail: combineDetails(out.detail, score.detail) } : {}),
|
|
524
618
|
wallMs: Date.now() - startedAt,
|
|
619
|
+
artifact: out.artifact,
|
|
620
|
+
...(out.usage === undefined ? {} : { usage: out.usage }),
|
|
621
|
+
...(out.events === undefined ? {} : { events: out.events }),
|
|
525
622
|
}
|
|
526
623
|
} catch (err) {
|
|
527
624
|
// A thrown shot/judge is infra error for THIS cell-task: ok=false excludes it from the
|
|
@@ -536,6 +633,9 @@ export async function runBenchmarks(opts: RunBenchmarksOptions): Promise<RunBenc
|
|
|
536
633
|
ok: false,
|
|
537
634
|
detail: err instanceof Error ? err.message.slice(0, 200) : String(err),
|
|
538
635
|
wallMs: Date.now() - startedAt,
|
|
636
|
+
...(out === undefined ? {} : { artifact: out.artifact }),
|
|
637
|
+
...(out?.usage === undefined ? {} : { usage: out.usage }),
|
|
638
|
+
...(out?.events === undefined ? {} : { events: out.events }),
|
|
539
639
|
}
|
|
540
640
|
}
|
|
541
641
|
void index
|
package/src/sandbox-run.ts
CHANGED
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
type AgentRunSpec,
|
|
15
15
|
type OutputAdapter,
|
|
16
16
|
} from '@tangle-network/agent-runtime/kernel'
|
|
17
|
-
import { parseExactAgentProfile } from '@tangle-network/agent-runtime'
|
|
17
|
+
import { parseExactAgentProfile } from '@tangle-network/agent-runtime/candidate-execution'
|
|
18
|
+
|
|
18
19
|
// `BackendType` is the sandbox SDK's harness union and its canonical home. Runtime consumes it
|
|
19
20
|
// from there too; benchmark profiles use the same values as their exact harness identity.
|
|
20
21
|
import type { BackendType } from '@tangle-network/sandbox'
|
|
@@ -211,6 +211,7 @@ describe('activation-predicate prefilter', () => {
|
|
|
211
211
|
loopsRepo = await mkdtemp(join(tmpdir(), 'act-repo-'))
|
|
212
212
|
outDir = await mkdtemp(join(tmpdir(), 'act-out-'))
|
|
213
213
|
await runOk('git', ['init', '-q', '-b', 'main', loopsRepo])
|
|
214
|
+
await runOk('git', ['-C', loopsRepo, 'config', 'core.hooksPath', '/dev/null'])
|
|
214
215
|
await runOk('git', ['-C', loopsRepo, 'config', 'user.email', 't@t.dev'])
|
|
215
216
|
await runOk('git', ['-C', loopsRepo, 'config', 'user.name', 'T'])
|
|
216
217
|
await writeFile(join(loopsRepo, 'src.ts'), 'base\n')
|
|
@@ -152,6 +152,7 @@ describe('briefing text', () => {
|
|
|
152
152
|
const repo = await mkdtemp(join(tmpdir(), 'briefing-repo-'))
|
|
153
153
|
try {
|
|
154
154
|
await runOk('git', ['init', '-q', '-b', 'main', repo])
|
|
155
|
+
await runOk('git', ['-C', repo, 'config', 'core.hooksPath', '/dev/null'])
|
|
155
156
|
await runOk('git', ['-C', repo, 'config', 'user.email', 't@t.dev'])
|
|
156
157
|
await runOk('git', ['-C', repo, 'config', 'user.name', 'T'])
|
|
157
158
|
await writeFile(join(repo, 'base.txt'), 'x\n')
|
|
@@ -344,6 +344,7 @@ describe('arms: patch extraction (real git, no docker)', () => {
|
|
|
344
344
|
const ws = await scratch('swe-arena-git-')
|
|
345
345
|
const git = (...argv: string[]) => runOk('git', ['-C', ws, ...argv])
|
|
346
346
|
await runOk('git', ['init', '-q', ws])
|
|
347
|
+
await runOk('git', ['-C', ws, 'config', 'core.hooksPath', '/dev/null'])
|
|
347
348
|
await git('config', 'user.email', 't@t')
|
|
348
349
|
await git('config', 'user.name', 't')
|
|
349
350
|
await writeFile(join(ws, 'lib.py'), 'x = 1\n')
|
|
@@ -904,7 +905,7 @@ describe('serialized-judge', () => {
|
|
|
904
905
|
const dir = await scratch('swe-arena-judge-flock-stale-bytes-')
|
|
905
906
|
const lockFile = join(dir, 'lock')
|
|
906
907
|
await writeFile(lockFile, 'half-written-or-dead-owner\n')
|
|
907
|
-
await expect(withJudgeLock(lockFile, async () => 'acquired', { timeoutMs:
|
|
908
|
+
await expect(withJudgeLock(lockFile, async () => 'acquired', { timeoutMs: 5_000 })).resolves.toBe('acquired')
|
|
908
909
|
expect(await readFile(lockFile, 'utf8')).toBe('half-written-or-dead-owner\n')
|
|
909
910
|
})
|
|
910
911
|
|
|
@@ -926,7 +927,7 @@ describe('serialized-judge', () => {
|
|
|
926
927
|
|
|
927
928
|
await expect(result).rejects.toThrow('cancelled active judge')
|
|
928
929
|
expect((await readFile(attempts, 'utf8')).trim().split('\n')).toEqual(['attempt'])
|
|
929
|
-
await expect(withJudgeLock(lockFile, async () => 'released', { timeoutMs:
|
|
930
|
+
await expect(withJudgeLock(lockFile, async () => 'released', { timeoutMs: 5_000 })).resolves.toBe('released')
|
|
930
931
|
}, 15_000)
|
|
931
932
|
|
|
932
933
|
it('bounds stale-container cleanup before starting the judge child', async () => {
|
|
@@ -246,6 +246,7 @@ async function makeSyntheticMirror(root: string): Promise<SyntheticMirror> {
|
|
|
246
246
|
const mirror = join(root, 'mirror')
|
|
247
247
|
await mkdir(mirror, { recursive: true })
|
|
248
248
|
await runOk('git', ['-C', mirror, 'init', '-q', '-b', 'main'])
|
|
249
|
+
await runOk('git', ['-C', mirror, 'config', 'core.hooksPath', '/dev/null'])
|
|
249
250
|
await writeFile(join(mirror, 'README.md'), '# synthetic\n')
|
|
250
251
|
await writeFile(
|
|
251
252
|
join(mirror, 'package.json'),
|
|
@@ -359,7 +360,8 @@ describe('parseVitestSummary', () => {
|
|
|
359
360
|
})
|
|
360
361
|
})
|
|
361
362
|
|
|
362
|
-
|
|
363
|
+
// The isolated Docker controller uses the Linux daemon socket, never personal Docker contexts.
|
|
364
|
+
describe.skipIf(process.platform !== 'linux')('factory command credential isolation', () => {
|
|
363
365
|
it('blocks arbitrary env, auth sockets, npm config, and home credentials from a package lifecycle script', async () => {
|
|
364
366
|
const ambientHome = join(root, 'ambient-home')
|
|
365
367
|
const ambientXdg = join(root, 'ambient-xdg')
|
|
@@ -506,7 +508,7 @@ describe('loadFactoryInstance', () => {
|
|
|
506
508
|
// Judge child pipeline on the synthetic fixture.
|
|
507
509
|
// ---------------------------------------------------------------------------
|
|
508
510
|
|
|
509
|
-
describe('judgeFactoryPatch', () => {
|
|
511
|
+
describe.skipIf(process.platform !== 'linux')('judgeFactoryPatch', () => {
|
|
510
512
|
it('gold (impl-only PR diff) resolves with full score', async () => {
|
|
511
513
|
const { result } = await judgeFactoryPatch(goodInst, await goldImplPatch(goodInst))
|
|
512
514
|
expect(result).toMatchObject({ resolved: true, score: 1, passed: 2, total: 2 })
|
|
@@ -597,7 +599,7 @@ describe('materializeFactoryWorkspace', () => {
|
|
|
597
599
|
// Calibration admission gate — both rejection directions.
|
|
598
600
|
// ---------------------------------------------------------------------------
|
|
599
601
|
|
|
600
|
-
describe('calibrateFactoryInstance', () => {
|
|
602
|
+
describe.skipIf(process.platform !== 'linux')('calibrateFactoryInstance', () => {
|
|
601
603
|
it('admits a well-formed instance (gold passes, base fails)', async () => {
|
|
602
604
|
const r = await calibrateFactoryInstance(goodInst)
|
|
603
605
|
expect(r).toMatchObject({
|
|
@@ -535,6 +535,7 @@ describe('fanOutLoopsGenerator with the gepa seat', () => {
|
|
|
535
535
|
loopsRepo = await mkdtemp(join(tmpdir(), 'gepa-repo-'))
|
|
536
536
|
outDir = await mkdtemp(join(tmpdir(), 'gepa-out-'))
|
|
537
537
|
await runOk('git', ['init', '-q', '-b', 'main', loopsRepo])
|
|
538
|
+
await runOk('git', ['-C', loopsRepo, 'config', 'core.hooksPath', '/dev/null'])
|
|
538
539
|
await git(['config', 'user.email', 't@t.dev'], loopsRepo)
|
|
539
540
|
await git(['config', 'user.name', 'T'], loopsRepo)
|
|
540
541
|
await mkdir(join(loopsRepo, 'extensions', 'pi', 'prompts'), { recursive: true })
|
|
@@ -995,6 +996,7 @@ describe('integration: real adapter roundtrip', () => {
|
|
|
995
996
|
const outDir = await mkdtemp(join(tmpdir(), 'gepa-int-out-'))
|
|
996
997
|
try {
|
|
997
998
|
await runOk('git', ['init', '-q', '-b', 'main', loopsRepo])
|
|
999
|
+
await runOk('git', ['-C', loopsRepo, 'config', 'core.hooksPath', '/dev/null'])
|
|
998
1000
|
await runOk('git', ['-C', loopsRepo, 'config', 'user.email', 't@t.dev'])
|
|
999
1001
|
await runOk('git', ['-C', loopsRepo, 'config', 'user.name', 'T'])
|
|
1000
1002
|
await mkdir(join(loopsRepo, 'extensions', 'pi', 'prompts'), { recursive: true })
|
|
@@ -182,6 +182,7 @@ describe('loadFullCampaignCells + incumbentSurfaceHash (real fs/git)', () => {
|
|
|
182
182
|
|
|
183
183
|
it('computes the same incumbent hash for the unchanged tip (baseCommit == candidateCommit, empty patch)', async () => {
|
|
184
184
|
await runOk('git', ['init', '-q', '-b', 'main', repo])
|
|
185
|
+
await runOk('git', ['-C', repo, 'config', 'core.hooksPath', '/dev/null'])
|
|
185
186
|
await runOk('git', ['-C', repo, 'config', 'user.email', 't@t.dev'])
|
|
186
187
|
await runOk('git', ['-C', repo, 'config', 'user.name', 'T'])
|
|
187
188
|
await writeFile(join(repo, 'a.txt'), 'x\n')
|
|
@@ -65,18 +65,16 @@ describe('run process-group timeout', () => {
|
|
|
65
65
|
const dir = await mkdtemp(join(tmpdir(), 'swe-proc-tree-'))
|
|
66
66
|
dirs.push(dir)
|
|
67
67
|
const pidFile = join(dir, 'grandchild.pid')
|
|
68
|
-
const grandchild = `process.on('SIGTERM',()=>{}); setInterval(()=>{},1000)`
|
|
68
|
+
const grandchild = `process.on('SIGTERM',()=>{}); require('node:fs').writeFileSync(process.argv[1],String(process.pid)); setInterval(()=>{},1000)`
|
|
69
69
|
const parent = [
|
|
70
70
|
`const {spawn}=require('node:child_process')`,
|
|
71
|
-
`
|
|
72
|
-
`const child=spawn(process.execPath,['-e',${JSON.stringify(grandchild)}],{stdio:'ignore'})`,
|
|
73
|
-
`fs.writeFileSync(process.argv[1],String(child.pid))`,
|
|
71
|
+
`spawn(process.execPath,['-e',${JSON.stringify(grandchild)},process.argv[1]],{stdio:'ignore'})`,
|
|
74
72
|
`process.on('SIGTERM',()=>process.exit(0))`,
|
|
75
73
|
`setInterval(()=>{},1000)`,
|
|
76
74
|
].join(';')
|
|
77
75
|
|
|
78
76
|
const result = await run(process.execPath, ['-e', parent, pidFile], {
|
|
79
|
-
timeoutMs:
|
|
77
|
+
timeoutMs: 2_000,
|
|
80
78
|
killGraceMs: 50,
|
|
81
79
|
})
|
|
82
80
|
const grandchildPid = Number(await readFile(pidFile, 'utf8'))
|
|
@@ -358,6 +358,7 @@ describe('fanOutLoopsGenerator', () => {
|
|
|
358
358
|
loopsRepo = await mkdtemp(join(tmpdir(), 'fanout-repo-'))
|
|
359
359
|
outDir = await mkdtemp(join(tmpdir(), 'fanout-out-'))
|
|
360
360
|
await runOk('git', ['init', '-q', '-b', 'main', loopsRepo])
|
|
361
|
+
await runOk('git', ['-C', loopsRepo, 'config', 'core.hooksPath', '/dev/null'])
|
|
361
362
|
await git(['config', 'user.email', 't@t.dev'], loopsRepo)
|
|
362
363
|
await git(['config', 'user.name', 'T'], loopsRepo)
|
|
363
364
|
await writeFile(join(loopsRepo, 'src.ts'), 'base\n')
|
|
@@ -399,6 +399,8 @@ export async function materializeFactoryWorkspace(
|
|
|
399
399
|
await writeFile(join(dest, 'SPEC.md'), inst.spec)
|
|
400
400
|
|
|
401
401
|
await runOk('git', ['-C', dest, 'init', '-q', '-b', 'work'])
|
|
402
|
+
// This synthetic benchmark repository must not execute the host's personal Git hooks.
|
|
403
|
+
await runOk('git', ['-C', dest, 'config', 'core.hooksPath', '/dev/null'])
|
|
402
404
|
await runOk('git', ['-C', dest, 'config', 'user.email', 'factory-bench@local'])
|
|
403
405
|
await runOk('git', ['-C', dest, 'config', 'user.name', 'factory-bench'])
|
|
404
406
|
await runOk('git', ['-C', dest, 'add', '-A'])
|
|
@@ -21,6 +21,7 @@ describe('scratch worktrees', () => {
|
|
|
21
21
|
const output = await mkdtemp(join(tmpdir(), 'scratch-worktree-out-'))
|
|
22
22
|
roots.push(repository, output)
|
|
23
23
|
await runOk('git', ['init', '-q', '-b', 'main', repository])
|
|
24
|
+
await runOk('git', ['-C', repository, 'config', 'core.hooksPath', '/dev/null'])
|
|
24
25
|
await runOk('git', ['-C', repository, 'config', 'user.email', 'test@example.com'])
|
|
25
26
|
await runOk('git', ['-C', repository, 'config', 'user.name', 'Test'])
|
|
26
27
|
await writeFile(join(repository, 'seed.txt'), 'seed\n')
|
|
@@ -51,5 +52,5 @@ describe('scratch worktrees', () => {
|
|
|
51
52
|
await runOk('git', ['-C', repository, 'worktree', 'list', '--porcelain'])
|
|
52
53
|
).stdout
|
|
53
54
|
expect(listed.match(/^worktree /gmu)).toHaveLength(1)
|
|
54
|
-
})
|
|
55
|
+
}, 60_000)
|
|
55
56
|
})
|
|
@@ -93,9 +93,13 @@ describe('SWE worker prompts', () => {
|
|
|
93
93
|
|
|
94
94
|
describe('SWE temporary directory', () => {
|
|
95
95
|
it('stays absolute when model temperature is configured through TEMPERATURE', () => {
|
|
96
|
+
const priorTmpdir = process.env.TMPDIR
|
|
97
|
+
const priorTmp = process.env.TMP
|
|
96
98
|
const priorTemp = process.env.TEMP
|
|
97
99
|
const priorTemperature = process.env.TEMPERATURE
|
|
98
100
|
try {
|
|
101
|
+
delete process.env.TMPDIR
|
|
102
|
+
delete process.env.TMP
|
|
99
103
|
delete process.env.TEMP
|
|
100
104
|
process.env.TEMPERATURE = '0.8'
|
|
101
105
|
assert.equal(isAbsolute(absoluteSweTempDir()), true)
|
|
@@ -103,6 +107,10 @@ describe('SWE temporary directory', () => {
|
|
|
103
107
|
process.env.TEMP = '0.8'
|
|
104
108
|
assert.throws(() => absoluteSweTempDir(), /must be absolute.*TEMPERATURE/)
|
|
105
109
|
} finally {
|
|
110
|
+
if (priorTmpdir === undefined) delete process.env.TMPDIR
|
|
111
|
+
else process.env.TMPDIR = priorTmpdir
|
|
112
|
+
if (priorTmp === undefined) delete process.env.TMP
|
|
113
|
+
else process.env.TMP = priorTmp
|
|
106
114
|
if (priorTemp === undefined) delete process.env.TEMP
|
|
107
115
|
else process.env.TEMP = priorTemp
|
|
108
116
|
if (priorTemperature === undefined) delete process.env.TEMPERATURE
|
|
@@ -197,7 +205,7 @@ describe('isInsideJail (realpath containment)', () => {
|
|
|
197
205
|
symlinkSync('/etc', link)
|
|
198
206
|
// `resolveInJail` does `realpathSync(join(ws.dir, relPath))` then this containment check.
|
|
199
207
|
const real = realpathSync(join(dir, 'escape/passwd'))
|
|
200
|
-
assert.equal(real, '/etc/passwd')
|
|
208
|
+
assert.equal(real, realpathSync('/etc/passwd'))
|
|
201
209
|
assert.equal(isInsideJail(jailRoot, real), false)
|
|
202
210
|
})
|
|
203
211
|
|
|
@@ -89,7 +89,10 @@ printf 'argv:%s\\n' "$*"
|
|
|
89
89
|
assert.match(meteredResult.out.command, /'tangle-router\/deepseek-v4-flash'/)
|
|
90
90
|
assert.match(
|
|
91
91
|
meteredResult.out.stdout,
|
|
92
|
-
|
|
92
|
+
// `--auto` is opencode's permission bypass, emitted for an unattended container run the
|
|
93
|
+
// same way claude-code and codex emit theirs. Without it the harness denies writes outside
|
|
94
|
+
// the working directory and reports "The user rejected permission".
|
|
95
|
+
/argv:exec -i --workdir \/work -e OPENAI_BASE_URL=http:\/\/router\.test cid \/bin\/sh -c exec 'opencode' 'run' 'echo hello' '--auto' '-m' 'tangle-router\/deepseek-v4-flash'/,
|
|
93
96
|
)
|
|
94
97
|
assert.deepEqual(meteredResult.spent.tokens, { input: 7, output: 11 })
|
|
95
98
|
assert.equal(meteredResult.spent.usd, 0.004)
|
|
@@ -113,6 +113,7 @@ async function main(): Promise<void> {
|
|
|
113
113
|
blobs,
|
|
114
114
|
makeWorkerAgent,
|
|
115
115
|
perWorker: { maxIterations: 40, maxTokens: 200_000 },
|
|
116
|
+
toolNames: ['spawn_worker', 'observe_agent', 'await_event', 'stop'],
|
|
116
117
|
host: '0.0.0.0',
|
|
117
118
|
onEvent: (event) => logEvent('bus', event),
|
|
118
119
|
})
|
|
@@ -200,11 +200,11 @@ class OpenCodeSupervisorAgent(OpenCodeRouterAgent):
|
|
|
200
200
|
try:
|
|
201
201
|
supervised_instruction = (
|
|
202
202
|
"You are the supervisor. You have an MCP server named \"coordination\" with "
|
|
203
|
-
"tools including
|
|
203
|
+
"tools including spawn_worker(profile, task), observe_agent(workerId), "
|
|
204
204
|
"await_event(), and stop(). A spawned worker runs a full coding agent INSIDE "
|
|
205
205
|
"THIS SAME container and its file changes persist here, so you may delegate "
|
|
206
206
|
"concrete sub-tasks (e.g. \"install perl and 7z\", \"crack the hash with john\") "
|
|
207
|
-
"to workers via
|
|
207
|
+
"to workers via spawn_worker, then observe_agent/await_event for their results. "
|
|
208
208
|
"Delegate independent or heavy sub-tasks to workers when useful; do the rest "
|
|
209
209
|
"yourself. Complete the task fully.\n\n"
|
|
210
210
|
f"TASK:\n{instruction}"
|