@tangle-network/agent-bench 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.6
4
+
5
+ - Require typed proposal findings with explicit search or production origin throughout the SWE improvement loop.
6
+ - Consume Runtime 0.108.0 with Eval 0.134.1, Interface 0.36.0, Knowledge 6.1.7, Materialize 0.9.2, and Sandbox 0.15.2.
7
+
8
+ ## 0.4.5
9
+
10
+ - Allow the zero-model Pier proof to complete a cold separate-verifier image build before its task and overall execution deadlines.
11
+ - Consume Runtime 0.107.5 with Eval 0.133.3, Interface 0.36.0, Knowledge 6.1.5, and Sandbox 0.15.1.
12
+
3
13
  ## 0.4.4
4
14
 
5
15
  - Consume Runtime 0.107.2 and Sandbox 0.15.0 with the current Eval, Interface, and Knowledge packages.
@@ -16,7 +16,7 @@ tags = ["no-model", "separate-verifier"]
16
16
  timeout_sec = 60.0
17
17
 
18
18
  [verifier]
19
- timeout_sec = 60.0
19
+ timeout_sec = 300.0
20
20
  environment_mode = "separate"
21
21
 
22
22
  [verifier.environment]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-bench",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "type": "module",
5
5
  "description": "Benchmark adapters and execution for agent-runtime across coding, tool-use, RAG, memory, browser, and terminal tasks.",
6
6
  "repository": {
@@ -25,11 +25,11 @@
25
25
  }
26
26
  },
