@tangle-network/agent-bench 0.4.5 → 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 +5 -0
- package/package.json +5 -5
- package/src/swe-arena/activation.test.mts +2 -3
- package/src/swe-arena/gepa-seat.test.mts +0 -2
- package/src/swe-arena/outer-loop.mts +17 -9
- package/src/swe-arena/outer-loop.test.mts +22 -4
- package/src/swe-arena/proposer-fanout.mts +12 -14
- package/src/swe-arena/proposer-fanout.test.mts +20 -20
- package/src/swe-arena/scratch-worktree-serialization.test.mts +72 -0
- package/src/swe-arena/scratch-worktree.ts +46 -16
- package/src/swe-code-improve.mts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
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
|
+
|
|
3
8
|
## 0.4.5
|
|
4
9
|
|
|
5
10
|
- Allow the zero-model Pier proof to complete a cold separate-verifier image build before its task and overall execution deadlines.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-bench",
|
|
3
|
-
"version": "0.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.
|
|
28
|
+
"@tangle-network/agent-eval": "0.134.1",
|
|
29
29
|
"@tangle-network/agent-interface": "0.36.0",
|
|
30
|
-
"@tangle-network/agent-knowledge": "6.1.
|
|
31
|
-
"@tangle-network/sandbox": "0.15.
|
|
32
|
-
"@tangle-network/agent-runtime": "0.
|
|
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",
|
|
@@ -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 {
|
|
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
|
-
|
|
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 {
|
|
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: {
|
|
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:
|
|
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 =
|
|
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<
|
|
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:
|
|
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 =
|
|
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
|
-
{
|
|
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
|
-
|
|
595
|
-
|
|
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 {
|
|
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: {
|
|
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(
|
|
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: {
|
|
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: (
|
|
547
|
-
proposerBuildPrompt(
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
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 = (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
173
|
-
expect(() => proposerBuildPrompt({
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
616
|
+
expect(proposerBuildPrompt({ findings: [] }, spec, PARENTS, {})).toBe(legacy)
|
|
617
617
|
})
|
|
618
618
|
})
|
|
@@ -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
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
23
|
-
'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
}
|
package/src/swe-code-improve.mts
CHANGED
|
@@ -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: {
|
|
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
|
'',
|