@tangle-network/agent-bench 0.4.9 → 0.5.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ - Consume Runtime 0.120.0 with Eval 0.140.1, Interface 0.40.0, Knowledge 7.0.4, and Sandbox 0.15.2.
6
+ - Released under a new minor, not a patch, because Knowledge crosses a major (6.1.11 -> 7.0.4) and Runtime crosses eleven minors. A `^0.4.9` range admits only patches, so shipping this alignment as 0.4.10 would hand every such consumer a major dependency move without a range change.
7
+ - Resolve the supervisor tree from the `{kind:'event', root, event}` journal envelope, and take the optional `controlAdapter` on the DSPy RLM trace engine, both by way of Eval 0.140.1. Bench calls neither surface directly, so no adapter code changed.
8
+
3
9
  ## 0.4.9
4
10
 
5
11
  - Consume Runtime 0.109.2 with Eval 0.135.2 and Knowledge 6.1.11 so benchmark runs use the corrected paired promotion decisions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-bench",
3
- "version": "0.4.9",
3
+ "version": "0.5.0",
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.135.2",
29
- "@tangle-network/agent-interface": "0.36.0",
30
- "@tangle-network/agent-knowledge": "6.1.11",
28
+ "@tangle-network/agent-eval": "0.140.1",
29
+ "@tangle-network/agent-interface": "0.40.0",
30
+ "@tangle-network/agent-knowledge": "7.0.4",
31
31
  "@tangle-network/sandbox": "0.15.2",