27
27
  "dependencies": {
28
- "@tangle-network/agent-eval": "0.133.0",
28
+ "@tangle-network/agent-eval": "0.134.1",
29
29
  "@tangle-network/agent-interface": "0.36.0",
30
- "@tangle-network/agent-knowledge": "6.1.2",
31
- "@tangle-network/sandbox": "0.15.0",
32
- "@tangle-network/agent-runtime": "0.107.2"
30
+ "@tangle-network/agent-knowledge": "6.1.7",
31
+ "@tangle-network/sandbox": "0.15.2",
32
+ "@tangle-network/agent-runtime": "0.108.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@arethetypeswrong/cli": "0.18.5",
@@ -435,7 +435,7 @@ ${proofArm === 'success' ? "(task / 'src/status.txt').write_text('ready\\nowner=
435
435
  workspace: taskWorkspace,
436
436
  evaluatorTaskContainer: container,
437
437
  limits: {
438
- timeoutMs: 180_000,
438
+ timeoutMs: 600_000,
439
439
  maxSteps: 8,
440
440
  maxModelCalls: 0,
441
441
  maxInputTokens: 0,
@@ -9,7 +9,7 @@ import { existsSync } from 'node:fs'
9
9
  import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
10
10
  import { tmpdir } from 'node:os'
11
11
  import { join } from 'node:path'
12
- import type { AnalystFinding } from '@tangle-network/agent-eval'
12
+ import type { ProposalFinding } from '@tangle-network/agent-eval'
13
13
  import { afterEach, beforeEach, describe, expect, it } from 'vitest'
14
14
  import {
15
15
  ACTIVATION_PREDICATE_RELPATH,
@@ -236,8 +236,7 @@ describe('activation-predicate prefilter', () => {
236
236
 
237
237
  const generatorArgs = (candidateIndex: number) => ({
238
238
  worktreePath: driverWt,
239
- report: undefined,
240
- findings: [] as AnalystFinding[],
239
+ findings: [] as ProposalFinding[],
241
240
  maxShots: 1,
242
241
  signal: new AbortController().signal,
243
242
  generation: 0,
@@ -553,7 +553,6 @@ describe('fanOutLoopsGenerator with the gepa seat', () => {
553
553
 
554
554
  const generatorArgs = (candidateIndex: number) => ({
555
555
  worktreePath: driverWt,
556
- report: undefined,
557
556
  findings: [],
558
557
  maxShots: 1,
559
558
  signal: new AbortController().signal,
@@ -1032,7 +1031,6 @@ describe('integration: real adapter roundtrip', () => {
1032
1031
  )
1033
1032
  const result = await gen.generate({
1034
1033
  worktreePath: driverWt,
1035
- report: undefined,
1036
1034
  findings: [],
1037
1035
  maxShots: 1,
1038
1036
  signal: new AbortController().signal,
@@ -73,7 +73,11 @@ import {
73
73
  } from '@tangle-network/agent-runtime'
74
74
  import { runLocalHarness } from '@tangle-network/agent-runtime/mcp'
75
75
  import { canonicalCandidateDigest } from '@tangle-network/agent-interface'
76
- import { makeFinding } from '@tangle-network/agent-eval'
76
+ import {
77
+ makeProposalFinding,
78
+ type AnalystFinding,
79
+ type ProposalFinding,
80
+ } from '@tangle-network/agent-eval'
77
81
  import {
78
82
  FsLabeledScenarioStore,
79
83
  surfaceHash,
@@ -1111,6 +1115,10 @@ export async function removeEvalWorktree(loopsRepo: string, dest: string): Promi
1111
1115
  * artifact path its gate checks for. */
1112
1116
  export const RAW_TRACE_DIAGNOSIS_PATH = '.improve/raw-trace-diagnosis.md'
1113
1117
 
1118
+ function asSearchProposalFinding(finding: AnalystFinding): ProposalFinding {
1119
+ return { ...finding, proposal_origin: 'search' }
1120
+ }
1121
+
1114
1122
  export function changeSpaceInstruction(space: ChangeSpace = LOOPS_CHANGE_SPACE): string {
1115
1123
  return [
1116
1124
  'DECLARED CHANGE-SPACE (hard constraint, enforced by an automated gate):',
@@ -1123,7 +1131,7 @@ export function changeSpaceInstruction(space: ChangeSpace = LOOPS_CHANGE_SPACE):
1123
1131
  ].join('\n')
1124
1132
  }
1125
1133
 
1126
- export function round4BuildPrompt(args: { report: unknown; findings: Array<Record<string, unknown>> }): string {
1134
+ export function round4BuildPrompt(args: { findings: ReadonlyArray<ProposalFinding> }): string {
1127
1135
  const lines: string[] = [
1128
1136
  'You are the optimizer of the "loops" pi SUPERVISOR — an agent that plans, spawns sandboxed coding',
1129
1137
  'workers, and settles a delivered patch for SWE-bench Verified instances (glm-5.2 in both seats, frozen).',
@@ -1274,8 +1282,7 @@ export function constrainedLoopsGenerator(config: OuterLoopConfig): CandidateGen
1274
1282
  const inner = agenticGenerator({
1275
1283
  harness: config.proposerHarness,
1276
1284
  timeoutMs: config.proposerTimeoutMs,
1277
- buildPrompt: (args) =>
1278
- round4BuildPrompt(args as unknown as { report: unknown; findings: Array<Record<string, unknown>> }),
1285
+ buildPrompt: round4BuildPrompt,
1279
1286
  verify: loopsCandidateVerifier(config.loopsRepo),
1280
1287
  runHarness: (options) => runLocalHarness({ ...options, env: proposerShotEnv(config.proposerHarness) }),
1281
1288
  // Three runs died as "author shot exited with code 1" with the shot's
@@ -1982,7 +1989,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1982
1989
 
1983
1990
  // ── diagnosis at the analyzeGeneration seam ──────────────────────────
1984
1991
  const rawTrace = rawTraceDistiller<Scenario, R4Artifact>({ fallbackFindings: [] })
1985
- const steeringFinding = makeFinding({
1992
+ const steeringFinding = makeProposalFinding({
1986
1993
  analyst_id: 'round4-protocol',
1987
1994
  severity: 'high',
1988
1995
  area: 'constraint',
@@ -1992,13 +1999,14 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1992
1999
  '(plus the .improve/ diagnosis artifact). Judge, verify scripts, task prompts, model ids and budgets are immutable.',
1993
2000
  recommended_action: 'Keep every edit inside the change-space; out-of-space candidate diffs are rejected before evaluation.',
1994
2001
  evidence_refs: [],
2002
+ proposal_origin: 'search',
1995
2003
  })
1996
2004
  const analyzeGeneration = async (input: {
1997
2005
  generation: number
1998
2006
  runDir: string
1999
2007
  candidates: Array<{ surfaceHash: string; composite: number; campaign: unknown }>
2000
2008
  history: unknown[]
2001
- }): Promise<unknown[]> => {
2009
+ }): Promise<ProposalFinding[]> => {
2002
2010
  signal?.throwIfAborted()
2003
2011
  const runs: SupRunArtifacts[] = []
2004
2012
  if (input.generation === -1) {
@@ -2038,7 +2046,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
2038
2046
  })
2039
2047
  }
2040
2048
  }
2041
- let ensembleFindings: unknown[] = []
2049
+ let ensembleFindings: ProposalFinding[] = []
2042
2050
  if (runs.length > 0) {
2043
2051
  try {
2044
2052
  const scratch = join(config.outDir, 'diagnosis', `gen-${input.generation}`)
@@ -2058,7 +2066,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
2058
2066
  ensembleFindings = fusedToAnalystFindings(ensemble.fused, {
2059
2067
  dirs: [...new Set(runs.map((r) => r.dir))],
2060
2068
  totalAnalysts: analysts.length,
2061
- })
2069
+ }).map(asSearchProposalFinding)
2062
2070
  } catch (cause) {
2063
2071
  if (signal?.aborted) throw signal.reason
2064
2072
  // A dead router must not kill the round: the raw-trace context below
@@ -2084,7 +2092,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
2084
2092
  }),
2085
2093
  }
2086
2094
  signal?.throwIfAborted()
2087
- const rawFindings = (await rawTrace(censoredInput as Parameters<typeof rawTrace>[0])) as unknown[]
2095
+ const rawFindings = await rawTrace(censoredInput as Parameters<typeof rawTrace>[0])
2088
2096
  signal?.throwIfAborted()
2089
2097
  return [steeringFinding, ...ensembleFindings, ...rawFindings]
2090
2098
  }
@@ -9,6 +9,7 @@ import { existsSync } from 'node:fs'
9
9
  import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
10
10
  import { tmpdir } from 'node:os'
11
11
  import { join } from 'node:path'
12
+ import { makeProposalFinding } from '@tangle-network/agent-eval'
12
13
  import { describe, expect, it } from 'vitest'
13
14
  import { runOk } from './proc.ts'
14
15
  import {
@@ -574,9 +575,17 @@ describe('launch guards', () => {
574
575
  describe('round4BuildPrompt', () => {
575
576
  it('declares the change-space and renders findings', () => {
576
577
  const prompt = round4BuildPrompt({
577
- report: undefined,
578
578
  findings: [
579
- { severity: 'high', claim: 'fix placement mismatch', recommended_action: 'settle where maintainers expect' },
579
+ makeProposalFinding({
580
+ analyst_id: 'test',
581
+ severity: 'high',
582
+ area: 'mechanism',
583
+ claim: 'fix placement mismatch',
584
+ recommended_action: 'settle where maintainers expect',
585
+ evidence_refs: [],
586
+ confidence: 1,
587
+ proposal_origin: 'search',
588
+ }),
580
589
  ],
581
590
  })
582
591
  expect(prompt).toContain('DECLARED CHANGE-SPACE')
@@ -591,8 +600,17 @@ describe('round4BuildPrompt', () => {
591
600
 
592
601
  it('adds the raw-trace evidence contract when raw-trace findings are present', () => {
593
602
  const prompt = round4BuildPrompt({
594
- report: undefined,
595
- findings: [{ severity: 'high', area: 'raw-trace-context', claim: 'traces at /run/gen-0' }],
603
+ findings: [
604
+ makeProposalFinding({
605
+ analyst_id: 'raw-trace-distiller',
606
+ severity: 'high',
607
+ area: 'raw-trace-context',
608
+ claim: 'traces at /run/gen-0',
609
+ evidence_refs: [],
610
+ confidence: 1,
611
+ proposal_origin: 'search',
612
+ }),
613
+ ],
596
614
  })
597
615
  expect(prompt).toContain('Raw trace evidence requirement')
598
616
  expect(prompt).toContain(RAW_TRACE_DIAGNOSIS_PATH)
@@ -49,7 +49,7 @@ import {
49
49
  } from '@tangle-network/agent-runtime'
50
50
  import { runLocalHarness } from '@tangle-network/agent-runtime/mcp'
51
51
  import type { AgentProfile } from '@tangle-network/agent-interface'
52
- import type { AnalystFinding, CostLedgerHandle } from '@tangle-network/agent-eval'
52
+ import type { CostLedgerHandle, ProposalFinding } from '@tangle-network/agent-eval'
53
53
  import {
54
54
  changeSpaceViolations,
55
55
  loopsCandidateVerifier,
@@ -297,7 +297,7 @@ export function parentsPromptSection(parents: ParetoParentContext[]): string {
297
297
  * stay (round4BuildPrompt), and the TASK is replaced with an explicit
298
298
  * coherent-union merge of the parents' diffs. */
299
299
  export function mergeAuthorPrompt(
300
- args: { report: unknown; findings: Array<Record<string, unknown>> },
300
+ args: { findings: ReadonlyArray<ProposalFinding> },
301
301
  spec: ProposerSpec,
302
302
  parents: ParetoParentContext[],
303
303
  ): string {
@@ -349,7 +349,10 @@ function isSteeringOrRawTrace(f: Record<string, unknown>): boolean {
349
349
  }
350
350
 
351
351
  /** Slice the diagnosis findings for one proposer. Pure. */
352
- export function sliceFindings(findings: AnalystFinding[], slice: ProposerSpec['diagnosisSlice']): AnalystFinding[] {
352
+ export function sliceFindings(
353
+ findings: ReadonlyArray<ProposalFinding>,
354
+ slice: ProposerSpec['diagnosisSlice'],
355
+ ): ProposalFinding[] {
353
356
  if (slice === undefined || slice === 'all') return findings
354
357
  return findings.filter((finding) => {
355
358
  const f = finding as unknown as Record<string, unknown>
@@ -380,7 +383,7 @@ function appendGen5Sections(prompt: string, extras: Gen5PromptExtras): string {
380
383
  * gen-5 briefing/activation sections when configured. A merge-seat spec gets
381
384
  * the dedicated merge prompt instead (gen-5 sections still apply). */
382
385
  export function proposerBuildPrompt(
383
- args: { report: unknown; findings: Array<Record<string, unknown>> },
386
+ args: { findings: ReadonlyArray<ProposalFinding> },
384
387
  spec: ProposerSpec,
385
388
  parents: ParetoParentContext[] = [],
386
389
  extras: Gen5PromptExtras = {},
@@ -543,16 +546,11 @@ function defaultAuthor(config: OuterLoopConfig, deps: FanOutDeps): AuthorFn {
543
546
  harness,
544
547
  ...(profile ? { profile } : {}),
545
548
  timeoutMs: config.proposerTimeoutMs,
546
- buildPrompt: (a) =>
547
- proposerBuildPrompt(
548
- a as unknown as { report: unknown; findings: Array<Record<string, unknown>> },
549
- proposer,
550
- deps.parents ?? [],
551
- {
552
- ...(deps.briefing ? { briefing: deps.briefing } : {}),
553
- ...(config.activationGate === true ? { activationGate: true } : {}),
554
- },
555
- ),
549
+ buildPrompt: (args) =>
550
+ proposerBuildPrompt(args, proposer, deps.parents ?? [], {
551
+ ...(deps.briefing ? { briefing: deps.briefing } : {}),
552
+ ...(config.activationGate === true ? { activationGate: true } : {}),
553
+ }),
556
554
  verify: loopsCandidateVerifier(config.loopsRepo),
557
555
  runHarness: (options) => runLocalHarness({ ...options, env: proposerShotEnv(harness) }),
558
556
  onShotCompleted: proposerShotHooks({
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
2
2
  import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join } from 'node:path'
5
- import type { AnalystFinding } from '@tangle-network/agent-eval'
5
+ import { makeProposalFinding, type ProposalFinding } from '@tangle-network/agent-eval'
6
6
  import { afterEach, beforeEach, describe, expect, it } from 'vitest'
7
7
  import {
8
8
  defaultGen3Config,
@@ -37,19 +37,20 @@ import { runOk } from './proc.ts'
37
37
  // Pure pieces.
38
38
  // ---------------------------------------------------------------------------
39
39
 
40
- const finding = (over: Record<string, unknown>): AnalystFinding =>
41
- ({
42
- schema_version: '1.0.0',
43
- finding_id: `f-${Math.random().toString(36).slice(2)}`,
40
+ const finding = (
41
+ over: Partial<Pick<ProposalFinding, 'analyst_id' | 'area' | 'claim'>>,
42
+ ): ProposalFinding =>
43
+ makeProposalFinding({
44
44
  analyst_id: 'analyst',
45
- produced_at: new Date(0).toISOString(),
46
45
  severity: 'high',
47
46
  area: 'mechanism',
48
47
  claim: 'workers drop build artifacts on clone',
49
48
  evidence_refs: [],
50
49
  confidence: 0.9,
51
50
  ...over,
52
- }) as unknown as AnalystFinding
51
+ proposal_origin: 'search',
52
+ produced_at: new Date(0).toISOString(),
53
+ })
53
54
 
54
55
  describe('sliceFindings', () => {
55
56
  const mech = finding({ claim: 'sandbox clone drops untracked build artifacts' })
@@ -73,7 +74,7 @@ describe('sliceFindings', () => {
73
74
  describe('proposerBuildPrompt', () => {
74
75
  it('appends the lens AFTER the shared protocol prompt, leaving the change-space text intact', () => {
75
76
  const spec: ProposerSpec = { name: 'x', harness: 'claude', lens: 'Prefer code-path fixes.' }
76
- const prompt = proposerBuildPrompt({ report: undefined, findings: [] }, spec)
77
+ const prompt = proposerBuildPrompt({ findings: [] }, spec)
77
78
  expect(prompt).toContain('DECLARED CHANGE-SPACE')
78
79
  expect(prompt.indexOf('DECLARED CHANGE-SPACE')).toBeLessThan(prompt.indexOf('YOUR AUTHORING LENS (x)'))
79
80
  expect(prompt).toContain('Prefer code-path fixes.')
@@ -81,7 +82,7 @@ describe('proposerBuildPrompt', () => {
81
82
 
82
83
  it('is the bare round prompt without a lens', () => {
83
84
  const spec: ProposerSpec = { name: 'x', harness: 'claude' }
84
- expect(proposerBuildPrompt({ report: undefined, findings: [] }, spec)).not.toContain('AUTHORING LENS')
85
+ expect(proposerBuildPrompt({ findings: [] }, spec)).not.toContain('AUTHORING LENS')
85
86
  })
86
87
  })
87
88
 
@@ -134,7 +135,7 @@ describe('resolveAuthorProfile (pinned models)', () => {
134
135
  describe('proposerBuildPrompt with pareto parents', () => {
135
136
  it('appends the parents section (evidence + diffs) after the protocol prompt and lens', () => {
136
137
  const spec: ProposerSpec = { name: 'x', harness: 'claude', lens: 'Prefer code-path fixes.' }
137
- const prompt = proposerBuildPrompt({ report: undefined, findings: [] }, spec, PARENTS)
138
+ const prompt = proposerBuildPrompt({ findings: [] }, spec, PARENTS)
138
139
  expect(prompt).toContain('DECLARED CHANGE-SPACE')
139
140
  expect(prompt).toContain('PARETO PARENTS')
140
141
  expect(prompt.indexOf('YOUR AUTHORING LENS')).toBeLessThan(prompt.indexOf('PARETO PARENTS'))
@@ -146,7 +147,7 @@ describe('proposerBuildPrompt with pareto parents', () => {
146
147
 
147
148
  it('leaves the prompt untouched when no parents are seeded (gen-3 behavior)', () => {
148
149
  const spec: ProposerSpec = { name: 'x', harness: 'claude' }
149
- expect(proposerBuildPrompt({ report: undefined, findings: [] }, spec)).not.toContain('PARETO PARENTS')
150
+ expect(proposerBuildPrompt({ findings: [] }, spec)).not.toContain('PARETO PARENTS')
150
151
  expect(parentsPromptSection(PARENTS)).toContain('measured evidence')
151
152
  })
152
153
  })
@@ -155,7 +156,7 @@ describe('mergeAuthorPrompt', () => {
155
156
  const spec: ProposerSpec = { name: 'merge-author', harness: 'claude', merge: true }
156
157
 
157
158
  it('keeps the change-space contract and presents BOTH parent diffs with the coherent-union task', () => {
158
- const prompt = proposerBuildPrompt({ report: undefined, findings: [] }, spec, PARENTS)
159
+ const prompt = proposerBuildPrompt({ findings: [] }, spec, PARENTS)
159
160
  expect(prompt).toContain('DECLARED CHANGE-SPACE')
160
161
  expect(prompt).toContain('MERGE SEAT')
161
162
  expect(prompt).toContain('UNION of the')
@@ -169,8 +170,8 @@ describe('mergeAuthorPrompt', () => {
169
170
  })
170
171
 
171
172
  it('fails loud with fewer than two parents', () => {
172
- expect(() => mergeAuthorPrompt({ report: undefined, findings: [] }, spec, [PARENTS[0]!])).toThrow(/>=2/)
173
- expect(() => proposerBuildPrompt({ report: undefined, findings: [] }, spec, [])).toThrow(/>=2/)
173
+ expect(() => mergeAuthorPrompt({ findings: [] }, spec, [PARENTS[0]!])).toThrow(/>=2/)
174
+ expect(() => proposerBuildPrompt({ findings: [] }, spec, [])).toThrow(/>=2/)
174
175
  })
175
176
  })
176
177
 
@@ -347,8 +348,7 @@ describe('fanOutLoopsGenerator', () => {
347
348
 
348
349
  const generatorArgs = (candidateIndex: number) => ({
349
350
  worktreePath: driverWt,
350
- report: undefined,
351
- findings: [] as AnalystFinding[],
351
+ findings: [] as ProposalFinding[],
352
352
  maxShots: 1,
353
353
  signal: new AbortController().signal,
354
354
  generation: 0,
@@ -586,7 +586,7 @@ describe('gen-5 prompt sections', () => {
586
586
 
587
587
  it('appends EVIDENCE MAP + briefing + activation contract after the protocol prompt', () => {
588
588
  const spec: ProposerSpec = { name: 'x', harness: 'claude' }
589
- const prompt = proposerBuildPrompt({ report: undefined, findings: [] }, spec, [], {
589
+ const prompt = proposerBuildPrompt({ findings: [] }, spec, [], {
590
590
  briefing,
591
591
  activationGate: true,
592
592
  })
@@ -599,7 +599,7 @@ describe('gen-5 prompt sections', () => {
599
599
 
600
600
  it('the merge seat gets the gen-5 sections too', () => {
601
601
  const spec: ProposerSpec = { name: 'merge-author', harness: 'claude', merge: true }
602
- const prompt = proposerBuildPrompt({ report: undefined, findings: [] }, spec, PARENTS, {
602
+ const prompt = proposerBuildPrompt({ findings: [] }, spec, PARENTS, {
603
603
  briefing,
604
604
  activationGate: true,
605
605
  })
@@ -610,9 +610,9 @@ describe('gen-5 prompt sections', () => {
610
610
 
611
611
  it('leaves gen-3/gen-4 prompts byte-identical when no extras are passed', () => {
612
612
  const spec: ProposerSpec = { name: 'x', harness: 'claude' }
613
- const legacy = proposerBuildPrompt({ report: undefined, findings: [] }, spec, PARENTS)
613
+ const legacy = proposerBuildPrompt({ findings: [] }, spec, PARENTS)
614
614
  expect(legacy).not.toContain('EVIDENCE MAP')
615
615
  expect(legacy).not.toContain('ACTIVATION PREDICATE')
616
- expect(proposerBuildPrompt({ report: undefined, findings: [] }, spec, PARENTS, {})).toBe(legacy)
616
+ expect(proposerBuildPrompt({ findings: [] }, spec, PARENTS, {})).toBe(legacy)
617
617
  })
618
618
  })
@@ -3,11 +3,25 @@ import { tmpdir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
  import { afterAll, describe, expect, it } from 'vitest'
5
5
  import { runSupervisorShim, supervisorDriverKillGraceMs } from './arms.ts'
6
- import { run, TIMEOUT_RC } from './proc.ts'
6
+ import { ABORT_RC, run } from './proc.ts'
7
7
 
8
8
  const dirs: string[] = []
9
9
  const workerPids: number[] = []
10
10
 
11
+ async function waitForFile(path: string, timeoutMs = 10_000): Promise<void> {
12
+ const deadline = Date.now() + timeoutMs
13
+ while (Date.now() < deadline) {
14
+ try {
15
+ await access(path)
16
+ return
17
+ } catch (error) {
18
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
19
+ }
20
+ await new Promise((resolve) => setTimeout(resolve, 10))
21
+ }
22
+ throw new Error(`timed out waiting for ${path}`)
23
+ }
24
+
11
25
  afterAll(async () => {
12
26
  for (const pid of workerPids) {
13
27
  try {
@@ -122,6 +136,7 @@ export default function fakeExtension(pi) {
122
136
  writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'running' }))
123
137
  appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
124
138
  appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
139
+ writeFileSync(join(ctx.cwd, 'outer-driver-ready.txt'), 'ready')
125
140
  return text('spawned supervisor ' + id)
126
141
  },
127
142
  })
@@ -140,10 +155,11 @@ export default function fakeExtension(pi) {
140
155
  `)
141
156
 
142
157
  const cancelTimeoutMs = 1_500
143
- const result = await run(process.execPath, [
158
+ const controller = new AbortController()
159
+ const resultPromise = run(process.execPath, [
144
160
  '--import', 'tsx', runSupervisorShim, extension, workspace, params,
145
161
  ], {
146
- timeoutMs: 1_000,
162
+ signal: controller.signal,
147
163
  killGraceMs: supervisorDriverKillGraceMs({
148
164
  DRIVER_CANCEL_TIMEOUT_MS: String(cancelTimeoutMs),
149
165
  }),
@@ -154,9 +170,19 @@ export default function fakeExtension(pi) {
154
170
  DRIVER_CANCEL_TIMEOUT_MS: String(cancelTimeoutMs),
155
171
  },
156
172
  })
173
+ try {
174
+ await waitForFile(join(workspace, 'outer-driver-ready.txt'))
175
+ } catch (error) {
176
+ controller.abort()
177
+ await resultPromise
178
+ throw error
179
+ }
180
+ controller.abort()
181
+ const result = await resultPromise
157
182
 
158
- expect(result.code, result.stdout + result.stderr).toBe(TIMEOUT_RC)
159
- expect(result.timedOut).toBe(true)
183
+ expect(result.code, result.stdout + result.stderr).toBe(ABORT_RC)
184
+ expect(result.aborted).toBe(true)
185
+ expect(result.timedOut).toBe(false)
160
186
  expect(await readFile(join(workspace, 'outer-cancel-settled.txt'), 'utf8')).toBe('settled')
161
187
  expect(JSON.parse(await readFile(join(workspace, '.loops', 'supervisor', 'sup-2-fake34', 'state.json'), 'utf8')))
162
188
  .toMatchObject({ status: 'cancelled' })
@@ -170,10 +196,10 @@ export default function fakeExtension(pi) {
170
196
  const extension = join(dir, 'fake-extension.mjs')
171
197
  await mkdir(workspace)
172
198
  await writeFile(params, '{}')
173
- const workerScript = `const fs=require('node:fs'); const marker=process.argv[1]; process.on('SIGTERM',()=>setTimeout(()=>{fs.writeFileSync(marker,'clean'); process.exit(0)},30)); setInterval(()=>{},1000)`
199
+ const workerScript = `const fs=require('node:fs'); const marker=process.argv[1]; const ready=process.argv[2]; process.on('SIGTERM',()=>setTimeout(()=>{fs.writeFileSync(marker,'clean'); process.exit(0)},30)); fs.writeFileSync(ready,'ready'); setInterval(()=>{},1000)`
174
200
  await writeFile(extension, `
175
201
  import { spawn } from 'node:child_process'
176
- import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
202
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'
177
203
  import { join } from 'node:path'
178
204
 
179
205
  const id = 'sup-3-signal56'
@@ -188,11 +214,13 @@ export default function fakeExtension(pi) {
188
214
  writeFileSync(join(runDir, 'state.json'), JSON.stringify({ status: 'running' }))
189
215
  appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id, label: 'root' }) + '\\n')
190
216
  appendFileSync(join(runDir, 'journal.jsonl'), JSON.stringify({ kind: 'spawned', id: id + ':s1', parent: id, label: 'w-0' }) + '\\n')
191
- child = spawn(process.execPath, ['-e', ${JSON.stringify(workerScript)}, join(ctx.cwd, 'signal-worker-cleaned.txt')], {
217
+ const readyPath = join(ctx.cwd, 'signal-worker-ready.txt')
218
+ child = spawn(process.execPath, ['-e', ${JSON.stringify(workerScript)}, join(ctx.cwd, 'signal-worker-cleaned.txt'), readyPath], {
192
219
  detached: true,
193
220
  stdio: 'ignore',
194
221
  })
195
222
  writeFileSync(join(ctx.cwd, 'signal-worker.pid'), String(child.pid))
223
+ while (!existsSync(readyPath)) await new Promise((resolve) => setTimeout(resolve, 5))
196
224
  writeFileSync(join(ctx.cwd, 'spawn-entered.txt'), 'entered')
197
225
  await new Promise((resolve) => setTimeout(resolve, 30_000))
198
226
  return text('spawned supervisor ' + id)
@@ -214,10 +242,11 @@ export default function fakeExtension(pi) {
214
242
  `)
215
243
 
216
244
  const cancelTimeoutMs = 2_000
217
- const result = await run(process.execPath, [
245
+ const controller = new AbortController()
246
+ const resultPromise = run(process.execPath, [
218
247
  '--import', 'tsx', runSupervisorShim, extension, workspace, params,
219
248
  ], {
220
- timeoutMs: 1_000,
249
+ signal: controller.signal,
221
250
  killGraceMs: supervisorDriverKillGraceMs({
222
251
  DRIVER_CANCEL_TIMEOUT_MS: String(cancelTimeoutMs),
223
252
  }),
@@ -228,11 +257,21 @@ export default function fakeExtension(pi) {
228
257
  DRIVER_CANCEL_TIMEOUT_MS: String(cancelTimeoutMs),
229
258
  },
230
259
  })
260
+ try {
261
+ await waitForFile(join(workspace, 'spawn-entered.txt'))
262
+ } catch (error) {
263
+ controller.abort()
264
+ await resultPromise
265
+ throw error
266
+ }
267
+ controller.abort()
268
+ const result = await resultPromise
231
269
  const workerPid = Number(await readFile(join(workspace, 'signal-worker.pid'), 'utf8'))
232
270
  workerPids.push(workerPid)
233
271
 
234
- expect(result.code, result.stdout + result.stderr).toBe(TIMEOUT_RC)
235
- expect(result.timedOut).toBe(true)
272
+ expect(result.code, result.stdout + result.stderr).toBe(ABORT_RC)
273
+ expect(result.aborted).toBe(true)
274
+ expect(result.timedOut).toBe(false)
236
275
  expect(await readFile(join(workspace, 'spawn-entered.txt'), 'utf8')).toBe('entered')
237
276
  expect(await readFile(join(workspace, 'signal-worker-cleaned.txt'), 'utf8')).toBe('clean')
238
277
  expect(await readFile(join(workspace, 'signal-cancel-settled.txt'), 'utf8')).toBe('settled')
@@ -0,0 +1,72 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ const gitCalls = vi.hoisted(() => ({
7
+ activeByRepository: new Map<string, number>(),
8
+ maximumByRepository: new Map<string, number>(),
9
+ active: 0,
10
+ maximum: 0,
11
+ }))
12
+
13
+ vi.mock('./proc', () => {
14
+ const recordGitCall = async (_binary: string, arguments_: string[]) => {
15
+ const repository = arguments_[1] ?? ''
16
+ const activeForRepository = (gitCalls.activeByRepository.get(repository) ?? 0) + 1
17
+ gitCalls.activeByRepository.set(repository, activeForRepository)
18
+ gitCalls.maximumByRepository.set(
19
+ repository,
20
+ Math.max(gitCalls.maximumByRepository.get(repository) ?? 0, activeForRepository),
21
+ )
22
+ gitCalls.active += 1
23
+ gitCalls.maximum = Math.max(gitCalls.maximum, gitCalls.active)
24
+ await new Promise((resolve) => setTimeout(resolve, 5))
25
+ gitCalls.active -= 1
26
+ gitCalls.activeByRepository.set(repository, activeForRepository - 1)
27
+ return { code: 0, stdout: '', stderr: '' }
28
+ }
29
+ return {
30
+ run: vi.fn(recordGitCall),
31
+ runOk: vi.fn(recordGitCall),
32
+ }
33
+ })
34
+
35
+ import {
36
+ createDetachedWorktree,
37
+ pruneDetachedWorktrees,
38
+ removeDetachedWorktree,
39
+ } from './scratch-worktree.ts'
40
+
41
+ describe('scratch worktree mutation serialization', () => {
42
+ const roots: string[] = []
43
+
44
+ afterEach(async () => {
45
+ gitCalls.activeByRepository.clear()
46
+ gitCalls.maximumByRepository.clear()
47
+ gitCalls.active = 0
48
+ gitCalls.maximum = 0
49
+ await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
50
+ })
51
+
52
+ it('serializes metadata changes per repository without blocking separate repositories', async () => {
53
+ const output = await mkdtemp(join(tmpdir(), 'scratch-worktree-serialization-'))
54
+ roots.push(output)
55
+ const operations = ['repository-a', 'repository-b'].flatMap((repository) =>
56
+ Array.from({ length: 4 }, (_, index) => {
57
+ const destination = join(output, `${repository}-candidate-${index}`)
58
+ return [
59
+ createDetachedWorktree(repository, 'commit', destination),
60
+ removeDetachedWorktree(repository, destination),
61
+ pruneDetachedWorktrees(repository),
62
+ ]
63
+ }),
64
+ )
65
+
66
+ await Promise.all(operations.flat())
67
+
68
+ expect(gitCalls.maximumByRepository.get('repository-a')).toBe(1)
69
+ expect(gitCalls.maximumByRepository.get('repository-b')).toBe(1)
70
+ expect(gitCalls.maximum).toBe(2)
71
+ })
72
+ })
@@ -1,34 +1,64 @@
1
1
  import { rm } from 'node:fs/promises'
2
2
  import { run, runOk } from './proc'
3
3
 
4
+ // Git worktree subcommands mutate shared metadata under .git/worktrees and are
5
+ // not safe to overlap, even when each command targets a different destination.
6
+ const repositoryMutationQueues = new Map<string, Promise<void>>()
7
+
8
+ async function mutateWorktrees<T>(
9
+ repository: string,
10
+ operation: () => Promise<T>,
11
+ ): Promise<T> {
12
+ const predecessor = repositoryMutationQueues.get(repository) ?? Promise.resolve()
13
+ const result = predecessor.then(operation, operation)
14
+ const settled = result.then(
15
+ () => undefined,
16
+ () => undefined,
17
+ )
18
+ repositoryMutationQueues.set(repository, settled)
19
+ try {
20
+ return await result
21
+ } finally {
22
+ if (repositoryMutationQueues.get(repository) === settled) {
23
+ repositoryMutationQueues.delete(repository)
24
+ }
25
+ }
26
+ }
27
+
4
28
  export async function createDetachedWorktree(
5
29
  repository: string,
6
30
  commit: string,
7
31
  destination: string,
8
32
  ): Promise<void> {
9
- await run('git', ['-C', repository, 'worktree', 'remove', '--force', '--', destination])
10
- await rm(destination, { recursive: true, force: true })
11
- await runOk('git', ['-C', repository, 'worktree', 'add', '--detach', destination, commit])
33
+ await mutateWorktrees(repository, async () => {
34
+ await run('git', ['-C', repository, 'worktree', 'remove', '--force', '--', destination])
35
+ await rm(destination, { recursive: true, force: true })
36
+ await runOk('git', ['-C', repository, 'worktree', 'add', '--detach', destination, commit])
37
+ })
12
38
  }
13
39
 
14
40
  export async function pruneDetachedWorktrees(repository: string): Promise<void> {
15
- await runOk('git', ['-C', repository, 'worktree', 'prune'])
41
+ await mutateWorktrees(repository, async () => {
42
+ await runOk('git', ['-C', repository, 'worktree', 'prune'])
43
+ })
16
44
  }
17
45
 
18
46
  export async function removeDetachedWorktree(
19
47
  repository: string,
20
48
  destination: string,
21
49
  ): Promise<void> {
22
- const result = await run('git', [
23
- '-C',
24
- repository,
25
- 'worktree',
26
- 'remove',
27
- '--force',
28
- '--',
29
- destination,
30
- ])
31
- if (result.code !== 0) {
32
- await rm(destination, { recursive: true, force: true })
33
- }
50
+ await mutateWorktrees(repository, async () => {
51
+ const result = await run('git', [
52
+ '-C',
53
+ repository,
54
+ 'worktree',
55
+ 'remove',
56
+ '--force',
57
+ '--',
58
+ destination,
59
+ ])
60
+ if (result.code !== 0) {
61
+ await rm(destination, { recursive: true, force: true })
62
+ }
63
+ })
34
64
  }
@@ -33,6 +33,7 @@ import { spawn, spawnSync } from 'node:child_process'
33
33
  import { existsSync, mkdirSync, symlinkSync } from 'node:fs'
34
34
  import { join } from 'node:path'
35
35
  import { improve, agenticGenerator } from '@tangle-network/agent-runtime'
36
+ import type { ProposalFinding } from '@tangle-network/agent-eval'
36
37
  import type { DispatchContext, JudgeConfig, Scenario } from '@tangle-network/agent-eval/contract'
37
38
  import { createSweBenchAdapter } from './benchmarks/swe-bench'
38
39
  import type { BenchTask } from './benchmarks/types'
@@ -238,7 +239,7 @@ async function main(): Promise<void> {
238
239
 
239
240
  // Domain prompt: name the EDIT BOUNDARY (scaffold logic only) + keep the raw-trace evidence discipline
240
241
  // (agenticGenerator discards a raw-trace candidate that doesn't inspect a trace + write the diagnosis).
241
- const buildPrompt = (args: { report: unknown; findings: Array<{ severity?: string; claim?: string; recommended_action?: string }> }): string => {
242
+ const buildPrompt = (args: { findings: ReadonlyArray<ProposalFinding> }): string => {
242
243
  const lines: string[] = [
243
244
  'You are improving a SWE-bench coding SCAFFOLD: a harness that drives a FIXED worker model to fix real GitHub bugs via list_files/read_file/edit_file tools. Your job is to rewrite the SCAFFOLD LOGIC so the SAME worker model resolves MORE instances on a held-out split.',
244
245
  '',