@tangle-network/agent-bench 0.7.0 → 0.8.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.
Files changed (131) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/HARNESS.md +1 -1
  3. package/dist/adapters.js +2 -2
  4. package/dist/benchmarks/appworld.d.ts +61 -2
  5. package/dist/benchmarks/appworld.js +111 -40
  6. package/dist/benchmarks/appworld.js.map +1 -1
  7. package/dist/benchmarks/cadbench.js +1 -1
  8. package/dist/benchmarks/cadgenbench.js +1 -1
  9. package/dist/benchmarks/finresearchbench.js +16 -22
  10. package/dist/benchmarks/finresearchbench.js.map +1 -1
  11. package/dist/benchmarks/finsearchcomp.js +16 -22
  12. package/dist/benchmarks/finsearchcomp.js.map +1 -1
  13. package/dist/benchmarks/frames.js +18 -20
  14. package/dist/benchmarks/frames.js.map +1 -1
  15. package/dist/benchmarks/simpleqa.js +15 -19
  16. package/dist/benchmarks/simpleqa.js.map +1 -1
  17. package/dist/benchmarks/trata-hedge.js +20 -23
  18. package/dist/benchmarks/trata-hedge.js.map +1 -1
  19. package/dist/{cadbench-BrpwOU6A.js → cadbench-BLSyxR1N.js} +21 -20
  20. package/dist/cadbench-BLSyxR1N.js.map +1 -0
  21. package/dist/{cadgenbench-DF7hYHdl.js → cadgenbench-x2OFkf8y.js} +2 -2
  22. package/dist/cadgenbench-x2OFkf8y.js.map +1 -0
  23. package/dist/index.d.ts +0 -1
  24. package/dist/index.js +10 -9
  25. package/dist/index.js.map +1 -1
  26. package/dist/router-turn-C2wMiDoo.js +31 -0
  27. package/dist/router-turn-C2wMiDoo.js.map +1 -0
  28. package/package.json +7 -7
  29. package/scripts/appworld_driver.py +1 -170
  30. package/scripts/run-package-tests.mjs +56 -36
  31. package/scripts/run-package-tests.test.mjs +33 -0
  32. package/scripts/trata-hedge/README.md +3 -3
  33. package/scripts/trata-hedge/run.sh +6 -2
  34. package/src/aec-gate.mts +30 -9
  35. package/src/agent-graphs-gen2.mts +523 -0
  36. package/src/agent-graphs-gen3.mts +660 -0
  37. package/src/agent-graphs-improve/offline-seams.mts +128 -0
  38. package/src/agent-graphs-improve.mts +747 -0
  39. package/src/atom-humaneval.mts +76 -55
  40. package/src/atom-mcp-e2e.mts +27 -13
  41. package/src/benchmarks/appworld.test.mts +119 -1
  42. package/src/benchmarks/appworld.ts +156 -33
  43. package/src/benchmarks/cadbench.ts +21 -8
  44. package/src/benchmarks/finresearchbench.ts +20 -16
  45. package/src/benchmarks/finsearchcomp.ts +20 -20
  46. package/src/benchmarks/frames.ts +19 -18
  47. package/src/benchmarks/simpleqa.ts +19 -17
  48. package/src/benchmarks/trata-hedge.ts +26 -18
  49. package/src/browser/ui-reviewer.ts +1 -1
  50. package/src/clbench-context-gate.mts +45 -9
  51. package/src/cloud-loop.mts +24 -6
  52. package/src/commit0-env-run.mts +6 -3
  53. package/src/commit0-env.ts +2 -3
  54. package/src/commit0-gate.mts +53 -24
  55. package/src/corpus.ts +3 -3
  56. package/src/david-attribution.mts +28 -9
  57. package/src/david-goliath.mts +31 -12
  58. package/src/egress-probe.mts +3 -3
  59. package/src/examples/lean-proof-gate.mts +10 -3
  60. package/src/examples/math-demo.mts +10 -3
  61. package/src/examples/strategy-demo.mts +10 -4
  62. package/src/fleet.mts +30 -8
  63. package/src/gate-cli.mts +9 -4
  64. package/src/gate.test.mts +7 -2
  65. package/src/gate.ts +23 -75
  66. package/src/generate-eval/certify.ts +13 -5
  67. package/src/hev-eval.mts +39 -10
  68. package/src/hev-improve.mts +28 -16
  69. package/src/hev-structural.mts +28 -20
  70. package/src/humaneval-gate.mts +24 -8
  71. package/src/humaneval-object-ablation.mts +52 -14
  72. package/src/humaneval-repair-gate.mts +53 -14
  73. package/src/mbpp-structural.mts +28 -20
  74. package/src/mcp-mount-probe.mts +23 -11
  75. package/src/official-optimizer-config.mts +61 -12
  76. package/src/official-optimizer-config.test.mts +72 -3
  77. package/src/quant-arena/profiles/quant-researcher.profile.json +6 -1
  78. package/src/quant-arena/quant-loop.mts +88 -61
  79. package/src/research-shot.ts +21 -11
  80. package/src/resolve-client.ts +6 -8
  81. package/src/rollout-ledger/backfill-swe-arena.test.mts +7 -2
  82. package/src/router-executor.ts +1 -1
  83. package/src/router-turn.ts +135 -0
  84. package/src/run-benchmarks.ts +7 -2
  85. package/src/sandbox-run.ts +23 -41
  86. package/src/search-bench/bridge.ts +66 -8
  87. package/src/search-bench/parametric-check.mts +19 -4
  88. package/src/search-bench/profiles.ts +24 -9
  89. package/src/search-bench/run.mts +3 -4
  90. package/src/smoke-structural-rollout.mts +21 -5
  91. package/src/supervisor-arena.mts +28 -20
  92. package/src/swe-arena/arms.ts +73 -15
  93. package/src/swe-arena/capacity.ts +44 -41
  94. package/src/swe-arena/diagnosis-ensemble.ts +51 -48
  95. package/src/swe-arena/execution.test.mts +12 -12
  96. package/src/swe-arena/gepa-seat.mts +12 -5
  97. package/src/swe-arena/gepa-seat.test.mts +45 -33
  98. package/src/swe-arena/outer-loop.mts +94 -70
  99. package/src/swe-arena/premeasured-from-cells.mts +16 -1
  100. package/src/swe-arena/premeasured-from-cells.test.mts +21 -1
  101. package/src/swe-arena/profiles/deepseek-author.profile.json +12 -0
  102. package/src/swe-arena/profiles/default-author.profile.json +9 -1
  103. package/src/swe-arena/proposer-fanout.mts +52 -99
  104. package/src/swe-arena/proposer-fanout.test.mts +92 -57
  105. package/src/swe-arena/proposer-provenance.mts +1 -1
  106. package/src/swe-code-improve.mts +30 -52
  107. package/src/swe-emit-patch.mts +26 -5
  108. package/src/swe-improve.mts +27 -8
  109. package/src/swe-jail.test.ts +128 -0
  110. package/src/swe-jail.ts +106 -22
  111. package/src/swe-local-proof.mts +27 -5
  112. package/src/swe-repro-calibrate.mts +5 -0
  113. package/src/swe-self-improve.mts +45 -10
  114. package/src/swe-stream.mts +68 -6
  115. package/src/swe-structural.mts +46 -7
  116. package/src/tb-container-executor.mts +38 -6
  117. package/src/tb-container-executor.test.mts +11 -3
  118. package/src/tb-supervisor-sidecar.mts +11 -27
  119. package/src/terminal-compare.ts +38 -18
  120. package/src/trata-gate.mts +26 -26
  121. package/src/trata-gepa.mts +25 -16
  122. package/src/trata-hedge-solve.mts +80 -0
  123. package/src/worker-blender.ts +21 -11
  124. package/src/worker-browser.ts +18 -5
  125. package/src/worker-build123d.ts +17 -8
  126. package/src/worker-cad.ts +31 -12
  127. package/src/worker.ts +1 -1
  128. package/tb_agents/opencode_router_agent.py +84 -42
  129. package/dist/cadbench-BrpwOU6A.js.map +0 -1
  130. package/dist/cadgenbench-DF7hYHdl.js.map +0 -1
  131. package/scripts/trata-hedge/solve.py +0 -83