32
- "@tangle-network/agent-runtime": "0.109.2"
32
+ "@tangle-network/agent-runtime": "0.120.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@arethetypeswrong/cli": "0.18.5",
@@ -88,11 +88,11 @@ export const PINNED_BASELINES: Record<string, GenerateSignals> = {
88
88
  /** The two demo author seats: the plain author and the quant lens. */
89
89
  export function defaultQuantProposers(): ProposerSpec[] {
90
90
  return [
91
- { name: 'default-author', profile: 'default-author.profile.json', harness: 'claude' },
91
+ { name: 'default-author', profile: 'default-author.profile.json', harness: 'claude-code' },
92
92
  {
93
93
  name: 'quant-researcher',
94
94
  profile: join(QUANT_PROFILES_DIR, 'quant-researcher.profile.json'),
95
- harness: 'claude',
95
+ harness: 'claude-code',
96
96
  lens:
97
97
  'Favor ONE economically-motivated effect (trend, mean reversion, vol targeting) with few parameters. ' +
98
98
  'State the regime in which it should work and keep turnover low enough that 15bps a side cannot eat the edge.',
@@ -218,7 +218,7 @@ async function claudeShot(opts: {
218
218
  const res = await run('claude', argv, {
219
219
  stdin: opts.prompt,
220
220
  cwd: opts.cwd,
221
- env: proposerShotEnv('claude'),
221
+ env: proposerShotEnv('claude-code'),
222
222
  timeoutMs: opts.timeoutMs,
223
223
  })
224
224
  if (res.code !== 0) {
@@ -158,7 +158,7 @@ async function buildFixtureTree(): Promise<void> {
158
158
  candidateIndex: 0,
159
159
  shot: 1,
160
160
  maxShots: 3,
161
- harness: 'claude',
161
+ harness: 'claude-code',
162
162
  model: null,
163
163
  promptSha256: 'sha256:abc',
164
164
  startedAt: '2026-07-22T19:27:18.350Z',
@@ -256,7 +256,7 @@ describe('backfillSweArena', () => {
256
256
  expect(proposer?.messages).toHaveLength(2)
257
257
  expect(proposer?.outcome.reward).toBe(1)
258
258
  expect(proposer?.outcome.reward_source).toBe('swe-arena-official-judge/candidate-resolved-fraction')
259
- expect(proposer?.policy).toMatchObject({ harness: 'claude', model: 'claude-fable-5' })
259
+ expect(proposer?.policy).toMatchObject({ harness: 'claude-code', model: 'claude-fable-5' })
260
260
  expect(proposer?.cost.tokens_out).toBe(462)
261
261
  expect(proposer?.task).toMatchObject({ suite: 'swe-arena-proposer', rep: 1 })
262
262
 
@@ -245,9 +245,9 @@ describe('activation-predicate prefilter', () => {
245
245
 
246
246
  it('kills a candidate without .improve/activation.json (stage activation-predicate) and passes one WITH it', async () => {
247
247
  const proposers: ProposerSpec[] = [
248
- { name: 'with-predicate', harness: 'claude' },
249
- { name: 'without-predicate', harness: 'claude' },
250
- { name: 'invalid-predicate', harness: 'claude' },
248
+ { name: 'with-predicate', harness: 'claude-code' },
249
+ { name: 'without-predicate', harness: 'claude-code' },
250
+ { name: 'invalid-predicate', harness: 'claude-code' },
251
251
  ]
252
252
  const gen = fanOutLoopsGenerator(config(proposers), {
253
253
  author: async (proposer, args) => {
@@ -284,7 +284,7 @@ describe('activation-predicate prefilter', () => {
284
284
  })
285
285
 
286
286
  it('does not require a predicate when the gate is off (gen-4 behavior unchanged)', async () => {
287
- const cfg = config([{ name: 'legacy', harness: 'claude' }])
287
+ const cfg = config([{ name: 'legacy', harness: 'claude-code' }])
288
288
  cfg.activationGate = false
289
289
  const gen = fanOutLoopsGenerator(cfg, {
290
290
  author: async (_p, args) => {
@@ -510,19 +510,19 @@ describe('judgeFactoryPatch', () => {
510
510
  it('gold (impl-only PR diff) resolves with full score', async () => {
511
511
  const { result } = await judgeFactoryPatch(goodInst, await goldImplPatch(goodInst))
512
512
  expect(result).toMatchObject({ resolved: true, score: 1, passed: 2, total: 2 })
513
- })
513
+ }, 60_000)
514
514
 
515
515
  it('empty patch (bare base) fails: judge tests cannot even collect', async () => {
516
516
  const { result } = await judgeFactoryPatch(goodInst, '')
517
517
  expect(result).toMatchObject({ resolved: false, score: 0, passed: 0, total: 2 })
518
- })
518
+ }, 60_000)
519
519
 
520
520
  it('partial impl earns partial credit, never resolved', async () => {
521
521
  // add correct, mul wrong → 1/2.
522
522
  const patch = await makePatch(goodInst, 'export const add = (a, b) => a + b\nexport const mul = (a, b) => a + b\n')
523
523
  const { result } = await judgeFactoryPatch(goodInst, patch)
524
524
  expect(result).toMatchObject({ resolved: false, score: 0.5, passed: 1, total: 2 })
525
- })
525
+ }, 60_000)
526
526
 
527
527
  it('an unappliable patch is the candidate\'s failure (resolved false), not infra', async () => {
528
528
  const garbage = 'diff --git a/nope.txt b/nope.txt\n--- a/nope.txt\n+++ b/nope.txt\n@@ -1 +1 @@\n-x\n+y\n'
@@ -58,7 +58,7 @@ describe('validateGepaSeat', () => {
58
58
  expect(() => validateGepaSeat(seat())).not.toThrow()
59
59
  expect(() => validateGepaSeat(seat({ engine: 'omni', maxMetricCalls: 8 }))).not.toThrow()
60
60
  expect(isGepaSeat(seat())).toBe(true)
61
- expect(isGepaSeat({ name: 'x', harness: 'claude' })).toBe(false)
61
+ expect(isGepaSeat({ name: 'x', harness: 'claude-code' })).toBe(false)
62
62
  })
63
63
 
64
64
  it('requires a surface inside the declared change-space', () => {
@@ -68,7 +68,7 @@ describe('validateGepaSeat', () => {
68
68
  })
69
69
 
70
70
  it('rejects harness-seat fields on an engine seat instead of silently ignoring them', () => {
71
- expect(() => validateGepaSeat(seat({ harness: 'claude' }))).toThrow(/'harness' belongs to harness-authored/)
71
+ expect(() => validateGepaSeat(seat({ harness: 'claude-code' }))).toThrow(/'harness' belongs to harness-authored/)
72
72
  expect(() => validateGepaSeat(seat({ merge: true }))).toThrow(/'merge'/)
73
73
  expect(() => validateGepaSeat(seat({ model: 'x' }))).toThrow(/'model'/)
74
74
  expect(() => validateGepaSeat(seat({ profile: 'p.json' }))).toThrow(/'profile'/)
@@ -234,7 +234,7 @@ describe('captureProposerProvenance with a gepa seat', () => {
234
234
  return { code: 0, stdout: 'source', stderr: '' }
235
235
  }
236
236
  it('records engine, surface, gepa version, bridge module, and the python runtime as harnessVersion', async () => {
237
- const record = await captureProposerProvenance([{ name: 'claude-author', harness: 'claude' }, seat()], {
237
+ const record = await captureProposerProvenance([{ name: 'claude-author', harness: 'claude-code' }, seat()], {
238
238
  exec: okExec,
239
239
  readSettingsModel: () => 'settings-model',
240
240
  })
@@ -251,7 +251,7 @@ describe('captureProposerProvenance with a gepa seat', () => {
251
251
  expect(gepa.harness).toBeUndefined()
252
252
  // The claude seat is untouched by the gepa capture path.
253
253
  expect(record.proposers.find((p) => p.name === 'claude-author')).toMatchObject({
254
- harness: 'claude',
254
+ harness: 'claude-code',
255
255
  settingsModel: 'settings-model',
256
256
  })
257
257
  })
@@ -646,7 +646,7 @@ export interface OuterLoopConfig {
646
646
  * budget.maxImprovementShots; the LIB owns the dial (capabilities.mts
647
647
  * fails loud on a substrate that would drop it). */
648
648
  maxShots: number
649
- proposerHarness: 'claude' | 'codex' | 'opencode'
649
+ proposerHarness: 'claude-code' | 'codex' | 'opencode'
650
650
  proposerTimeoutMs: number
651
651
  /** GEN-3 proposer fan-out: N proposers author candidates CONCURRENTLY, each
652
652
  * an AgentProfile-pinned harness invocation (see proposer-fanout.mts).
@@ -759,7 +759,7 @@ export function defaultRound4Config(
759
759
  // bootstrap run writes it; the lib validates it on every consumption.
760
760
  premeasuredBaselinePath: join(hh, 'r4', 'premeasured-baseline.json'),
761
761
  maxShots: 3,
762
- proposerHarness: 'claude',
762
+ proposerHarness: 'claude-code',
763
763
  // Per author SHOT (agenticGenerator timeoutMs). 20 min timed out 3× under
764
764
  // degraded capacity in gen-1 ("author shot timed out") — doubled to 40 min.
765
765
  proposerTimeoutMs: 2_400_000,
@@ -858,18 +858,18 @@ export function defaultGen3Config(
858
858
  const base = defaultRound4Config(hh, opts)
859
859
  const outDirName = opts.outDirName ?? 'gen3'
860
860
  const proposers: ProposerSpec[] = [
861
- { name: 'default-author', profile: 'default-author.profile.json', harness: 'claude' },
861
+ { name: 'default-author', profile: 'default-author.profile.json', harness: 'claude-code' },
862
862
  {
863
863
  name: 'mechanics-author',
864
864
  profile: 'default-author.profile.json',
865
- harness: 'claude',
865
+ harness: 'claude-code',
866
866
  diagnosisSlice: 'mechanics',
867
867
  lens: 'Focus on MECHANICS: worker lifecycle, sandbox/clone contracts, settlement and delivery paths. Prefer code-path fixes over prompt wording.',
868
868
  },
869
869
  {
870
870
  name: 'prompts-author',
871
871
  profile: 'default-author.profile.json',
872
- harness: 'claude',
872
+ harness: 'claude-code',
873
873
  diagnosisSlice: 'prompts',
874
874
  lens: 'Focus on PROMPTS: worker/brain instruction wording, placement guidance, self-check discipline. Prefer prompt/instruction changes over code-path rewrites.',
875
875
  },
@@ -945,10 +945,10 @@ export function defaultGen4Config(
945
945
  ): OuterLoopConfig {
946
946
  const base = defaultGen3Config(hh, { outDirName: opts.outDirName ?? 'gen4' })
947
947
  const proposers: ProposerSpec[] = [
948
- { name: 'claude-author', profile: 'default-author.profile.json', harness: 'claude' },
948
+ { name: 'claude-author', profile: 'default-author.profile.json', harness: 'claude-code' },
949
949
  { name: 'glm-author', harness: 'opencode', model: 'zai-coding-plan/glm-5.2' },
950
950
  ...(opts.includeCodex === false ? [] : [{ name: 'codex-author', harness: 'codex' } satisfies ProposerSpec]),
951
- { name: 'merge-author', profile: 'default-author.profile.json', harness: 'claude', merge: true },
951
+ { name: 'merge-author', profile: 'default-author.profile.json', harness: 'claude-code', merge: true },
952
952
  ]
953
953
  return {
954
954
  ...base,
@@ -1263,7 +1263,7 @@ const CODEX_AMBIENT_AUTH_VARS = ['OPENAI_API_KEY', 'OPENAI_BASE_URL'] as const
1263
1263
 
1264
1264
  export function proposerShotEnv(harness: OuterLoopConfig['proposerHarness']): NodeJS.ProcessEnv {
1265
1265
  const env: NodeJS.ProcessEnv = { ...process.env }
1266
- if (harness === 'claude') {
1266
+ if (harness === 'claude-code') {
1267
1267
  for (const name of CLAUDE_AMBIENT_AUTH_VARS) delete env[name]
1268
1268
  }
1269
1269
  if (harness === 'codex') {
@@ -36,7 +36,7 @@ describe('run process-group timeout', () => {
36
36
  '-e',
37
37
  `const fs=require('node:fs'); const marker=process.argv[1]; process.on('SIGTERM',()=>{fs.writeFileSync(marker,'settled'); setTimeout(()=>process.exit(0),25)}); setInterval(()=>{},1000)`,
38
38
  marker,
39
- ], { timeoutMs: 60, killGraceMs: 500 })
39
+ ], { timeoutMs: 1_000, killGraceMs: 500 })
40
40
 
41
41
  expect(result.code).toBe(TIMEOUT_RC)
42
42
  expect(result.timedOut).toBe(true)
@@ -91,7 +91,7 @@ export interface ProposerSpec {
91
91
  profile?: string
92
92
  /** Required for harness-authored seats. Absent on an engine seat
93
93
  * (`engine` set) — enforced both ways at generator construction. */
94
- harness?: 'claude' | 'codex' | 'opencode'
94
+ harness?: 'claude-code' | 'codex' | 'opencode'
95
95
  /** GEN-4 pinned model id, threaded to the harness CLI as `-m <model>` via
96
96
  * the author profile's `model.default` (harnessInvocation maps it for all
97
97
  * three harnesses). Unset = the CLI's own resolved model (its login/settings
@@ -201,7 +201,7 @@ export function loadAuthorProfile(spec: ProposerSpec): AgentProfile | undefined
201
201
 
202
202
  /** The gen-2 author, codified: one bare-profile claude proposer. */
203
203
  export function defaultProposers(): ProposerSpec[] {
204
- return [{ name: 'default-author', profile: 'default-author.profile.json', harness: 'claude' }]
204
+ return [{ name: 'default-author', profile: 'default-author.profile.json', harness: 'claude-code' }]
205
205
  }
206
206
 
207
207
  /** The profile the author shot actually runs: the loaded profile (if any) with
@@ -73,7 +73,7 @@ describe('sliceFindings', () => {
73
73
 
74
74
  describe('proposerBuildPrompt', () => {
75
75
  it('appends the lens AFTER the shared protocol prompt, leaving the change-space text intact', () => {
76
- const spec: ProposerSpec = { name: 'x', harness: 'claude', lens: 'Prefer code-path fixes.' }
76
+ const spec: ProposerSpec = { name: 'x', harness: 'claude-code', lens: 'Prefer code-path fixes.' }
77
77
  const prompt = proposerBuildPrompt({ findings: [] }, spec)
78
78
  expect(prompt).toContain('DECLARED CHANGE-SPACE')
79
79
  expect(prompt.indexOf('DECLARED CHANGE-SPACE')).toBeLessThan(prompt.indexOf('YOUR AUTHORING LENS (x)'))
@@ -81,7 +81,7 @@ describe('proposerBuildPrompt', () => {
81
81
  })
82
82
 
83
83
  it('is the bare round prompt without a lens', () => {
84
- const spec: ProposerSpec = { name: 'x', harness: 'claude' }
84
+ const spec: ProposerSpec = { name: 'x', harness: 'claude-code' }
85
85
  expect(proposerBuildPrompt({ findings: [] }, spec)).not.toContain('AUTHORING LENS')
86
86
  })
87
87
  })
@@ -111,7 +111,7 @@ describe('resolveAuthorProfile (pinned models)', () => {
111
111
  const spec: ProposerSpec = {
112
112
  name: 'x',
113
113
  profile: 'default-author.profile.json',
114
- harness: 'claude',
114
+ harness: 'claude-code',
115
115
  model: 'claude-fable-5',
116
116
  }
117
117
  const profile = resolveAuthorProfile(spec)
@@ -126,15 +126,15 @@ describe('resolveAuthorProfile (pinned models)', () => {
126
126
  })
127
127
 
128
128
  it('is byte-identical to loadAuthorProfile without a pin (gen-3 seats unchanged)', () => {
129
- const spec: ProposerSpec = { name: 'x', profile: 'default-author.profile.json', harness: 'claude' }
129
+ const spec: ProposerSpec = { name: 'x', profile: 'default-author.profile.json', harness: 'claude-code' }
130
130
  expect(resolveAuthorProfile(spec)).toEqual(loadAuthorProfile(spec))
131
- expect(resolveAuthorProfile({ name: 'bare', harness: 'claude' })).toBeUndefined()
131
+ expect(resolveAuthorProfile({ name: 'bare', harness: 'claude-code' })).toBeUndefined()
132
132
  })
133
133
  })
134
134
 
135
135
  describe('proposerBuildPrompt with pareto parents', () => {
136
136
  it('appends the parents section (evidence + diffs) after the protocol prompt and lens', () => {
137
- const spec: ProposerSpec = { name: 'x', harness: 'claude', lens: 'Prefer code-path fixes.' }
137
+ const spec: ProposerSpec = { name: 'x', harness: 'claude-code', lens: 'Prefer code-path fixes.' }
138
138
  const prompt = proposerBuildPrompt({ findings: [] }, spec, PARENTS)
139
139
  expect(prompt).toContain('DECLARED CHANGE-SPACE')
140
140
  expect(prompt).toContain('PARETO PARENTS')
@@ -146,14 +146,14 @@ describe('proposerBuildPrompt with pareto parents', () => {
146
146
  })
147
147
 
148
148
  it('leaves the prompt untouched when no parents are seeded (gen-3 behavior)', () => {
149
- const spec: ProposerSpec = { name: 'x', harness: 'claude' }
149
+ const spec: ProposerSpec = { name: 'x', harness: 'claude-code' }
150
150
  expect(proposerBuildPrompt({ findings: [] }, spec)).not.toContain('PARETO PARENTS')
151
151
  expect(parentsPromptSection(PARENTS)).toContain('measured evidence')
152
152
  })
153
153
  })
154
154
 
155
155
  describe('mergeAuthorPrompt', () => {
156
- const spec: ProposerSpec = { name: 'merge-author', harness: 'claude', merge: true }
156
+ const spec: ProposerSpec = { name: 'merge-author', harness: 'claude-code', merge: true }
157
157
 
158
158
  it('keeps the change-space contract and presents BOTH parent diffs with the coherent-union task', () => {
159
159
  const prompt = proposerBuildPrompt({ findings: [] }, spec, PARENTS)
@@ -182,11 +182,11 @@ describe('defaultGen4Config', () => {
182
182
  expect(config.proposers).toHaveLength(4)
183
183
  expect(config.populationSize).toBe(4)
184
184
  const byName = Object.fromEntries(config.proposers!.map((p) => [p.name, p]))
185
- expect(byName['claude-author']).toMatchObject({ harness: 'claude', profile: 'default-author.profile.json' })
185
+ expect(byName['claude-author']).toMatchObject({ harness: 'claude-code', profile: 'default-author.profile.json' })
186
186
  expect(byName['claude-author']!.model).toBeUndefined()
187
187
  expect(byName['glm-author']).toMatchObject({ harness: 'opencode', model: 'zai-coding-plan/glm-5.2' })
188
188
  expect(byName['codex-author']).toMatchObject({ harness: 'codex' })
189
- expect(byName['merge-author']).toMatchObject({ harness: 'claude', merge: true })
189
+ expect(byName['merge-author']).toMatchObject({ harness: 'claude-code', merge: true })
190
190
  })
191
191
 
192
192
  it('drops the codex seat (and shrinks the population) when includeCodex is false', () => {
@@ -216,7 +216,7 @@ describe('defaultGen4Config', () => {
216
216
 
217
217
  describe('loadAuthorProfile', () => {
218
218
  it('loads the committed default-author profile (bare: no prompt, no model)', () => {
219
- const profile = loadAuthorProfile({ name: 'a', profile: 'default-author.profile.json', harness: 'claude' })
219
+ const profile = loadAuthorProfile({ name: 'a', profile: 'default-author.profile.json', harness: 'claude-code' })
220
220
  expect(profile?.name).toBe('swe-arena-default-author')
221
221
  expect(profile?.prompt).toBeUndefined()
222
222
  expect(profile?.model).toBeUndefined()
@@ -224,7 +224,7 @@ describe('loadAuthorProfile', () => {
224
224
  })
225
225
 
226
226
  it('returns undefined without a profile path', () => {
227
- expect(loadAuthorProfile({ name: 'a', harness: 'claude' })).toBeUndefined()
227
+ expect(loadAuthorProfile({ name: 'a', harness: 'claude-code' })).toBeUndefined()
228
228
  })
229
229
 
230
230
  it('fails loud when a non-codex proposer declares profile resources (they would be dropped)', async () => {
@@ -232,7 +232,7 @@ describe('loadAuthorProfile', () => {
232
232
  try {
233
233
  const path = join(dir, 'with-resources.json')
234
234
  await writeFile(path, JSON.stringify({ name: 'r', resources: { files: [] } }))
235
- expect(() => loadAuthorProfile({ name: 'a', profile: path, harness: 'claude' })).toThrow(/silently drop/)
235
+ expect(() => loadAuthorProfile({ name: 'a', profile: path, harness: 'claude-code' })).toThrow(/silently drop/)
236
236
  } finally {
237
237
  await rm(dir, { recursive: true, force: true })
238
238
  }
@@ -302,7 +302,7 @@ describe('defaultGen3Config', () => {
302
302
 
303
303
  it('defaultProposers codifies the gen-2 author: one bare-profile claude entry', () => {
304
304
  expect(defaultProposers()).toEqual([
305
- { name: 'default-author', profile: 'default-author.profile.json', harness: 'claude' },
305
+ { name: 'default-author', profile: 'default-author.profile.json', harness: 'claude-code' },
306
306
  ])
307
307
  })
308
308
  })
@@ -357,19 +357,25 @@ describe('fanOutLoopsGenerator', () => {
357
357
 
358
358
  it('authors ALL proposers concurrently in separate worktrees and applies each patch to its candidate slot', async () => {
359
359
  const proposers: ProposerSpec[] = [
360
- { name: 'alpha', harness: 'claude' },
361
- { name: 'beta', harness: 'claude' },
360
+ { name: 'alpha', harness: 'claude-code' },
361
+ { name: 'beta', harness: 'claude-code' },
362
362
  ]
363
363
  let inFlight = 0
364
364
  let maxInFlight = 0
365
365
  const seenWorktrees: string[] = []
366
+ let releaseAuthors!: () => void
367
+ const bothAuthorsStarted = new Promise<void>((resolve) => {
368
+ releaseAuthors = resolve
369
+ })
366
370
  const gen = fanOutLoopsGenerator(baseConfig(proposers), {
367
371
  author: async (proposer, args) => {
368
372
  inFlight++
369
373
  maxInFlight = Math.max(maxInFlight, inFlight)
370
374
  seenWorktrees.push(args.worktreePath)
371
- // Hold the slot across a tick so real parallelism is observable.
372
- await new Promise((r) => setTimeout(r, 30))
375
+ if (inFlight === proposers.length) releaseAuthors()
376
+ // Neither author may finish before both have started. A serial implementation
377
+ // therefore fails this test instead of passing or failing with machine speed.
378
+ await bothAuthorsStarted
373
379
  // Inside the declared change-space (extensions/pi/**) so the always-on
374
380
  // change-space pre-filter does not kill the candidate.
375
381
  await mkdir(join(args.worktreePath, 'extensions', 'pi'), { recursive: true })
@@ -390,7 +396,7 @@ describe('fanOutLoopsGenerator', () => {
390
396
 
391
397
  expect(first).toMatchObject({ applied: true, label: 'alpha' })
392
398
  expect(second).toMatchObject({ applied: true, label: 'beta' })
393
- expect(first.rationale).toContain('proposer alpha (claude)')
399
+ expect(first.rationale).toContain('proposer alpha (claude-code)')
394
400
  // Both patches landed on the SAME driver worktree across the two calls —
395
401
  // the driver discards/finalizes between calls in production; here we only
396
402
  // assert each call applied its own proposer's file.
@@ -401,8 +407,8 @@ describe('fanOutLoopsGenerator', () => {
401
407
 
402
408
  it('kills a candidate at the smoke pre-filter: applied=false, no patch applied, kill recorded with reason', async () => {
403
409
  const proposers: ProposerSpec[] = [
404
- { name: 'good', harness: 'claude' },
405
- { name: 'bad', harness: 'claude' },
410
+ { name: 'good', harness: 'claude-code' },
411
+ { name: 'bad', harness: 'claude-code' },
406
412
  ]
407
413
  const config = baseConfig(proposers)
408
414
  config.prefilter = { enabled: true, smokeInstance: 'cheapest-of-set' }
@@ -455,7 +461,7 @@ describe('fanOutLoopsGenerator', () => {
455
461
  })
456
462
 
457
463
  it('kills an out-of-space diff at the change-space pre-filter before any smoke spend', async () => {
458
- const proposers: ProposerSpec[] = [{ name: 'rogue', harness: 'claude' }]
464
+ const proposers: ProposerSpec[] = [{ name: 'rogue', harness: 'claude-code' }]
459
465
  const config = baseConfig(proposers)
460
466
  config.prefilter = { enabled: true, smokeInstance: 'cheapest-of-set' }
461
467
  let smokeRan = false
@@ -482,7 +488,7 @@ describe('fanOutLoopsGenerator', () => {
482
488
  })
483
489
 
484
490
  it('returns applied:false without a kill when a proposer authors nothing', async () => {
485
- const gen = fanOutLoopsGenerator(baseConfig([{ name: 'idle', harness: 'claude' }]), {
491
+ const gen = fanOutLoopsGenerator(baseConfig([{ name: 'idle', harness: 'claude-code' }]), {
486
492
  author: async () => ({ applied: false, summary: '' }),
487
493
  })
488
494
  expect((await gen.generate(generatorArgs(0))).applied).toBe(false)
@@ -490,7 +496,7 @@ describe('fanOutLoopsGenerator', () => {
490
496
  })
491
497
 
492
498
  it('fails loud when candidateIndex exceeds the proposer list (populationSize drift)', async () => {
493
- const gen = fanOutLoopsGenerator(baseConfig([{ name: 'only', harness: 'claude' }]), {
499
+ const gen = fanOutLoopsGenerator(baseConfig([{ name: 'only', harness: 'claude-code' }]), {
494
500
  author: async () => ({ applied: false, summary: '' }),
495
501
  })
496
502
  await expect(gen.generate(generatorArgs(1))).rejects.toThrow(/populationSize must equal/)
@@ -514,7 +520,7 @@ describe('fanOutLoopsGenerator', () => {
514
520
  })
515
521
 
516
522
  it('refuses a merge seat without >=2 materialized parents', () => {
517
- const config = baseConfig([{ name: 'merge-author', harness: 'claude', merge: true }])
523
+ const config = baseConfig([{ name: 'merge-author', harness: 'claude-code', merge: true }])
518
524
  expect(() => fanOutLoopsGenerator(config, { author: async () => ({ applied: false, summary: '' }) })).toThrow(
519
525
  /merge proposer/,
520
526
  )
@@ -533,8 +539,8 @@ describe('fanOutLoopsGenerator', () => {
533
539
  it('rejects duplicate proposer names and empty proposer lists', () => {
534
540
  expect(() =>
535
541
  fanOutLoopsGenerator(baseConfig([
536
- { name: 'dup', harness: 'claude' },
537
- { name: 'dup', harness: 'claude' },
542
+ { name: 'dup', harness: 'claude-code' },
543
+ { name: 'dup', harness: 'claude-code' },
538
544
  ])),
539
545
  ).toThrow(/duplicate/)
540
546
  expect(() => fanOutLoopsGenerator({ ...defaultRound4Config(), loopsRepo, outDir })).toThrow(/empty/)
@@ -585,7 +591,7 @@ describe('gen-5 prompt sections', () => {
585
591
  }
586
592
 
587
593
  it('appends EVIDENCE MAP + briefing + activation contract after the protocol prompt', () => {
588
- const spec: ProposerSpec = { name: 'x', harness: 'claude' }
594
+ const spec: ProposerSpec = { name: 'x', harness: 'claude-code' }
589
595
  const prompt = proposerBuildPrompt({ findings: [] }, spec, [], {
590
596
  briefing,
591
597
  activationGate: true,
@@ -598,7 +604,7 @@ describe('gen-5 prompt sections', () => {
598
604
  })
599
605
 
600
606
  it('the merge seat gets the gen-5 sections too', () => {
601
- const spec: ProposerSpec = { name: 'merge-author', harness: 'claude', merge: true }
607
+ const spec: ProposerSpec = { name: 'merge-author', harness: 'claude-code', merge: true }
602
608
  const prompt = proposerBuildPrompt({ findings: [] }, spec, PARENTS, {
603
609
  briefing,
604
610
  activationGate: true,
@@ -609,7 +615,7 @@ describe('gen-5 prompt sections', () => {
609
615
  })
610
616
 
611
617
  it('leaves gen-3/gen-4 prompts byte-identical when no extras are passed', () => {
612
- const spec: ProposerSpec = { name: 'x', harness: 'claude' }
618
+ const spec: ProposerSpec = { name: 'x', harness: 'claude-code' }
613
619
  const legacy = proposerBuildPrompt({ findings: [] }, spec, PARENTS)
614
620
  expect(legacy).not.toContain('EVIDENCE MAP')
615
621
  expect(legacy).not.toContain('ACTIVATION PREDICATE')
@@ -24,6 +24,7 @@
24
24
  import { readFileSync } from 'node:fs'
25
25
  import { homedir } from 'node:os'
26
26
  import { join } from 'node:path'
27
+ import { localHarnessExecutable } from '@tangle-network/agent-runtime/mcp'
27
28
  import {
28
29
  DEFAULT_GEPA_PYTHON,
29
30
  isGepaSeat,
@@ -117,10 +118,13 @@ export async function captureProposerProvenance(
117
118
  (h): h is NonNullable<ProposerSpec['harness']> => h !== undefined,
118
119
  )
119
120
  for (const harness of harnesses) {
120
- const res = await exec(harness, ['--version'])
121
+ // The harness id is not the binary name (`claude-code` runs `claude`); read the executable
122
+ // from the runtime's harness table rather than spawning the id.
123
+ const executable = localHarnessExecutable(harness)
124
+ const res = await exec(executable, ['--version'])
121
125
  if (res.code !== 0) {
122
126
  throw new Error(
123
- `proposer provenance: '${harness} --version' failed (rc=${res.code}) — the ${harness} seat cannot author. ` +
127
+ `proposer provenance: '${executable} --version' failed (rc=${res.code}) — the ${harness} seat cannot author. ` +
124
128
  `stderr: ${res.stderr.slice(0, 300)}`,
125
129
  )
126
130
  }
@@ -163,7 +167,7 @@ export async function captureProposerProvenance(
163
167
  harness: spec.harness,
164
168
  pinnedModel: spec.model ?? null,
165
169
  harnessVersion: versionByHarness.get(spec.harness!)!,
166
- settingsModel: spec.harness === 'claude' && !spec.model ? readSettingsModel() : null,
170
+ settingsModel: spec.harness === 'claude-code' && !spec.model ? readSettingsModel() : null,
167
171
  authStatus: (spec.harness !== undefined ? authByHarness.get(spec.harness) : undefined) ?? null,
168
172
  merge: spec.merge === true,
169
173
  }
@@ -9,10 +9,10 @@ import type { ProposerSpec } from './proposer-fanout.mts'
9
9
  const ok = (stdout: string) => ({ code: 0, stdout, stderr: '' })
10
10
 
11
11
  const gen4ish: ProposerSpec[] = [
12
- { name: 'claude-author', profile: 'default-author.profile.json', harness: 'claude' },
12
+ { name: 'claude-author', profile: 'default-author.profile.json', harness: 'claude-code' },
13
13
  { name: 'glm-author', harness: 'opencode', model: 'zai-coding-plan/glm-5.2' },
14
14
  { name: 'codex-author', harness: 'codex' },
15
- { name: 'merge-author', profile: 'default-author.profile.json', harness: 'claude', merge: true },
15
+ { name: 'merge-author', profile: 'default-author.profile.json', harness: 'claude-code', merge: true },
16
16
  ]
17
17
 
18
18
  describe('captureProposerProvenance', () => {
@@ -31,7 +31,7 @@ describe('captureProposerProvenance', () => {
31
31
  const byName = Object.fromEntries(record.proposers.map((p) => [p.name, p]))
32
32
  // Claude seat: no pin — the CLI's resolved settings model is the record.
33
33
  expect(byName['claude-author']).toMatchObject({
34
- harness: 'claude',
34
+ harness: 'claude-code',
35
35
  pinnedModel: null,
36
36
  settingsModel: 'claude-fable-5',
37
37
  harnessVersion: 'claude-version 9.9.9',
@@ -10,7 +10,7 @@
10
10
  * Wiring (all verified in this worktree):
11
11
  * - improve()/codeProposerFor + rawTraceContext come from the LOCAL agent-runtime build, linked into
12
12
  * this bench's node_modules (bench/node_modules/@tangle-network/agent-runtime -> /home/drew/code/agent-runtime).
13
- * - The candidate proposer is agenticGenerator(harness:'claude'), BUT the shipped runLocalHarness
13
+ * - The candidate proposer is agenticGenerator(harness:'claude-code'), BUT the shipped runLocalHarness
14
14
  * spawns `claude --headless -p` and --headless is an unknown option on the current CLI (exit 1, no
15
15
  * edits ever). We pass code.generator with a corrected runHarness that spawns
16
16
  * `claude -p <prompt> --dangerously-skip-permissions` so the coding agent can actually edit the
@@ -286,7 +286,7 @@ async function main(): Promise<void> {
286
286
  }
287
287
 
288
288
  const generator = agenticGenerator({
289
- harness: 'claude',
289
+ harness: 'claude-code',
290
290
  verify,
291
291
  timeoutMs: harnessTimeoutMs,
292
292
  // eslint-disable-next-line @typescript-eslint/no-explicit-any