@@ -21,14 +21,9 @@
21
21
  * stage 3 — survivors proceed through the unchanged pinned-baseline
22
22
  * reps-fail-closed gate.
23
23
  *
24
- * Proposer identity: each proposer is an `AgentProfile` invocation
25
- * (`agenticGenerator`'s `profile` option `harnessInvocation`, which threads
26
- * `prompt.systemPrompt`/`instructions` into the composed prompt and
27
- * `model.default` into the harness's `-m` flag — verified supported for the
28
- * claude CLI; a bare profile is byte-identical to the legacy prompt-only
29
- * invocation). Profile `resources` are only materialized on the reproducible
30
- * Codex path, so a non-codex proposer with resources fails loud here instead
31
- * of silently dropping them.
24
+ * Proposer identity: each harness-authored proposer supplies one complete
25
+ * `AgentProfile`. Runtime validates and materializes that exact profile, then
26
+ * runs it through the CLI bridge in the candidate worktree.
32
27
  *
33
28
  * NOTE on the import cycle with outer-loop.mts: outer-loop imports this
34
29
  * module's generator factory; this module imports outer-loop's change-space +
@@ -47,13 +42,11 @@ import {
47
42
  type AgenticGeneratorShotExecution,
48
43
  type AgenticGeneratorShotReceipt,
49
44
  } from '@tangle-network/agent-runtime'
50
- import { runLocalHarness } from '@tangle-network/agent-runtime/mcp'
51
- import type { AgentProfile } from '@tangle-network/agent-interface'
45
+ import { agentProfileSchema, type AgentProfile } from '@tangle-network/agent-interface'
52
46
  import type { CostLedgerHandle, ProposalFinding } from '@tangle-network/agent-eval'
53
47
  import {
54
48
  changeSpaceViolations,
55
49
  loopsCandidateVerifier,
56
- proposerShotEnv,
57
50
  purgeIgnoredArtifacts,
58
51
  round4BuildPrompt,
59
52
  type OuterLoopConfig,
@@ -86,17 +79,12 @@ import {
86
79
  export interface ProposerSpec {
87
80
  /** Unique short name — becomes the candidate's staircase `label`. */
88
81
  name: string
89
- /** Path to an `AgentProfile` JSON. Absolute, or relative to this module's
90
- * `profiles/` directory. Omitted = bare profile (legacy invocation). */
82
+ /** Path to the complete `AgentProfile` JSON. Absolute, or relative to this
83
+ * module's `profiles/` directory. Required by the Runtime author path. */
91
84
  profile?: string
92
- /** Required for harness-authored seats. Absent on an engine seat
93
- * (`engine` set) enforced both ways at generator construction. */
94
- harness?: 'claude-code' | 'codex' | 'opencode'
95
- /** GEN-4 pinned model id, threaded to the harness CLI as `-m <model>` via
96
- * the author profile's `model.default` (harnessInvocation maps it for all
97
- * three harnesses). Unset = the CLI's own resolved model (its login/settings
98
- * default) — recorded per run by the proposer-provenance capture, so the
99
- * seat identity is pinned in provenance even when the flag is absent. */
85
+ /** Optional provenance mirror of `profile.harness`; a mismatch is rejected. */
86
+ harness?: 'claude-code' | 'codex' | 'opencode' | 'pi'
87
+ /** Optional provenance mirror of `profile.model.default`; a mismatch is rejected. */
100
88
  model?: string
101
89
  /** GEN-4 merge seat: this proposer's task is to MERGE the configured
102
90
  * Pareto parents' diffs into one coherent surface (see
@@ -179,42 +167,40 @@ export interface PrefilterKill {
179
167
 
180
168
  export const PROFILES_DIR = fileURLToPath(new URL('./profiles', import.meta.url))
181
169
 
182
- /** Load + validate a proposer's `AgentProfile`. Fail-closed on resources for
183
- * non-codex harnesses: `agenticGenerator` only materializes profile resource
184
- * files on the reproducible Codex path, so accepting them here would drop
185
- * them silently. */
170
+ /** Load and schema-validate a proposer's exact `AgentProfile`. */
186
171
  export function loadAuthorProfile(spec: ProposerSpec): AgentProfile | undefined {
187
172
  if (!spec.profile) return undefined
188
173
  const path = spec.profile.startsWith('/') ? spec.profile : join(PROFILES_DIR, spec.profile)
189
- const profile = JSON.parse(readFileSync(path, 'utf8')) as AgentProfile
190
- if (typeof profile.name !== 'string' || profile.name.length === 0) {
191
- throw new Error(`proposer ${spec.name}: profile ${path} has no name`)
192
- }
193
- if (profile.resources && spec.harness !== 'codex') {
194
- throw new Error(
195
- `proposer ${spec.name}: profile ${path} declares resources, which only materialize on the ` +
196
- `reproducible codex path — the ${spec.harness} harness would silently drop them`,
197
- )
198
- }
199
- return profile
174
+ return agentProfileSchema.parse(JSON.parse(readFileSync(path, 'utf8')))
200
175
  }
201
176
 
202
- /** The gen-2 author, codified: one bare-profile claude proposer. */
177
+ /** Default author: Pi with the exact GLM profile committed beside this module. */
203
178
  export function defaultProposers(): ProposerSpec[] {
204
- return [{ name: 'default-author', profile: 'default-author.profile.json', harness: 'claude-code' }]
179
+ return [
180
+ {
181
+ name: 'default-author',
182
+ profile: 'default-author.profile.json',
183
+ harness: 'pi',
184
+ model: 'glm-5.2',
185
+ },
186
+ ]
205
187
  }
206
188
 
207
- /** The profile the author shot actually runs: the loaded profile (if any) with
208
- * the spec's PINNED MODEL merged into `model.default` — harnessInvocation
209
- * turns that into the CLI's `-m <model>` flag (claude/codex/opencode all map
210
- * it). Without a pinned model this is byte-identical to `loadAuthorProfile`,
211
- * so gen-3 seats keep their exact invocation. A pinned model with no profile
212
- * path synthesizes a minimal named profile carrying only the pin. */
189
+ /** Resolve the one exact profile; duplicated provenance fields may only agree. */
213
190
  export function resolveAuthorProfile(spec: ProposerSpec): AgentProfile | undefined {
214
191
  const profile = loadAuthorProfile(spec)
215
- if (!spec.model) return profile
216
- const base: AgentProfile = profile ?? { name: `${spec.name}-pinned` }
217
- return { ...base, model: { ...base.model, default: spec.model } }
192
+ if (!profile) return undefined
193
+ if (spec.harness !== undefined && profile.harness !== spec.harness) {
194
+ throw new Error(
195
+ `proposer ${spec.name}: harness ${spec.harness} conflicts with profile.harness ${profile.harness}`,
196
+ )
197
+ }
198
+ if (spec.model !== undefined && profile.model?.default !== spec.model) {
199
+ throw new Error(
200
+ `proposer ${spec.name}: model ${spec.model} conflicts with profile.model.default ${profile.model?.default ?? 'missing'}`,
201
+ )
202
+ }
203
+ return profile
218
204
  }
219
205
 
220
206
  // ---------------------------------------------------------------------------
@@ -353,7 +339,7 @@ export function sliceFindings(
353
339
  findings: ReadonlyArray<ProposalFinding>,
354
340
  slice: ProposerSpec['diagnosisSlice'],
355
341
  ): ProposalFinding[] {
356
- if (slice === undefined || slice === 'all') return findings
342
+ if (slice === undefined || slice === 'all') return [...findings]
357
343
  return findings.filter((finding) => {
358
344
  const f = finding as unknown as Record<string, unknown>
359
345
  if (isSteeringOrRawTrace(f)) return true
@@ -402,47 +388,14 @@ export function proposerBuildPrompt(
402
388
 
403
389
  export function proposerShotHooks(opts: {
404
390
  shotDir: string
405
- harness: ProposerSpec['harness']
406
- ledger: () => CostLedgerHandle | undefined
407
- phase: () => string | undefined
408
391
  }): (receipt: AgenticGeneratorShotReceipt, execution: AgenticGeneratorShotExecution | null) => Promise<void> {
409
392
  return async (receipt, execution) => {
410
- const tail = (s: string | undefined): string | null =>
411
- s === undefined ? null : s.length > 20_000 ? s.slice(-20_000) : s
412
393
  await mkdir(opts.shotDir, { recursive: true })
413
394
  const name = `gen${receipt.generation ?? 'x'}-cand${receipt.candidateIndex ?? 'x'}-shot${receipt.shot}.json`
414
395
  await writeFile(
415
396
  join(opts.shotDir, name),
416
- JSON.stringify({ receipt, stdoutTail: tail(execution?.stdout), stderrTail: tail(execution?.stderr) }, null, 2),
397
+ JSON.stringify({ receipt, execution }, null, 2),
417
398
  )
418
- // Proposer-shot spend → the lib's run ledger. The generator only settles
419
- // its own receipts on the codexReproducible path (costCallId non-null);
420
- // the claude/opencode author path otherwise leaves every shot as $0 in
421
- // the run's spend summary. Import the shot receipt's measured usage.
422
- const ledger = opts.ledger()
423
- if (ledger && receipt.costCallId === null && (receipt.usage || receipt.costUsdKnown)) {
424
- const usage = receipt.usage
425
- const paid = await ledger.runPaidCall({
426
- channel: 'driver',
427
- phase: opts.phase() ?? 'search.proposal',
428
- actor: `proposer-shot:${opts.harness}`,
429
- model: receipt.model ?? `${opts.harness}-cli`,
430
- tags: {
431
- generation: String(receipt.generation ?? -1),
432
- candidateIndex: String(receipt.candidateIndex ?? -1),
433
- shot: String(receipt.shot),
434
- },
435
- execute: async () => receipt,
436
- receipt: () => ({
437
- model: receipt.model ?? `${opts.harness}-cli`,
438
- inputTokens: usage?.inputTokens ?? 0,
439
- outputTokens: usage ? usage.outputTokens + usage.reasoningOutputTokens : 0,
440
- ...(usage ? { cachedTokens: usage.cachedInputTokens } : { usageUnknown: true }),
441
- ...(receipt.costUsdKnown && receipt.costUsd !== null ? { actualCostUsd: receipt.costUsd } : {}),
442
- }),
443
- })
444
- if (!paid.succeeded) throw paid.error
445
- }
446
399
  }
447
400
  }
448
401
 
@@ -512,7 +465,6 @@ function defaultAuthor(config: OuterLoopConfig, deps: FanOutDeps): AuthorFn {
512
465
  // One agenticGenerator per proposer, created lazily and cached: each carries
513
466
  // its own profile, lens prompt, and shot-receipt home.
514
467
  const inners = new Map<string, CandidateGenerator>()
515
- const ledgers = new Map<string, { ledger?: CostLedgerHandle; phase?: string }>()
516
468
  // The GEPA seat authors through the agent-eval adapter, not a
517
469
  // harness CLI. Its inner evaluator is the SAME injected smoke runner the
518
470
  // pre-filter uses (presence enforced at generator construction).
@@ -531,20 +483,27 @@ function defaultAuthor(config: OuterLoopConfig, deps: FanOutDeps): AuthorFn {
531
483
  })
532
484
  return gepaAuthor(proposer, args)
533
485
  }
534
- const harness = proposer.harness
535
- if (harness === undefined) {
536
- throw new Error(`proposer ${proposer.name}: harness is required for a non-engine seat`)
537
- }
538
486
  let inner = inners.get(proposer.name)
539
- const slot = ledgers.get(proposer.name) ?? {}
540
- ledgers.set(proposer.name, slot)
541
487
  if (!inner) {
542
- // GEN-4: the resolved profile carries the spec's pinned model
543
- // (`model.default` → the harness CLI's `-m` flag).
544
488
  const profile = resolveAuthorProfile(proposer)
489
+ if (!profile) {
490
+ throw new Error(`proposer ${proposer.name}: profile is required for a Runtime author seat`)
491
+ }
492
+ const bridgeUrl = process.env.CLI_BRIDGE_URL ?? process.env.BRIDGE_URL
493
+ const bridgeBearer = process.env.CLI_BRIDGE_BEARER ?? process.env.BRIDGE_BEARER
494
+ if (!bridgeUrl || !bridgeBearer) {
495
+ throw new Error(
496
+ 'proposer authoring requires CLI_BRIDGE_URL/BRIDGE_URL and CLI_BRIDGE_BEARER/BRIDGE_BEARER',
497
+ )
498
+ }
545
499
  inner = agenticGenerator({
546
- harness,
547
- ...(profile ? { profile } : {}),
500
+ profile,
501
+ executorForWorktree: (cwd) => ({
502
+ backend: 'bridge',
503
+ bridgeUrl,
504
+ bridgeBearer,
505
+ cwd,
506
+ }),
548
507
  timeoutMs: config.proposerTimeoutMs,
549
508
  buildPrompt: (args) =>
550
509
  proposerBuildPrompt(args, proposer, deps.parents ?? [], {
@@ -552,18 +511,12 @@ function defaultAuthor(config: OuterLoopConfig, deps: FanOutDeps): AuthorFn {
552
511
  ...(config.activationGate === true ? { activationGate: true } : {}),
553
512
  }),
554
513
  verify: loopsCandidateVerifier(config.loopsRepo),
555
- runHarness: (options) => runLocalHarness({ ...options, env: proposerShotEnv(harness) }),
556
514
  onShotCompleted: proposerShotHooks({
557
515
  shotDir: join(config.outDir, 'proposer-shots', sanitize(proposer.name)),
558
- harness,
559
- ledger: () => slot.ledger,
560
- phase: () => slot.phase,
561
516
  }),
562
517
  })
563
518
  inners.set(proposer.name, inner)
564
519
  }
565
- slot.ledger = args.costLedger
566
- slot.phase = args.costPhase
567
520
  return inner.generate(args)
568
521
  }
569
522
  }
@@ -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-code', lens: 'Prefer code-path fixes.' }
76
+ const spec: ProposerSpec = { name: 'x', harness: 'pi', 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-code' }
84
+ const spec: ProposerSpec = { name: 'x', harness: 'pi' }
85
85
  expect(proposerBuildPrompt({ findings: [] }, spec)).not.toContain('AUTHORING LENS')
86
86
  })
87
87
  })
@@ -106,35 +106,47 @@ const PARENTS: ParetoParentContext[] = [
106
106
  },
107
107
  ]
108
108
 
109
- describe('resolveAuthorProfile (pinned models)', () => {
110
- it('merges the spec model into the loaded profile as model.default', () => {
109
+ describe('resolveAuthorProfile', () => {
110
+ it('returns the exact committed profile when provenance mirrors agree', () => {
111
111
  const spec: ProposerSpec = {
112
112
  name: 'x',
113
113
  profile: 'default-author.profile.json',
114
- harness: 'claude-code',
115
- model: 'claude-fable-5',
114
+ harness: 'pi',
115
+ model: 'glm-5.2',
116
116
  }
117
117
  const profile = resolveAuthorProfile(spec)
118
118
  expect(profile?.name).toBe('swe-arena-default-author')
119
- expect(profile?.model?.default).toBe('claude-fable-5')
119
+ expect(profile?.harness).toBe('pi')
120
+ expect(profile?.model?.default).toBe('glm-5.2')
120
121
  })
121
122
 
122
- it('synthesizes a minimal named profile for a pinned model without a profile path', () => {
123
- const profile = resolveAuthorProfile({ name: 'glm-author', harness: 'opencode', model: 'zai-coding-plan/glm-5.2' })
124
- expect(profile?.name).toBe('glm-author-pinned')
125
- expect(profile?.model?.default).toBe('zai-coding-plan/glm-5.2')
123
+ it('rejects duplicated harness or model fields that conflict with the profile', () => {
124
+ expect(() =>
125
+ resolveAuthorProfile({
126
+ name: 'bad-harness',
127
+ profile: 'default-author.profile.json',
128
+ harness: 'codex',
129
+ }),
130
+ ).toThrow(/conflicts with profile\.harness/)
131
+ expect(() =>
132
+ resolveAuthorProfile({
133
+ name: 'bad-model',
134
+ profile: 'default-author.profile.json',
135
+ model: 'different-model',
136
+ }),
137
+ ).toThrow(/conflicts with profile\.model\.default/)
126
138
  })
127
139
 
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-code' }
140
+ it('does not synthesize an ambient profile when the path is missing', () => {
141
+ const spec: ProposerSpec = { name: 'x', profile: 'default-author.profile.json', harness: 'pi' }
130
142
  expect(resolveAuthorProfile(spec)).toEqual(loadAuthorProfile(spec))
131
- expect(resolveAuthorProfile({ name: 'bare', harness: 'claude-code' })).toBeUndefined()
143
+ expect(resolveAuthorProfile({ name: 'bare', harness: 'pi' })).toBeUndefined()
132
144
  })
133
145
  })
134
146
 
135
147
  describe('proposerBuildPrompt with pareto parents', () => {
136
148
  it('appends the parents section (evidence + diffs) after the protocol prompt and lens', () => {
137
- const spec: ProposerSpec = { name: 'x', harness: 'claude-code', lens: 'Prefer code-path fixes.' }
149
+ const spec: ProposerSpec = { name: 'x', harness: 'pi', lens: 'Prefer code-path fixes.' }
138
150
  const prompt = proposerBuildPrompt({ findings: [] }, spec, PARENTS)
139
151
  expect(prompt).toContain('DECLARED CHANGE-SPACE')
140
152
  expect(prompt).toContain('PARETO PARENTS')
@@ -146,14 +158,14 @@ describe('proposerBuildPrompt with pareto parents', () => {
146
158
  })
147
159
 
148
160
  it('leaves the prompt untouched when no parents are seeded (gen-3 behavior)', () => {
149
- const spec: ProposerSpec = { name: 'x', harness: 'claude-code' }
161
+ const spec: ProposerSpec = { name: 'x', harness: 'pi' }
150
162
  expect(proposerBuildPrompt({ findings: [] }, spec)).not.toContain('PARETO PARENTS')
151
163
  expect(parentsPromptSection(PARENTS)).toContain('measured evidence')
152
164
  })
153
165
  })
154
166
 
155
167
  describe('mergeAuthorPrompt', () => {
156
- const spec: ProposerSpec = { name: 'merge-author', harness: 'claude-code', merge: true }
168
+ const spec: ProposerSpec = { name: 'merge-author', harness: 'pi', merge: true }
157
169
 
158
170
  it('keeps the change-space contract and presents BOTH parent diffs with the coherent-union task', () => {
159
171
  const prompt = proposerBuildPrompt({ findings: [] }, spec, PARENTS)
@@ -178,21 +190,23 @@ describe('mergeAuthorPrompt', () => {
178
190
  describe('defaultGen4Config', () => {
179
191
  const config = defaultGen4Config()
180
192
 
181
- it('seats four proposers (one candidate slot each) with the pinned glm model and the merge seat', () => {
182
- expect(config.proposers).toHaveLength(4)
183
- expect(config.populationSize).toBe(4)
193
+ it('seats Pi authors with exact GLM and DeepSeek profiles', () => {
194
+ expect(config.proposers).toHaveLength(3)
195
+ expect(config.populationSize).toBe(3)
184
196
  const byName = Object.fromEntries(config.proposers!.map((p) => [p.name, p]))
185
- expect(byName['claude-author']).toMatchObject({ harness: 'claude-code', profile: 'default-author.profile.json' })
186
- expect(byName['claude-author']!.model).toBeUndefined()
187
- expect(byName['glm-author']).toMatchObject({ harness: 'opencode', model: 'zai-coding-plan/glm-5.2' })
188
- expect(byName['codex-author']).toMatchObject({ harness: 'codex' })
189
- expect(byName['merge-author']).toMatchObject({ harness: 'claude-code', merge: true })
197
+ expect(byName['glm-author']).toMatchObject({ harness: 'pi', model: 'glm-5.2' })
198
+ expect(byName['deepseek-author']).toMatchObject({
199
+ harness: 'pi',
200
+ model: 'deepseek-v4-flash',
201
+ profile: 'deepseek-author.profile.json',
202
+ })
203
+ expect(byName['merge-author']).toMatchObject({ harness: 'pi', merge: true })
190
204
  })
191
205
 
192
- it('drops the codex seat (and shrinks the population) when includeCodex is false', () => {
193
- const noCodex = defaultGen4Config(undefined, { includeCodex: false })
194
- expect(noCodex.proposers!.map((p) => p.name)).toEqual(['claude-author', 'glm-author', 'merge-author'])
195
- expect(noCodex.populationSize).toBe(3)
206
+ it('can omit the DeepSeek seat while keeping population aligned', () => {
207
+ const noDeepseek = defaultGen4Config(undefined, { includeDeepseek: false })
208
+ expect(noDeepseek.proposers!.map((p) => p.name)).toEqual(['glm-author', 'merge-author'])
209
+ expect(noDeepseek.populationSize).toBe(2)
196
210
  })
197
211
 
198
212
  it('seeds the gen-3 frontier as pareto parents and points at a fresh gen4 outDir + baseline artifact', () => {
@@ -215,24 +229,40 @@ describe('defaultGen4Config', () => {
215
229
  })
216
230
 
217
231
  describe('loadAuthorProfile', () => {
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-code' })
232
+ it('loads the complete committed Pi/GLM author profile', () => {
233
+ const profile = loadAuthorProfile({
234
+ name: 'a',
235
+ profile: 'default-author.profile.json',
236
+ harness: 'pi',
237
+ })
220
238
  expect(profile?.name).toBe('swe-arena-default-author')
221
- expect(profile?.prompt).toBeUndefined()
222
- expect(profile?.model).toBeUndefined()
239
+ expect(profile?.harness).toBe('pi')
240
+ expect(profile?.prompt?.systemPrompt).toContain('measured failure evidence')
241
+ expect(profile?.model).toMatchObject({ provider: 'tangle-router', default: 'glm-5.2' })
223
242
  expect(existsSync(join(PROFILES_DIR, 'default-author.profile.json'))).toBe(true)
224
243
  })
225
244
 
226
245
  it('returns undefined without a profile path', () => {
227
- expect(loadAuthorProfile({ name: 'a', harness: 'claude-code' })).toBeUndefined()
246
+ expect(loadAuthorProfile({ name: 'a', harness: 'pi' })).toBeUndefined()
228
247
  })
229
248
 
230
- it('fails loud when a non-codex proposer declares profile resources (they would be dropped)', async () => {
249
+ it('preserves profile resources for Runtime materialization', async () => {
231
250
  const dir = await mkdtemp(join(tmpdir(), 'profile-'))
232
251
  try {
233
252
  const path = join(dir, 'with-resources.json')
234
- await writeFile(path, JSON.stringify({ name: 'r', resources: { files: [] } }))
235
- expect(() => loadAuthorProfile({ name: 'a', profile: path, harness: 'claude-code' })).toThrow(/silently drop/)
253
+ await writeFile(
254
+ path,
255
+ JSON.stringify({
256
+ name: 'r',
257
+ harness: 'pi',
258
+ model: { provider: 'test', default: 'model' },
259
+ prompt: { systemPrompt: 'Author changes.' },
260
+ resources: { files: [] },
261
+ }),
262
+ )
263
+ expect(loadAuthorProfile({ name: 'a', profile: path, harness: 'pi' })?.resources).toEqual({
264
+ files: [],
265
+ })
236
266
  } finally {
237
267
  await rm(dir, { recursive: true, force: true })
238
268
  }
@@ -300,9 +330,14 @@ describe('defaultGen3Config', () => {
300
330
  }
301
331
  })
302
332
 
303
- it('defaultProposers codifies the gen-2 author: one bare-profile claude entry', () => {
333
+ it('defaultProposers uses one exact Pi/GLM profile', () => {
304
334
  expect(defaultProposers()).toEqual([
305
- { name: 'default-author', profile: 'default-author.profile.json', harness: 'claude-code' },
335
+ {
336
+ name: 'default-author',
337
+ profile: 'default-author.profile.json',
338
+ harness: 'pi',
339
+ model: 'glm-5.2',
340
+ },
306
341
  ])
307
342
  })
308
343
  })
@@ -357,8 +392,8 @@ describe('fanOutLoopsGenerator', () => {
357
392
 
358
393
  it('authors ALL proposers concurrently in separate worktrees and applies each patch to its candidate slot', async () => {
359
394
  const proposers: ProposerSpec[] = [
360
- { name: 'alpha', harness: 'claude-code' },
361
- { name: 'beta', harness: 'claude-code' },
395
+ { name: 'alpha', harness: 'pi' },
396
+ { name: 'beta', harness: 'pi' },
362
397
  ]
363
398
  let inFlight = 0
364
399
  let maxInFlight = 0
@@ -396,7 +431,7 @@ describe('fanOutLoopsGenerator', () => {
396
431
 
397
432
  expect(first).toMatchObject({ applied: true, label: 'alpha' })
398
433
  expect(second).toMatchObject({ applied: true, label: 'beta' })
399
- expect(first.rationale).toContain('proposer alpha (claude-code)')
434
+ expect(first.rationale).toContain('proposer alpha (pi)')
400
435
  // Both patches landed on the SAME driver worktree across the two calls —
401
436
  // the driver discards/finalizes between calls in production; here we only
402
437
  // assert each call applied its own proposer's file.
@@ -407,8 +442,8 @@ describe('fanOutLoopsGenerator', () => {
407
442
 
408
443
  it('kills a candidate at the smoke pre-filter: applied=false, no patch applied, kill recorded with reason', async () => {
409
444
  const proposers: ProposerSpec[] = [
410
- { name: 'good', harness: 'claude-code' },
411
- { name: 'bad', harness: 'claude-code' },
445
+ { name: 'good', harness: 'pi' },
446
+ { name: 'bad', harness: 'pi' },
412
447
  ]
413
448
  const config = baseConfig(proposers)
414
449
  config.prefilter = { enabled: true, smokeInstance: 'cheapest-of-set' }
@@ -461,7 +496,7 @@ describe('fanOutLoopsGenerator', () => {
461
496
  })
462
497
 
463
498
  it('kills an out-of-space diff at the change-space pre-filter before any smoke spend', async () => {
464
- const proposers: ProposerSpec[] = [{ name: 'rogue', harness: 'claude-code' }]
499
+ const proposers: ProposerSpec[] = [{ name: 'rogue', harness: 'pi' }]
465
500
  const config = baseConfig(proposers)
466
501
  config.prefilter = { enabled: true, smokeInstance: 'cheapest-of-set' }
467
502
  let smokeRan = false
@@ -488,7 +523,7 @@ describe('fanOutLoopsGenerator', () => {
488
523
  })
489
524
 
490
525
  it('returns applied:false without a kill when a proposer authors nothing', async () => {
491
- const gen = fanOutLoopsGenerator(baseConfig([{ name: 'idle', harness: 'claude-code' }]), {
526
+ const gen = fanOutLoopsGenerator(baseConfig([{ name: 'idle', harness: 'pi' }]), {
492
527
  author: async () => ({ applied: false, summary: '' }),
493
528
  })
494
529
  expect((await gen.generate(generatorArgs(0))).applied).toBe(false)
@@ -496,7 +531,7 @@ describe('fanOutLoopsGenerator', () => {
496
531
  })
497
532
 
498
533
  it('fails loud when candidateIndex exceeds the proposer list (populationSize drift)', async () => {
499
- const gen = fanOutLoopsGenerator(baseConfig([{ name: 'only', harness: 'claude-code' }]), {
534
+ const gen = fanOutLoopsGenerator(baseConfig([{ name: 'only', harness: 'pi' }]), {
500
535
  author: async () => ({ applied: false, summary: '' }),
501
536
  })
502
537
  await expect(gen.generate(generatorArgs(1))).rejects.toThrow(/populationSize must equal/)
@@ -520,7 +555,7 @@ describe('fanOutLoopsGenerator', () => {
520
555
  })
521
556
 
522
557
  it('refuses a merge seat without >=2 materialized parents', () => {
523
- const config = baseConfig([{ name: 'merge-author', harness: 'claude-code', merge: true }])
558
+ const config = baseConfig([{ name: 'merge-author', harness: 'pi', merge: true }])
524
559
  expect(() => fanOutLoopsGenerator(config, { author: async () => ({ applied: false, summary: '' }) })).toThrow(
525
560
  /merge proposer/,
526
561
  )
@@ -539,8 +574,8 @@ describe('fanOutLoopsGenerator', () => {
539
574
  it('rejects duplicate proposer names and empty proposer lists', () => {
540
575
  expect(() =>
541
576
  fanOutLoopsGenerator(baseConfig([
542
- { name: 'dup', harness: 'claude-code' },
543
- { name: 'dup', harness: 'claude-code' },
577
+ { name: 'dup', harness: 'pi' },
578
+ { name: 'dup', harness: 'pi' },
544
579
  ])),
545
580
  ).toThrow(/duplicate/)
546
581
  expect(() => fanOutLoopsGenerator({ ...defaultRound4Config(), loopsRepo, outDir })).toThrow(/empty/)
@@ -576,10 +611,10 @@ describe('defaultGen5Config', () => {
576
611
  expect(config.priorEvidenceDirs!.some((d) => d.includes('gen4'))).toBe(true)
577
612
  })
578
613
 
579
- it('drops the codex seat when includeCodex is false (population follows)', () => {
580
- const noCodex = defaultGen5Config(undefined, { includeCodex: false })
581
- expect(noCodex.proposers!.map((p) => p.name)).toEqual(['claude-author', 'glm-author', 'merge-author'])
582
- expect(noCodex.populationSize).toBe(3)
614
+ it('can omit the DeepSeek seat while keeping population aligned', () => {
615
+ const noDeepseek = defaultGen5Config(undefined, { includeDeepseek: false })
616
+ expect(noDeepseek.proposers!.map((p) => p.name)).toEqual(['glm-author', 'merge-author'])
617
+ expect(noDeepseek.populationSize).toBe(2)
583
618
  })
584
619
  })
585
620
 
@@ -591,7 +626,7 @@ describe('gen-5 prompt sections', () => {
591
626
  }
592
627
 
593
628
  it('appends EVIDENCE MAP + briefing + activation contract after the protocol prompt', () => {
594
- const spec: ProposerSpec = { name: 'x', harness: 'claude-code' }
629
+ const spec: ProposerSpec = { name: 'x', harness: 'pi' }
595
630
  const prompt = proposerBuildPrompt({ findings: [] }, spec, [], {
596
631
  briefing,
597
632
  activationGate: true,
@@ -604,7 +639,7 @@ describe('gen-5 prompt sections', () => {
604
639
  })
605
640
 
606
641
  it('the merge seat gets the gen-5 sections too', () => {
607
- const spec: ProposerSpec = { name: 'merge-author', harness: 'claude-code', merge: true }
642
+ const spec: ProposerSpec = { name: 'merge-author', harness: 'pi', merge: true }
608
643
  const prompt = proposerBuildPrompt({ findings: [] }, spec, PARENTS, {
609
644
  briefing,
610
645
  activationGate: true,
@@ -615,7 +650,7 @@ describe('gen-5 prompt sections', () => {
615
650
  })
616
651
 
617
652
  it('leaves gen-3/gen-4 prompts byte-identical when no extras are passed', () => {
618
- const spec: ProposerSpec = { name: 'x', harness: 'claude-code' }
653
+ const spec: ProposerSpec = { name: 'x', harness: 'pi' }
619
654
  const legacy = proposerBuildPrompt({ findings: [] }, spec, PARENTS)
620
655
  expect(legacy).not.toContain('EVIDENCE MAP')
621
656
  expect(legacy).not.toContain('ACTIVATION PREDICATE')
@@ -120,7 +120,7 @@ export async function captureProposerProvenance(
120
120
  for (const harness of harnesses) {
121
121
  // The harness id is not the binary name (`claude-code` runs `claude`); read the executable
122
122
  // from the runtime's harness table rather than spawning the id.
123
- const executable = localHarnessExecutable(harness)
123
+ const executable = harness === 'pi' ? 'pi' : localHarnessExecutable(harness)
124
124
  const res = await exec(executable, ['--version'])
125
125
  if (res.code !== 0) {
126
126
  throw new Error(