@tangle-network/agent-bench 0.3.7 → 0.4.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 (211) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/HARNESS.md +43 -0
  3. package/README.md +7 -0
  4. package/dist/adapters.js +23 -23
  5. package/dist/benchmarks/_harness.d.ts +1 -1
  6. package/dist/benchmarks/_harness.js +1 -1
  7. package/dist/benchmarks/aec-bench.js +2 -2
  8. package/dist/benchmarks/agentbench.js +2 -2
  9. package/dist/benchmarks/appworld.js +2 -2
  10. package/dist/benchmarks/bfcl.js +2 -2
  11. package/dist/benchmarks/commit0.js +2 -2
  12. package/dist/benchmarks/crag.js +2 -2
  13. package/dist/benchmarks/dabstep.js +2 -2
  14. package/dist/benchmarks/enterpriseops-gym.js +2 -2
  15. package/dist/benchmarks/finresearchbench.js +2 -2
  16. package/dist/benchmarks/nomiracl.js +2 -2
  17. package/dist/benchmarks/open-rag-bench.js +2 -2
  18. package/dist/benchmarks/programbench.js +2 -2
  19. package/dist/benchmarks/ragbench.js +2 -2
  20. package/dist/benchmarks/swe-bench.js +2 -2
  21. package/dist/benchmarks/t2-ragbench.js +2 -2
  22. package/dist/benchmarks/tau-bench-shared.js +2 -2
  23. package/dist/benchmarks/tau2-bench.js +3 -3
  24. package/dist/benchmarks/tau3-banking.js +3 -3
  25. package/dist/benchmarks/terminal-bench.js +2 -2
  26. package/dist/benchmarks/toollm.js +2 -2
  27. package/dist/benchmarks/webarena-verified.js +2 -2
  28. package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
  29. package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
  30. package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
  31. package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
  32. package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
  33. package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
  34. package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
  35. package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
  36. package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
  37. package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
  38. package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
  39. package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
  40. package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
  41. package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
  42. package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
  43. package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
  44. package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
  45. package/dist/{chunk-PWQVGAJB.js → chunk-V7AEBY6U.js} +21 -21
  46. package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
  47. package/dist/chunk-WSKWVEQB.js.map +1 -0
  48. package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
  49. package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
  50. package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
  51. package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
  52. package/dist/index.js +23 -23
  53. package/package.json +5 -5
  54. package/scripts/trata-hedge/README.md +6 -5
  55. package/scripts/verify-packed-consumer.mjs +1 -1
  56. package/src/benchmarks/_harness.ts +20 -2
  57. package/src/benchmarks/humaneval.test.mts +2 -2
  58. package/src/gate.ts +1 -1
  59. package/src/hev-eval.mts +5 -2
  60. package/src/hev-improve.mts +118 -73
  61. package/src/official-optimizer-config.mts +89 -0
  62. package/src/official-optimizer-config.test.mts +88 -0
  63. package/src/profiles.ts +2 -2
  64. package/src/quant-arena/README.md +144 -0
  65. package/src/quant-arena/backtest.test.mts +135 -0
  66. package/src/quant-arena/backtest.ts +218 -0
  67. package/src/quant-arena/data.test.mts +44 -0
  68. package/src/quant-arena/data.ts +141 -0
  69. package/src/quant-arena/driver.test.mts +253 -0
  70. package/src/quant-arena/driver.ts +219 -0
  71. package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
  72. package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
  73. package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
  74. package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
  75. package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
  76. package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
  77. package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
  78. package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
  79. package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
  80. package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
  81. package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
  82. package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
  83. package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
  84. package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
  85. package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
  86. package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
  87. package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
  88. package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
  89. package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
  90. package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
  91. package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
  92. package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
  93. package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
  94. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
  95. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
  96. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
  97. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
  98. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
  99. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
  100. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
  101. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
  102. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
  103. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
  104. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
  105. package/src/quant-arena/holdout-certify.mts +206 -0
  106. package/src/quant-arena/holdout-certify.test.mts +82 -0
  107. package/src/quant-arena/leak-audit.test.mts +79 -0
  108. package/src/quant-arena/leak-audit.ts +95 -0
  109. package/src/quant-arena/make-fixtures.mts +161 -0
  110. package/src/quant-arena/multiplicity.test.mts +68 -0
  111. package/src/quant-arena/multiplicity.ts +87 -0
  112. package/src/quant-arena/nautilus-certify.ts +31 -0
  113. package/src/quant-arena/oms.ts +90 -0
  114. package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
  115. package/src/quant-arena/python/pyproject.toml +8 -0
  116. package/src/quant-arena/python/uv.lock +1297 -0
  117. package/src/quant-arena/python/vbt-worker.py +192 -0
  118. package/src/quant-arena/quant-loop.mts +813 -0
  119. package/src/quant-arena/quant-loop.test.mts +75 -0
  120. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
  121. package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
  122. package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
  123. package/src/quant-arena/types.ts +133 -0
  124. package/src/quant-arena/vbt-client.ts +321 -0
  125. package/src/quant-arena/vbt-parity.test.mts +183 -0
  126. package/src/quant-arena/windows.test.mts +45 -0
  127. package/src/quant-arena/windows.ts +54 -0
  128. package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
  129. package/src/rollout-ledger/backfill-swe-arena.test.mts +342 -0
  130. package/src/rollout-ledger/settle-capture.mts +442 -0
  131. package/src/rollout-ledger/settle-capture.test.mts +270 -0
  132. package/src/smoke-structural-rollout.mts +15 -9
  133. package/src/swe-arena/activation.mts +225 -0
  134. package/src/swe-arena/activation.test.mts +300 -0
  135. package/src/swe-arena/arms.ts +30 -14
  136. package/src/swe-arena/briefing.mts +217 -0
  137. package/src/swe-arena/briefing.test.mts +178 -0
  138. package/src/swe-arena/calibrate.ts +102 -1
  139. package/src/swe-arena/cell-evidence.mts +41 -9
  140. package/src/swe-arena/factory-command-container.ts +284 -0
  141. package/src/swe-arena/factory-judge-child.mts +228 -0
  142. package/src/swe-arena/factory.test.mts +643 -0
  143. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
  144. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
  145. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
  146. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
  147. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
  148. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
  149. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
  150. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
  151. package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
  152. package/src/swe-arena/fixtures/run-report/README.md +43 -0
  153. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
  154. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
  155. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
  156. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
  157. package/src/swe-arena/fixtures.ts +135 -2
  158. package/src/swe-arena/gepa-seat.mts +877 -0
  159. package/src/swe-arena/gepa-seat.test.mts +1059 -0
  160. package/src/swe-arena/implementation-ref.test.mts +64 -0
  161. package/src/swe-arena/implementation-ref.ts +62 -0
  162. package/src/swe-arena/ledger-orphans.mts +77 -0
  163. package/src/swe-arena/ledger-orphans.test.mts +147 -0
  164. package/src/swe-arena/outer-loop.mts +710 -33
  165. package/src/swe-arena/premeasured-from-cells.mts +281 -0
  166. package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
  167. package/src/swe-arena/proposer-fanout.mts +327 -31
  168. package/src/swe-arena/proposer-fanout.test.mts +246 -0
  169. package/src/swe-arena/proposer-provenance.mts +172 -0
  170. package/src/swe-arena/proposer-provenance.test.mts +106 -0
  171. package/src/swe-arena/run-experiment.mts +367 -1
  172. package/src/swe-arena/run-report.mts +75 -0
  173. package/src/swe-arena/run-supervisor.test.mts +5 -3
  174. package/src/swe-arena/score-split.mts +140 -0
  175. package/src/swe-arena/score-split.test.mts +123 -0
  176. package/src/swe-arena/scratch-worktree.test.mts +55 -0
  177. package/src/swe-arena/scratch-worktree.ts +34 -0
  178. package/src/swe-arena/types.ts +52 -0
  179. package/src/swe-code-improve.mts +24 -25
  180. package/src/swe-improve.mts +129 -96
  181. package/src/swe-local-proof.mts +6 -1
  182. package/src/swe-stream.mts +4 -2
  183. package/src/swe-structural.mts +245 -837
  184. package/src/tb-container-executor.test.mts +30 -6
  185. package/src/tb-supervisor-sidecar.mts +2 -1
  186. package/src/trata-gepa.mts +182 -245
  187. package/dist/chunk-LRRD7NAG.js.map +0 -1
  188. package/src/live-improve-campaign-mbpp.mts +0 -641
  189. package/src/live-improve-campaign.mts +0 -500
  190. /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
  191. /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
  192. /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
  193. /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
  194. /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
  195. /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
  196. /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
  197. /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
  198. /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
  199. /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
  200. /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
  201. /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
  202. /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
  203. /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
  204. /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
  205. /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
  206. /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
  207. /package/dist/{chunk-PWQVGAJB.js.map → chunk-V7AEBY6U.js.map} +0 -0
  208. /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
  209. /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
  210. /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
  211. /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
@@ -17,7 +17,7 @@
17
17
  * `rawTraceDistiller` path-context so the coding agent also greps the raw
18
18
  * traces itself (`rawTraceContext: true` names the mechanism; an explicit
19
19
  * `analyzeGeneration` wins, so the distiller is composed in directly).
20
- * (b) PROPOSE — `improvementDriver` + a change-space-constrained
20
+ * (b) PROPOSE — Runtime's code candidate driver + a change-space-constrained
21
21
  * `agenticGenerator` edit an isolated git worktree of loops. The DECLARED
22
22
  * CHANGE-SPACE is enforced twice: in the generator's verifier (feedback →
23
23
  * next shot) and fail-closed in the dispatch below (an out-of-space
@@ -59,7 +59,7 @@
59
59
  * generator's evidence gate requires).
60
60
  */
61
61
 
62
- import { appendFile, mkdir, readFile, rm, symlink, unlink, writeFile } from 'node:fs/promises'
62
+ import { appendFile, mkdir, readdir, readFile, rm, symlink, unlink, writeFile } from 'node:fs/promises'
63
63
  import { existsSync } from 'node:fs'
64
64
  import process from 'node:process'
65
65
  import { join } from 'node:path'
@@ -72,6 +72,7 @@ import {
72
72
  type Verifier,
73
73
  } from '@tangle-network/agent-runtime'
74
74
  import { runLocalHarness } from '@tangle-network/agent-runtime/mcp'
75
+ import { canonicalCandidateDigest } from '@tangle-network/agent-interface'
75
76
  import { makeFinding } from '@tangle-network/agent-eval'
76
77
  import {
77
78
  FsLabeledScenarioStore,
@@ -85,7 +86,12 @@ import {
85
86
  type Scenario,
86
87
  } from '@tangle-network/agent-eval/campaign'
87
88
  import type { CostLedgerHandle } from '@tangle-network/agent-eval'
89
+ import { runVenvPython } from '../benchmarks/_harness.ts'
88
90
  import { createSweBenchAdapter } from '../benchmarks/swe-bench.ts'
91
+ import {
92
+ fileTreeImplementationRef,
93
+ pythonDistributionImplementationRef,
94
+ } from './implementation-ref.ts'
89
95
  import {
90
96
  baselineDriftWarnings,
91
97
  cellsFromCampaign,
@@ -122,13 +128,46 @@ import {
122
128
  import {
123
129
  defaultProposers,
124
130
  fanOutLoopsGenerator,
131
+ materializeParetoParents,
125
132
  proposerShotHooks,
133
+ type ParetoParentContext,
134
+ type ParetoParentSeed,
126
135
  type PrefilterConfig,
127
136
  type PrefilterKill,
128
137
  type ProposerSpec,
129
138
  type SmokeRunner,
130
139
  type SmokeVerdict,
131
140
  } from './proposer-fanout.mts'
141
+ import { captureProposerProvenance } from './proposer-provenance.mts'
142
+ import { CRASH_ORPHAN_REASON, reconcileCrashOrphansOnDisk } from './ledger-orphans.mts'
143
+ import {
144
+ AUTHOR_BRIEFING_VERSION,
145
+ resolveAuthorBriefing,
146
+ writeEvidenceIndex,
147
+ type BriefingContext,
148
+ } from './briefing.mts'
149
+ import {
150
+ readCommittedPredicate,
151
+ runActivationPredicate,
152
+ ACTIVATION_PREDICATE_RELPATH,
153
+ type ActivationRecord,
154
+ } from './activation.mts'
155
+ import {
156
+ loadOrCreateScoreSplit,
157
+ subScores,
158
+ type ScoreSplit,
159
+ type ScoreSplitConfig,
160
+ } from './score-split.mts'
161
+ import {
162
+ reportSupervisorRound,
163
+ writeSupervisorRunReportSafe,
164
+ } from '@tangle-network/agent-eval/supervisor-run'
165
+ import {
166
+ campaignCoordsFromCellPath,
167
+ createSettleCapture,
168
+ type SettleCapture,
169
+ } from '../rollout-ledger/settle-capture.mts'
170
+ import { findSupervisorRunDir } from './arms.ts'
132
171
  import { installProcessSignalAbort, run, runOk } from './proc.ts'
133
172
  import { loadInstanceImages } from './run-experiment.mts'
134
173
  import {
@@ -480,6 +519,16 @@ export interface StaircaseRow {
480
519
  armProvenance: { repo: string; commit: string } | null
481
520
  diffPath: string | null
482
521
  diffSha256: string | null
522
+ /** GEN-5 public/private sub-scores (selection stays on the combined count;
523
+ * the private sub-score is never surfaced to proposers). */
524
+ split?: {
525
+ publicInstances: string[]
526
+ privateInstances: string[]
527
+ publicResolvedCount: number
528
+ privateResolvedCount: number
529
+ }
530
+ /** GEN-5 activation-gate outcome for this candidate. */
531
+ activation?: ActivationRecord
483
532
  }
484
533
 
485
534
  const STAIRCASE_VERDICTS: ReadonlySet<string> = new Set([
@@ -489,6 +538,7 @@ const STAIRCASE_VERDICTS: ReadonlySet<string> = new Set([
489
538
  'rejected-out-of-space',
490
539
  'rejected-incomplete',
491
540
  'rejected-prefilter',
541
+ 'quarantined-inactive',
492
542
  ])
493
543
 
494
544
  /** Parse + validate one staircase JSONL row. Throws on schema drift. */
@@ -598,12 +648,22 @@ export interface OuterLoopConfig {
598
648
  * an AgentProfile-pinned harness invocation (see proposer-fanout.mts).
599
649
  * When set, `populationSize` MUST equal `proposers.length` (one candidate
600
650
  * slot per proposer — enforced at launch). Unset = the legacy
601
- * single-author generator (`proposerHarness` + bare invocation). */
651
+ * single-author generator (`proposerHarness` + bare invocation).
652
+ * A spec with `engine` set is a GEPA seat (gepa-seat.mts); the
653
+ * agent-eval external-GEPA adapter optimizes ONE change-space file as a
654
+ * string against the pre-filter smoke cell; requires `prefilter.enabled`. */
602
655
  proposers?: ProposerSpec[]
603
656
  /** GEN-3 cheap pre-filter: per candidate, change-space + tsc (the authoring
604
657
  * verifier) plus ONE smoke arm cell before any full-evaluation spend.
605
658
  * Killed candidates become `rejected-prefilter` staircase dots. */
606
659
  prefilter?: PrefilterConfig
660
+ /** GEN-4 Pareto parents: prior-run frontier candidates (loops commits +
661
+ * measured per-instance results) seeded into every author's prompt and
662
+ * the merge seat's explicit input. Seeded at OUR buildPrompt seam, not the
663
+ * lib's `ctx.paretoParents` — the lib frontier is within-run only and a
664
+ * prior campaign cannot be injected without its runDir + ledger receipts
665
+ * (see proposer-fanout.mts). */
666
+ paretoParents?: ParetoParentSeed[]
607
667
  /** Replicates per holdout instance in the operator-approved certification
608
668
  * run (holdout-certify.mts). Default 2 — the gen-2 winner failed 3/6 vs
609
669
  * 4/6 on a 1-rep holdout with exactly one discordant cell, a known
@@ -614,6 +674,25 @@ export interface OuterLoopConfig {
614
674
  * reps/fail-closed protocol, or 'measure' — the incumbent runs the same
615
675
  * 2-rep holdout first in the certification run. */
616
676
  holdoutBaseline?: Record<string, boolean> | 'measure'
677
+ /** GEN-5 public/private score split (score-split.mts): proposers + the
678
+ * pre-filter see only PUBLIC instances' scores/evidence; selection stays
679
+ * on the combined set. Unset = everything public (pre-gen-5 behavior). */
680
+ scoreSplit?: ScoreSplitConfig
681
+ /** GEN-5 MAP+TOOLBOX briefing (briefing.mts): write the per-run evidence
682
+ * index and append the toolbox/permission briefing (change-space
683
+ * overridable) to every author prompt. */
684
+ briefing?: typeof AUTHOR_BRIEFING_VERSION
685
+ /** GEN-5 activation gate (activation.mts): require a machine-checkable
686
+ * activation predicate per candidate (prefilter-enforced) and quarantine
687
+ * candidates whose mechanism never fired in their own campaign traces. */
688
+ activationGate?: boolean
689
+ /** GEN-5 settle-time rollout-ledger capture (rollout-ledger/settle-capture.mts):
690
+ * emit tangle.rollout.v1 lines live after each cell judges, with label-v2
691
+ * rewards. Default path: <outDir>/rollout-ledger.jsonl. */
692
+ rolloutLedger?: { enabled: boolean; path?: string; opencodeDb?: string }
693
+ /** GEN-5 evidence map: prior run outDirs whose arm-runs/judge/candidate
694
+ * evidence the authors may mine (rendered into the evidence index). */
695
+ priorEvidenceDirs?: string[]
617
696
  /** Router model ids for the blind diagnosis ensemble (config, never a
618
697
  * hardcoded unrouted model). */
619
698
  analystModels: string[]
@@ -804,6 +883,114 @@ export function defaultGen3Config(
804
883
  }
805
884
  }
806
885
 
886
+ // ---------------------------------------------------------------------------
887
+ // GEN-4 configuration — pinned per-proposer models (recorded in provenance),
888
+ // Pareto-parent seeding from the gen-3 frontier, and a dedicated merge seat.
889
+ // ---------------------------------------------------------------------------
890
+
891
+ /** The gen-3 frontier (run r4-mrwc0awe): winner + runner-up, both 2/6 vs the
892
+ * 1/6 baseline on DIFFERENT instances — complementary lessons, the merge
893
+ * seat's input. Per-instance verdicts are the fail-closed all-reps values
894
+ * from `.evolve/rounds/gen-0.jsonl`. */
895
+ export const GEN3_PARETO_PARENTS: ParetoParentSeed[] = [
896
+ {
897
+ commit: 'cc0d95584c7ea14324cd57c21fe946c7c0f53827',
898
+ label: 'default-author',
899
+ resolvedInstances: ['pydata__xarray-4687', 'sphinx-doc__sphinx-9658'],
900
+ note:
901
+ 'mechanical patch-risk scan (patchRiskWarnings in src/worker-evidence.ts, threaded through ' +
902
+ 'extensions/pi/loops.ts) + never-reword / test-seam / run-the-neighbors worker rules + 3-check ' +
903
+ 'reviewer; pytest-dev__pytest-6197 split 0/1 across reps (near-miss)',
904
+ },
905
+ {
906
+ commit: 'a7a2a982e51551de3a8e796ee2efc448ed405e6a',
907
+ label: 'prompts-author',
908
+ resolvedInstances: ['django__django-11532', 'pydata__xarray-4687'],
909
+ note:
910
+ 'prompt-only: hidden-suite bullet in the supervisor GOAL-authoring section (the django seam), ' +
911
+ 'frozen-behavior worker section + run-the-repo-tests discipline, 2-check reviewer; ' +
912
+ 'sphinx-doc__sphinx-9658 split 0/1 across reps (near-miss)',
913
+ },
914
+ ]
915
+
916
+ /**
917
+ * The gen-4 config: protocol round 4 continues (frozen arm, same holdout
918
+ * registry, same roundsDir staircase) with three changes as a unit:
919
+ *
920
+ * 1. PINNED PER-PROPOSER MODELS — four seats: claude-author (claude CLI on
921
+ * its own login; the resolved model + CLI version are captured into
922
+ * `<outDir>/proposer-provenance.json` at t=0), glm-author (opencode
923
+ * pinned to zai-coding-plan/glm-5.2 via `-m`), codex-author (codex CLI on
924
+ * its ChatGPT login, auth provenance-gated at launch; drop the seat via
925
+ * `includeCodex: false` when the CLI is absent), and merge-author (claude,
926
+ * merge seat).
927
+ * 2. PARETO PARENTS — the gen-3 winner + runner-up diffs and their measured
928
+ * per-instance results seed every author's prompt; the merge seat's task
929
+ * is their coherent union. Seeded at the buildPrompt seam (our seam): the
930
+ * lib's `ctx.paretoParents` frontier is within-run only, and a prior
931
+ * campaign cannot cross runs without its runDir + ledger receipts.
932
+ * 3. PINNED BASELINE — the premeasured artifact at `hh/gen4/` is BUILT from
933
+ * gen-3's measured baseline cells (premeasured-from-cells.mts; gen-3
934
+ * measured astropy F, django F, matplotlib F, xarray F, pytest F,
935
+ * sphinx T — matplotlib/django false under current weather), so gen-4
936
+ * spends nothing re-measuring and fails loud if the loops tip moved.
937
+ */
938
+ export function defaultGen4Config(
939
+ hh = DEFAULT_HH_SCRATCHPAD,
940
+ opts: { outDirName?: string; includeCodex?: boolean } = {},
941
+ ): OuterLoopConfig {
942
+ const base = defaultGen3Config(hh, { outDirName: opts.outDirName ?? 'gen4' })
943
+ const proposers: ProposerSpec[] = [
944
+ { name: 'claude-author', profile: 'default-author.profile.json', harness: 'claude' },
945
+ { name: 'glm-author', harness: 'opencode', model: 'zai-coding-plan/glm-5.2' },
946
+ ...(opts.includeCodex === false ? [] : [{ name: 'codex-author', harness: 'codex' } satisfies ProposerSpec]),
947
+ { name: 'merge-author', profile: 'default-author.profile.json', harness: 'claude', merge: true },
948
+ ]
949
+ return {
950
+ ...base,
951
+ populationSize: proposers.length,
952
+ proposers,
953
+ paretoParents: [...GEN3_PARETO_PARENTS],
954
+ }
955
+ }
956
+
957
+ // ---------------------------------------------------------------------------
958
+ // GEN-5 configuration — gen-4's shape (4 proposers incl. the merge seat,
959
+ // Pareto parents, premeasured baseline carried forward per the same
960
+ // cell-derivation, 2 reps, deferred holdout) PLUS the gen-5 integration
961
+ // bundle as a unit:
962
+ //
963
+ // 1. MAP+TOOLBOX briefing — per-run evidence index + toolbox/permission
964
+ // briefing (change-space overridable at extensions/pi/author-briefing.md);
965
+ // the 3-analyst diagnosis stays as ONE input among the named tools.
966
+ // 2. PUBLIC/PRIVATE SPLIT — 4 public / 2 private of the 6 instances,
967
+ // deterministically seeded by runId and persisted per outDir; proposers +
968
+ // prefilter see public only, selection stays combined. Small-n caveat
969
+ // documented in score-split.mts.
970
+ // 3. ACTIVATION GATE — required machine-checkable predicate per candidate;
971
+ // never-fired mechanisms are quarantined even on an improved score.
972
+ // 4. SETTLE-TIME ROLLOUT LEDGER — tangle.rollout.v1 lines live per cell,
973
+ // label v2 (contribution-aware workers, baseline-relative proposers).
974
+ // ---------------------------------------------------------------------------
975
+
976
+ export function defaultGen5Config(
977
+ hh = DEFAULT_HH_SCRATCHPAD,
978
+ opts: { outDirName?: string; includeCodex?: boolean } = {},
979
+ ): OuterLoopConfig {
980
+ const base = defaultGen4Config(hh, {
981
+ outDirName: opts.outDirName ?? 'gen5',
982
+ ...(opts.includeCodex !== undefined ? { includeCodex: opts.includeCodex } : {}),
983
+ })
984
+ return {
985
+ ...base,
986
+ scoreSplit: { publicCount: 4 },
987
+ briefing: AUTHOR_BRIEFING_VERSION,
988
+ activationGate: true,
989
+ rolloutLedger: { enabled: true },
990
+ priorEvidenceDirs: [join(hh, 'gen4'), join(hh, 'gen3')],
991
+ }
992
+ }
993
+
807
994
  // ---------------------------------------------------------------------------
808
995
  // Round recorder — dispatch-time change-space fail-closed + diff writing ONLY.
809
996
  // NOT a scoring source: scoring reads the lib's campaign cells
@@ -1059,11 +1246,21 @@ export function loopsCandidateVerifier(loopsRepo: string): Verifier {
1059
1246
  * only — the rest of the run keeps its env untouched. */
1060
1247
  const CLAUDE_AMBIENT_AUTH_VARS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL'] as const
1061
1248
 
1249
+ /** Same failure class for the gen-4 codex seat: agent-state.env injects an
1250
+ * OPENAI_API_KEY meant for other tooling, and the codex CLI prefers env-key
1251
+ * auth over its ChatGPT login. The codex author shot must run on the CLI's
1252
+ * own login (`codex login status` is provenance-gated at launch), so the
1253
+ * leaked auth is stripped for the shot subprocess only. */
1254
+ const CODEX_AMBIENT_AUTH_VARS = ['OPENAI_API_KEY', 'OPENAI_BASE_URL'] as const
1255
+
1062
1256
  export function proposerShotEnv(harness: OuterLoopConfig['proposerHarness']): NodeJS.ProcessEnv {
1063
1257
  const env: NodeJS.ProcessEnv = { ...process.env }
1064
1258
  if (harness === 'claude') {
1065
1259
  for (const name of CLAUDE_AMBIENT_AUTH_VARS) delete env[name]
1066
1260
  }
1261
+ if (harness === 'codex') {
1262
+ for (const name of CODEX_AMBIENT_AUTH_VARS) delete env[name]
1263
+ }
1067
1264
  return env
1068
1265
  }
1069
1266
 
@@ -1151,6 +1348,33 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1151
1348
  // premeasured baseline and pin the depth dial (see capabilities.mts).
1152
1349
  assertSubstratePassthroughs(log)
1153
1350
 
1351
+ // GEN-4 model-identity provenance at t=0: harness CLI versions, the claude
1352
+ // seat's resolved settings model, codex auth, and every explicit model pin.
1353
+ // Fails loud on a missing/unauthed harness binary — populationSize equals
1354
+ // proposers.length, so a dead seat cannot be skipped mid-run.
1355
+ if (config.proposers !== undefined) {
1356
+ const provenance = await captureProposerProvenance(config.proposers)
1357
+ await mkdir(config.outDir, { recursive: true })
1358
+ await writeFile(join(config.outDir, 'proposer-provenance.json'), JSON.stringify(provenance, null, 2))
1359
+ for (const p of provenance.proposers) {
1360
+ log(
1361
+ `proposer ${p.name} (${p.harness}${p.merge ? ', merge seat' : ''}): ` +
1362
+ `model=${p.pinnedModel ?? `cli-default${p.settingsModel ? `:${p.settingsModel}` : ''}`} ` +
1363
+ `version=${p.harnessVersion.split('\n')[0]}`,
1364
+ )
1365
+ }
1366
+ }
1367
+
1368
+ // GEN-4 Pareto parents: materialize the configured prior-run frontier
1369
+ // (commit existence + full diffs) before any authoring.
1370
+ const paretoParents: ParetoParentContext[] =
1371
+ config.paretoParents !== undefined && config.paretoParents.length > 0
1372
+ ? await materializeParetoParents(config.loopsRepo, config.paretoParents)
1373
+ : []
1374
+ if (paretoParents.length > 0) {
1375
+ log(`pareto parents: ${paretoParents.map((p) => `${p.label}@${p.commit.slice(0, 10)}`).join(', ')}`)
1376
+ }
1377
+
1154
1378
  // The gate's only denominator: a stored prior baseline campaign the LIB
1155
1379
  // validates (surface hash, seed, reps, split digest, coverage) before
1156
1380
  // skipping the baseline campaign. A missing artifact = the BOOTSTRAP run —
@@ -1179,14 +1403,41 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1179
1403
  const excludes = await loadExcludes()
1180
1404
  const images = await loadInstanceImages(config.instanceImagesPath)
1181
1405
  const adapter = createSweBenchAdapter()
1406
+ const runId = `r${config.round}-${Date.now().toString(36)}`
1407
+ await mkdir(config.outDir, { recursive: true })
1408
+
1409
+ // GEN-5 public/private split — deterministic (seeded by runId), PERSISTED
1410
+ // per outDir so a resume can never rotate private instances into view.
1411
+ // Scored identically; selection stays combined; proposers + prefilter see
1412
+ // public only.
1413
+ const split: ScoreSplit | null =
1414
+ config.scoreSplit !== undefined
1415
+ ? await loadOrCreateScoreSplit({
1416
+ outDir: config.outDir,
1417
+ runId,
1418
+ instances: config.instances,
1419
+ publicCount: config.scoreSplit.publicCount,
1420
+ })
1421
+ : null
1422
+ const privateIids = new Set(split?.privateInstances ?? [])
1423
+ if (split !== null) {
1424
+ log(
1425
+ `score split (seeded by ${split.seededBy}): public [${split.publicInstances.join(', ')}] + ` +
1426
+ `${split.privateInstances.length} private instance(s) (identities withheld from proposers; ` +
1427
+ `selection uses public+private combined; small-n caveat: 2 private of 6 is a direction check, not certification)`,
1428
+ )
1429
+ }
1430
+
1182
1431
  // The pre-filter's smoke instance may sit outside the improvement set (e.g.
1183
1432
  // a designated cheap instance) — it needs the same problem/image/verify
1184
- // validation and rides the same loaded-task map.
1433
+ // validation and rides the same loaded-task map. Under the gen-5 split the
1434
+ // smoke choice is restricted to PUBLIC instances (the prefilter surfaces
1435
+ // its verdict to the kill log the authors can mine).
1185
1436
  const smokeIid =
1186
1437
  config.proposers !== undefined && config.prefilter?.enabled
1187
1438
  ? resolveSmokeInstance(
1188
1439
  config.prefilter.smokeInstance,
1189
- config.instances,
1440
+ split !== null ? split.publicInstances : config.instances,
1190
1441
  premeasured ? cellsFromCampaign(premeasured.campaign) : null,
1191
1442
  )
1192
1443
  : null
@@ -1208,12 +1459,57 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1208
1459
  const judge: SerializedJudge = createSerializedJudge(
1209
1460
  config.judgeTimeoutMs !== undefined ? { timeoutMs: config.judgeTimeoutMs } : {},
1210
1461
  )
1211
- const runId = `r${config.round}-${Date.now().toString(36)}`
1212
- await mkdir(config.outDir, { recursive: true })
1213
1462
  await mkdir(config.roundsDir, { recursive: true })
1214
1463
  const recorder = new RoundRecorder(config.loopsRepo, join(config.outDir, 'candidates'))
1215
1464
  const analysts: AnalystSpec[] = config.analystModels.map((model, i) => ({ id: `${model}#${i + 1}`, model }))
1216
1465
 
1466
+ // GEN-5 MAP+TOOLBOX briefing: persist the Pareto parent diffs, write the
1467
+ // per-run evidence index (a map — one line per evidence path, private
1468
+ // instances excluded), and resolve the briefing text (the change-space
1469
+ // override at extensions/pi/author-briefing.md wins over the default).
1470
+ let briefingCtx: BriefingContext | undefined
1471
+ if (config.briefing === AUTHOR_BRIEFING_VERSION) {
1472
+ const parentPatches: Array<{ label: string; path: string }> = []
1473
+ if (paretoParents.length > 0) {
1474
+ const parentsDir = join(config.outDir, 'pareto-parents')
1475
+ await mkdir(parentsDir, { recursive: true })
1476
+ for (const parent of paretoParents) {
1477
+ const patchPath = join(parentsDir, `${parent.label}.patch`)
1478
+ await writeFile(patchPath, parent.diff)
1479
+ parentPatches.push({ label: parent.label, path: patchPath })
1480
+ }
1481
+ }
1482
+ const index = await writeEvidenceIndex({
1483
+ outDir: config.outDir,
1484
+ roundsDir: config.roundsDir,
1485
+ seedArtifactRuns: config.seedArtifactRuns,
1486
+ ...(config.priorEvidenceDirs !== undefined ? { priorEvidenceDirs: config.priorEvidenceDirs } : {}),
1487
+ paretoParentPatches: parentPatches,
1488
+ split,
1489
+ })
1490
+ const briefing = await resolveAuthorBriefing(config.loopsRepo, config.loopsBaseRef)
1491
+ briefingCtx = { indexPath: index.path, briefingText: briefing.text, briefingSource: briefing.source }
1492
+ log(
1493
+ `briefing ${AUTHOR_BRIEFING_VERSION}: evidence index → ${index.path} (${index.rows.length} row(s)); ` +
1494
+ `briefing text source: ${briefing.source}`,
1495
+ )
1496
+ }
1497
+
1498
+ // GEN-5 settle-time rollout ledger — tangle.rollout.v1 lines appended live
1499
+ // after each cell judges (label v2); capture failure logs loud but never
1500
+ // kills a cell.
1501
+ const settleCapture: SettleCapture | null =
1502
+ config.rolloutLedger?.enabled === true
1503
+ ? createSettleCapture({
1504
+ ledgerPath: config.rolloutLedger.path ?? join(config.outDir, 'rollout-ledger.jsonl'),
1505
+ runId,
1506
+ instanceCount: config.instances.length,
1507
+ ...(config.rolloutLedger.opencodeDb !== undefined ? { opencodeDb: config.rolloutLedger.opencodeDb } : {}),
1508
+ log,
1509
+ })
1510
+ : null
1511
+ if (settleCapture !== null) log(`rollout-ledger: settle-time capture ON → ${settleCapture.path}`)
1512
+
1217
1513
  const sweScenarios: Scenario[] = config.instances.map((iid) => ({ id: iid, kind: 'swe-instance' }))
1218
1514
 
1219
1515
  // Capacity gates on BOTH paths the supervisor arm rides (worker + router).
@@ -1238,11 +1534,24 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1238
1534
  const smokeRunner: SmokeRunner | undefined =
1239
1535
  smokeIid === null
1240
1536
  ? undefined
1241
- : async ({ scratchPath, generation, proposer, costLedger }): Promise<SmokeVerdict> => {
1537
+ : async ({
1538
+ scratchPath,
1539
+ generation,
1540
+ proposer,
1541
+ evaluationKey,
1542
+ costLedger,
1543
+ }): Promise<SmokeVerdict> => {
1242
1544
  const iid = smokeIid
1545
+ if (!/^[a-zA-Z0-9_-]+$/.test(evaluationKey)) {
1546
+ throw new Error(`outer-loop: invalid smoke evaluation key ${JSON.stringify(evaluationKey)}`)
1547
+ }
1243
1548
  const requireResolved = config.prefilter?.requireResolved === true
1244
1549
  const entry = images[iid]!
1245
- const armOutDir = join(config.outDir, 'prefilter-smoke', `gen${generation}-${proposer.name}`)
1550
+ const armOutDir = join(
1551
+ config.outDir,
1552
+ 'prefilter-smoke',
1553
+ `gen${generation}-${proposer.name}-${evaluationKey}`,
1554
+ )
1246
1555
  const nm = join(scratchPath, 'node_modules')
1247
1556
  let linked = false
1248
1557
  const t0 = Date.now()
@@ -1301,7 +1610,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1301
1610
  const paid = await costLedger.runPaidCall({
1302
1611
  channel: 'agent',
1303
1612
  phase: 'search.prefilter',
1304
- actor: `prefilter-smoke:${iid}:g${generation}:${proposer.name}`,
1613
+ actor: `prefilter-smoke:${iid}:g${generation}:${proposer.name}:${evaluationKey}`,
1305
1614
  model: config.arm.workerModel,
1306
1615
  execute: runWork,
1307
1616
  receipt: ({ armRes }) => {
@@ -1337,6 +1646,8 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1337
1646
  resolved: outcome.resolved,
1338
1647
  patchLines: outcome.armRes.patch_lines,
1339
1648
  wallS,
1649
+ // The GEPA seat's inner-score tiebreak.
1650
+ verifyPass: outcome.armRes.verify_pass,
1340
1651
  }
1341
1652
  await mkdir(armOutDir, { recursive: true })
1342
1653
  await writeFile(join(armOutDir, 'smoke.json'), JSON.stringify(result, null, 2))
@@ -1360,6 +1671,79 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1360
1671
  }
1361
1672
  }
1362
1673
 
1674
+ let runnerImplementationRef: string | undefined
1675
+ let judgeImplementationRef: string | undefined
1676
+ const hasGepaSeat = config.proposers?.some((proposer) => proposer.engine !== undefined) === true
1677
+ if (hasGepaSeat && smokeIid !== null && smokeRunner !== undefined) {
1678
+ const runtimeRoot = fileURLToPath(new URL('../../..', import.meta.url))
1679
+ const benchRoot = fileURLToPath(new URL('../..', import.meta.url))
1680
+ const sourceCommit = (
1681
+ await runOk('git', ['-C', config.loopsRepo, 'rev-parse', 'HEAD'])
1682
+ ).stdout.trim()
1683
+ const entry = images[smokeIid]!
1684
+ const verifyScript = await readFile(join(config.verifyDir, `${smokeIid}.sh`), 'utf8')
1685
+ const implementationArtifacts = {
1686
+ benchSource: await fileTreeImplementationRef(join(benchRoot, 'src')),
1687
+ runtimeSource: await fileTreeImplementationRef(join(runtimeRoot, 'src')),
1688
+ runtimeDist: await fileTreeImplementationRef(join(runtimeRoot, 'dist')),
1689
+ packageFiles: canonicalCandidateDigest({
1690
+ runtimePackage: await readFile(join(runtimeRoot, 'package.json'), 'utf8'),
1691
+ benchPackage: await readFile(join(benchRoot, 'package.json'), 'utf8'),
1692
+ lockfile: await readFile(join(runtimeRoot, 'pnpm-lock.yaml'), 'utf8'),
1693
+ }),
1694
+ swebench: await pythonDistributionImplementationRef(
1695
+ 'swebench',
1696
+ (script, args) => runVenvPython(script, args),
1697
+ ),
1698
+ }
1699
+ const runnerSourceRef = canonicalCandidateDigest({
1700
+ benchSource: implementationArtifacts.benchSource,
1701
+ runtimeSource: implementationArtifacts.runtimeSource,
1702
+ runtimeDist: implementationArtifacts.runtimeDist,
1703
+ packageFiles: implementationArtifacts.packageFiles,
1704
+ })
1705
+ const judgeSourceRef = canonicalCandidateDigest({
1706
+ benchSource: implementationArtifacts.benchSource,
1707
+ packageFiles: implementationArtifacts.packageFiles,
1708
+ swebench: implementationArtifacts.swebench,
1709
+ })
1710
+ runnerImplementationRef = canonicalCandidateDigest({
1711
+ implementation: 'swe-arena-smoke-runner',
1712
+ sourceCommit,
1713
+ sourceRef: runnerSourceRef,
1714
+ smoke: {
1715
+ iid: smokeIid,
1716
+ image: entry.image,
1717
+ baseCommit: entry.base_commit,
1718
+ problemStatement: problemById.get(smokeIid)!,
1719
+ verifyScript,
1720
+ requireResolved: config.prefilter?.requireResolved === true,
1721
+ },
1722
+ arm: {
1723
+ name: config.armName,
1724
+ workerModel: config.arm.workerModel,
1725
+ driverModel: config.arm.driverModel,
1726
+ budget: config.arm.budget,
1727
+ maxSandboxes: config.arm.maxSandboxes,
1728
+ maxUsd: config.arm.maxUsd,
1729
+ maxDepth: config.arm.maxDepth,
1730
+ timeoutMs: config.arm.timeoutMs,
1731
+ envKnobs: config.arm.envKnobs ?? null,
1732
+ },
1733
+ dispatchTimeoutMs: config.dispatchTimeoutMs,
1734
+ capacityModel: config.capacityModel ?? null,
1735
+ gateWaitCeilingMs: config.gateWaitCeilingMs ?? null,
1736
+ excludes,
1737
+ })
1738
+ judgeImplementationRef = canonicalCandidateDigest({
1739
+ implementation: 'serialized-swebench-judge',
1740
+ sourceCommit,
1741
+ sourceRef: judgeSourceRef,
1742
+ timeoutMs: config.judgeTimeoutMs ?? JUDGE_TIMEOUT_FLOOR_MS,
1743
+ cacheLevel: 'instance',
1744
+ })
1745
+ }
1746
+
1363
1747
  // ── dispatch: one (surface × scenario) cell ──────────────────────────
1364
1748
  const agent = async (surface: MutableSurface, scenario: Scenario, ctx: DispatchContext): Promise<R4Artifact> => {
1365
1749
  const cs = asCodeSurface(surface)
@@ -1431,6 +1815,14 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1431
1815
  await writeFile(join(runDir, 'judge.json'), JSON.stringify({ ...verdict, wallS: judgeWallS }, null, 1))
1432
1816
  log(`${config.armName} ${rec.tag} ${iid} judged: resolved=${verdict.resolved} (attempts=${verdict.attempts})`)
1433
1817
 
1818
+ // Deterministic run observability, per cell: steer count, waves, concurrency,
1819
+ // idle, evidence→respawn, cost by role. The headline lands in the run log so
1820
+ // the answers are in the tail without a follow-up command.
1821
+ await writeSupervisorRunReportSafe(runDir, {
1822
+ appendHeadlineTo: join(config.outDir, 'run.log'),
1823
+ patchPath: armRes.patchPath,
1824
+ })
1825
+
1434
1826
  const spend = armRes.recoveredSpend
1435
1827
  const recovered =
1436
1828
  armRes.spentTokens === null && (spend?.workerTokSqlite ?? null) === null
@@ -1454,7 +1846,61 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1454
1846
  recoveredTokens: recovered,
1455
1847
  }) + '\n',
1456
1848
  )
1457
- await ctx.artifacts.writeJson('arm-summary.json', { runDir, patchPath: armRes.patchPath, verdict })
1849
+ const summaryPath = await ctx.artifacts.writeJson('arm-summary.json', {
1850
+ runDir,
1851
+ patchPath: armRes.patchPath,
1852
+ verdict,
1853
+ })
1854
+
1855
+ // GEN-5 settle-time rollout capture: emit supervisor + worker lines
1856
+ // NOW, while the opencode store still holds the worker transcripts.
1857
+ // Attribution comes from the campaign cell path (never dispatch
1858
+ // order); a capture failure logs loud but never kills the cell.
1859
+ if (settleCapture !== null) {
1860
+ try {
1861
+ const coords = campaignCoordsFromCellPath(summaryPath)
1862
+ if (coords === null) {
1863
+ log(`rollout-ledger: cannot derive campaign coords from ${summaryPath} — cell ${iid} r${ctx.rep} skipped`)
1864
+ } else {
1865
+ const supRunDir = await findSupervisorRunDir(armRes.ws)
1866
+ const deliveredPatch = await readFile(armRes.patchPath, 'utf8').catch(() => '')
1867
+ await settleCapture.captureCell({
1868
+ generation: coords.generation,
1869
+ candidateIndex: coords.candidateIndex,
1870
+ iid,
1871
+ rep: ctx.rep,
1872
+ seed: ctx.seed,
1873
+ splitVisibility: split === null ? null : privateIids.has(iid) ? 'private' : 'public',
1874
+ commit: cs.candidateCommit,
1875
+ resolved: verdict.resolved,
1876
+ judgeVerdict: { ...verdict, wallS: judgeWallS },
1877
+ runDir,
1878
+ patchPath: armRes.patchPath,
1879
+ supRunDir,
1880
+ deliveredPatch,
1881
+ workerModel: config.arm.workerModel,
1882
+ metrics: {
1883
+ resolved: verdict.resolved,
1884
+ verify_pass: armRes.verify_pass,
1885
+ patch_lines: armRes.patch_lines,
1886
+ judge_attempts: verdict.attempts ?? null,
1887
+ judge_wall_s: judgeWallS,
1888
+ spent_tokens: armRes.spentTokens,
1889
+ spent_usd: armRes.spentUsd,
1890
+ recovered_tokens: recovered,
1891
+ sup_status: armRes.sup_status,
1892
+ sup_verdict: armRes.sup_verdict,
1893
+ spawned: armRes.spawned,
1894
+ workers: armRes.workers,
1895
+ settled: armRes.settled,
1896
+ },
1897
+ cost: { usd: armRes.spentUsd, wallS: armRes.wall_s, spentTokens: armRes.spentTokens },
1898
+ })
1899
+ }
1900
+ } catch (cause) {
1901
+ log(`rollout-ledger: settle-time capture FAILED for ${iid} r${ctx.rep}: ${(cause as Error).message}`)
1902
+ }
1903
+ }
1458
1904
 
1459
1905
  if (verdict.resolved === null) {
1460
1906
  // Inconclusive judge (double flake / infra) — the cell must FAIL, not
@@ -1557,6 +2003,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1557
2003
  const runs: SupRunArtifacts[] = []
1558
2004
  if (input.generation === -1) {
1559
2005
  for (const seed of config.seedArtifactRuns) {
2006
+ if (privateIids.has(seed.iid)) continue // gen-5 split: never surfaced to proposers
1560
2007
  if (!existsSync(seed.dir)) {
1561
2008
  // A wiped scratchpad (host reboot) must not feed EMPTY bundles to the
1562
2009
  // analysts as if they were real artifacts — skip loudly.
@@ -1581,6 +2028,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1581
2028
  for (const cell of cells) {
1582
2029
  const a = cell.artifact
1583
2030
  if (a === null || a.kind !== 'swe-arm' || !a.runDir) continue
2031
+ if (privateIids.has(a.iid)) continue // gen-5 split: never surfaced to proposers
1584
2032
  runs.push({
1585
2033
  iid: a.iid,
1586
2034
  arm: config.armName,
@@ -1618,8 +2066,25 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1618
2066
  log(`diagnosis ensemble FAILED for gen ${input.generation}: ${(cause as Error).message}`)
1619
2067
  }
1620
2068
  }
2069
+ // GEN-5 split: the raw-trace distiller must not hand private-instance
2070
+ // cells' path context to the authors either — censor them out of the
2071
+ // candidates' campaigns before distillation.
2072
+ const censoredInput =
2073
+ split === null
2074
+ ? input
2075
+ : {
2076
+ ...input,
2077
+ candidates: input.candidates.map((cand) => {
2078
+ const campaign = cand.campaign as { cells?: Array<{ scenarioId: string }> } | null
2079
+ if (campaign === null || typeof campaign !== 'object' || !Array.isArray(campaign.cells)) return cand
2080
+ return {
2081
+ ...cand,
2082
+ campaign: { ...campaign, cells: campaign.cells.filter((c) => !privateIids.has(c.scenarioId)) },
2083
+ }
2084
+ }),
2085
+ }
1621
2086
  signal?.throwIfAborted()
1622
- const rawFindings = (await rawTrace(input as Parameters<typeof rawTrace>[0])) as unknown[]
2087
+ const rawFindings = (await rawTrace(censoredInput as Parameters<typeof rawTrace>[0])) as unknown[]
1623
2088
  signal?.throwIfAborted()
1624
2089
  return [steeringFinding, ...ensembleFindings, ...rawFindings]
1625
2090
  }
@@ -1638,22 +2103,47 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1638
2103
  'tsx src/swe-arena/holdout-certify.mts <config.json> --candidate <winner-loops-commit>'
1639
2104
  const improveRunDir = join(config.outDir, 'improve-run')
1640
2105
 
2106
+ // ── crash recovery: a killed run leaves its in-flight paid call 'pending'
2107
+ // in the durable cost ledger, and the ledger's fail-closed guard then
2108
+ // refuses ALL new paid work on resume. Under the outDir instance lock
2109
+ // (sole runner), every pending call restored from disk is provably from a
2110
+ // dead process — settle each as a $0 failure receipt (reason
2111
+ // 'process-crash-orphan') so the guard passes without erasing the crash
2112
+ // from the durable record. ───────────────────────────────────────────
2113
+ for (const receipt of reconcileCrashOrphansOnDisk(improveRunDir)) {
2114
+ log(
2115
+ `cost-ledger: reconciled crash-orphaned call '${receipt.callId}' ` +
2116
+ `(${receipt.actor}, ${receipt.phase}) as ${CRASH_ORPHAN_REASON}`,
2117
+ )
2118
+ }
2119
+
1641
2120
  // ── generator: the gen-3 proposer fan-out (parallel AgentProfile-pinned
1642
2121
  // authors + pre-filter) when `proposers` is configured; the legacy
1643
2122
  // single-author generator otherwise. ─────────────────────────────────
1644
2123
  const fanout =
1645
2124
  config.proposers !== undefined
1646
- ? fanOutLoopsGenerator(config, { ...(smokeRunner ? { smokeRunner } : {}), log })
2125
+ ? fanOutLoopsGenerator(config, {
2126
+ ...(smokeRunner ? { smokeRunner } : {}),
2127
+ ...(runnerImplementationRef ? { runnerImplementationRef } : {}),
2128
+ ...(judgeImplementationRef ? { judgeImplementationRef } : {}),
2129
+ ...(paretoParents.length > 0 ? { parents: paretoParents } : {}),
2130
+ ...(briefingCtx !== undefined ? { briefing: briefingCtx } : {}),
2131
+ // The GEPA seat's inner evaluator uses the same public-only
2132
+ // smoke instance; the split guards the never-surfaced invariant at
2133
+ // the bridge boundary too.
2134
+ ...(smokeIid !== null ? { smokeInstanceId: smokeIid } : {}),
2135
+ scoreSplit: split,
2136
+ log,
2137
+ })
1647
2138
  : null
1648
2139
  const generator = withParentCancellation(fanout ?? constrainedLoopsGenerator(config), signal)
1649
2140
 
1650
2141
  // ── the improve() call: the optimizer seat ───────────────────────────
1651
2142
  // Typed from improve()'s own parameter: the monorepo hoists two
1652
2143
  // agent-interface majors, so a nominal import can resolve to the wrong one.
1653
- const profile = { name: 'loops-pi-supervisor' } as Parameters<typeof improve>[0]
1654
2144
  signal?.throwIfAborted()
1655
2145
  log(`round ${config.round} runId=${runId}: improve(surface:'code') over ${config.loopsRepo}@${config.loopsBaseRef}`)
1656
- const result = await improve<Scenario, R4Artifact>(profile, [], {
2146
+ const result = await improve<Scenario, R4Artifact>({
1657
2147
  surface: 'code',
1658
2148
  // analyzeGeneration wins over this flag; the composite above embeds
1659
2149
  // rawTraceDistiller directly so the raw-trace mechanism stays active.
@@ -1737,10 +2227,23 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1737
2227
  }
1738
2228
  }
1739
2229
 
2230
+ // Collected per-candidate facts for activation and proposer rewards.
2231
+ const activationBySurface = new Map<string, ActivationRecord>()
2232
+ interface ProposerOutcomeFact {
2233
+ generation: number
2234
+ candidateIndex: number
2235
+ label: string
2236
+ commit: string | null
2237
+ resolvedCount: number
2238
+ diffPath: string | null
2239
+ }
2240
+ const proposerFacts: ProposerOutcomeFact[] = []
2241
+
1740
2242
  for (let g = 0; g < loop.generations.length; g++) {
1741
2243
  const gen = loop.generations[g]!
1742
2244
  const rows: StaircaseRow[] = []
1743
- for (const cand of gen.record.candidates) {
2245
+ for (let candIndex = 0; candIndex < gen.record.candidates.length; candIndex++) {
2246
+ const cand = gen.record.candidates[candIndex]!
1744
2247
  const surface = gen.surfaces.find((s) => s.surfaceHash === cand.surfaceHash)?.surface
1745
2248
  const cs = surface && typeof surface === 'object' && surface.kind === 'code' ? surface : null
1746
2249
  const desc = cs ? await recorder.ensure(cs) : undefined
@@ -1764,6 +2267,71 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1764
2267
  const parentResolvedCount =
1765
2268
  parentCampaign !== undefined ? resolvedCountOf(parentCampaign) : measuredBaselineCount
1766
2269
  const violations = desc?.violations ?? []
2270
+
2271
+ // GEN-5 activation gate: run the candidate's own committed predicate
2272
+ // over its own cell run dirs. Fail-closed — a missing/unparseable
2273
+ // predicate (the prefilter should have killed it) quarantines.
2274
+ let activation: ActivationRecord | undefined
2275
+ if (config.activationGate === true && cs !== null) {
2276
+ const committed = await readCommittedPredicate(config.loopsRepo, cs.candidateCommit)
2277
+ if (committed === null || !committed.parsed.ok) {
2278
+ const why =
2279
+ committed === null
2280
+ ? `no ${ACTIVATION_PREDICATE_RELPATH} at ${cs.candidateCommit.slice(0, 10)}`
2281
+ : `unparseable activation predicate: ${committed.parsed.ok ? '' : committed.parsed.error}`
2282
+ activation = {
2283
+ present: false,
2284
+ description: null,
2285
+ fired: false,
2286
+ evidence: [],
2287
+ warnings: [`${why} — fail-closed quarantine`],
2288
+ }
2289
+ } else {
2290
+ const runDirs = [
2291
+ ...new Set(
2292
+ cells
2293
+ .map((c) => (c.artifact !== null && c.artifact.kind === 'swe-arm' ? c.artifact.runDir : null))
2294
+ .filter((d): d is string => typeof d === 'string' && d.length > 0),
2295
+ ),
2296
+ ]
2297
+ const res = await runActivationPredicate(committed.parsed.predicate, runDirs)
2298
+ activation = {
2299
+ present: true,
2300
+ description: committed.parsed.predicate.description,
2301
+ fired: res.fired,
2302
+ evidence: res.evidence,
2303
+ warnings: res.warnings,
2304
+ }
2305
+ }
2306
+ activationBySurface.set(cand.surfaceHash, activation)
2307
+ log(
2308
+ `activation ${cand.label ?? cand.surfaceHash.slice(0, 10)}: present=${activation.present} ` +
2309
+ `fired=${activation.fired}${activation.fired ? ` — ${activation.evidence[0] ?? ''}` : ''}` +
2310
+ `${activation.warnings.length > 0 ? ` (warnings: ${activation.warnings.join('; ')})` : ''}`,
2311
+ )
2312
+ }
2313
+
2314
+ // GEN-5 split sub-scores: both halves logged per candidate; the
2315
+ // selection rule stays combined (candResolved over ALL instances).
2316
+ const verdicts = instanceVerdictsFromCells(cells, config.instances, reps)
2317
+ const splitScores = split !== null ? subScores(verdicts, split) : null
2318
+ if (split !== null && splitScores !== null) {
2319
+ log(
2320
+ `split scores ${cand.label ?? cand.surfaceHash.slice(0, 10)}: ` +
2321
+ `public ${splitScores.publicResolvedCount}/${split.publicInstances.length}, ` +
2322
+ `private ${splitScores.privateResolvedCount}/${split.privateInstances.length} (combined ${candResolved}/${config.instances.length})`,
2323
+ )
2324
+ }
2325
+
2326
+ const verdict = decideVerdict({
2327
+ violations,
2328
+ coverageComplete,
2329
+ resolvedCount: candResolved,
2330
+ parentResolvedCount,
2331
+ costRatio,
2332
+ costGuardRatio: config.costGuardRatio,
2333
+ ...(activation !== undefined ? { activationFired: activation.fired } : {}),
2334
+ })
1767
2335
  rows.push({
1768
2336
  schema: STAIRCASE_SCHEMA,
1769
2337
  round: config.round,
@@ -1786,18 +2354,31 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1786
2354
  costRatio,
1787
2355
  costGuardRatio: config.costGuardRatio,
1788
2356
  internallyPromoted: gen.record.promoted.includes(cand.surfaceHash),
1789
- verdict: decideVerdict({
1790
- violations,
1791
- coverageComplete,
1792
- resolvedCount: candResolved,
1793
- parentResolvedCount,
1794
- costRatio,
1795
- costGuardRatio: config.costGuardRatio,
1796
- }),
2357
+ verdict,
1797
2358
  holdout: 'operator-approval-required',
1798
2359
  armProvenance: desc?.armProvenance ?? null,
1799
2360
  diffPath: desc?.diffPath ?? null,
1800
2361
  diffSha256: desc?.diffSha256 ?? null,
2362
+ ...(split !== null && splitScores !== null
2363
+ ? {
2364
+ split: {
2365
+ publicInstances: split.publicInstances,
2366
+ privateInstances: split.privateInstances,
2367
+ ...splitScores,
2368
+ },
2369
+ }
2370
+ : {}),
2371
+ ...(activation !== undefined ? { activation } : {}),
2372
+ })
2373
+
2374
+ const label = cand.label ?? cs?.candidateCommit?.slice(0, 10) ?? cand.surfaceHash.slice(0, 10)
2375
+ proposerFacts.push({
2376
+ generation: g,
2377
+ candidateIndex: candIndex,
2378
+ label,
2379
+ commit: cs?.candidateCommit ?? null,
2380
+ resolvedCount: candResolved,
2381
+ diffPath: desc?.diffPath ?? null,
1801
2382
  })
1802
2383
  }
1803
2384
  const genFile = join(config.roundsDir, `gen-${g}.jsonl`)
@@ -1823,7 +2404,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1823
2404
  parent: premeasured?.surfaceHash ?? 'baseline',
1824
2405
  parentResolvedCount: measuredBaselineCount,
1825
2406
  label: kill.proposer,
1826
- rationale: `prefilter kill at stage '${kill.stage}' (${kill.harness})`,
2407
+ rationale: `prefilter kill at stage '${kill.stage}' (${kill.harness ?? 'engine'})`,
1827
2408
  changedFiles: [],
1828
2409
  changeSpaceViolations: kill.stage === 'change-space' ? [kill.reason] : [],
1829
2410
  perInstance: [],
@@ -1847,6 +2428,38 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1847
2428
  }
1848
2429
  }
1849
2430
 
2431
+ // GEN-5 label-v2 proposer rewards: baseline-relative (candidate − baseline
2432
+ // resolved fraction, improvement positive), one settle-time ledger line
2433
+ // per evaluated candidate now that the round's scores are final.
2434
+ if (settleCapture !== null) {
2435
+ const sanitizeName = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_')
2436
+ for (const fact of proposerFacts) {
2437
+ try {
2438
+ const flatDir = join(config.outDir, 'proposer-shots')
2439
+ const pattern = new RegExp(`^gen${fact.generation}-cand${fact.candidateIndex}-shot\\d+\\.json$`)
2440
+ const receiptPaths: string[] = []
2441
+ for (const dir of [flatDir, join(flatDir, sanitizeName(fact.label))]) {
2442
+ for (const name of (await readdir(dir).catch(() => [])).sort()) {
2443
+ if (pattern.test(name)) receiptPaths.push(join(dir, name))
2444
+ }
2445
+ }
2446
+ await settleCapture.captureProposer({
2447
+ generation: fact.generation,
2448
+ candidateIndex: fact.candidateIndex,
2449
+ proposer: fact.label,
2450
+ harness: config.proposers?.find((p) => p.name === fact.label)?.harness ?? null,
2451
+ commit: fact.commit,
2452
+ candResolved: fact.resolvedCount,
2453
+ baselineResolved: measuredBaselineCount,
2454
+ shotReceiptPaths: receiptPaths,
2455
+ diffPath: fact.diffPath,
2456
+ })
2457
+ } catch (cause) {
2458
+ log(`rollout-ledger: proposer capture FAILED for ${fact.label}: ${(cause as Error).message}`)
2459
+ }
2460
+ }
2461
+ }
2462
+
1850
2463
  const winnerSurface = result.raw.winner.surface
1851
2464
  const winnerCs =
1852
2465
  typeof winnerSurface === 'object' && winnerSurface !== null && winnerSurface.kind === 'code'
@@ -1884,6 +2497,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1884
2497
  // is worth approving.
1885
2498
  const winnerHash = winnerCs ? surfaceHash(winnerCs) : null
1886
2499
  const winnerCampaign = winnerHash !== null ? campaignBySurface.get(winnerHash) : undefined
2500
+ const winnerActivation = winnerHash !== null ? activationBySurface.get(winnerHash) : undefined
1887
2501
  const improvementSet =
1888
2502
  winnerCampaign !== undefined && winnerRec !== undefined
1889
2503
  ? gateEvidenceFromCells({
@@ -1893,6 +2507,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1893
2507
  iids: config.instances,
1894
2508
  reps,
1895
2509
  costGuardRatio: config.costGuardRatio,
2510
+ ...(winnerActivation !== undefined ? { activationFired: winnerActivation.fired } : {}),
1896
2511
  })
1897
2512
  : null
1898
2513
  const wouldKeep = improvementSet !== null && improvementSet.verdict === 'accepted'
@@ -1938,6 +2553,38 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1938
2553
  gateReasons: loop.gateResult.reasons,
1939
2554
  // Improvement-set (search-split) evidence — NOT a held-out measurement.
1940
2555
  improvementSet: improvementSet === null ? null : { ...improvementSet, wouldKeep },
2556
+ // GEN-5: the public/private split (sub-scores live per candidate in the
2557
+ // staircase rows; selection stays combined; private never surfaced to
2558
+ // proposers — the 2-of-6 private half is a direction check, not a
2559
+ // certification).
2560
+ scoreSplit:
2561
+ split === null
2562
+ ? null
2563
+ : {
2564
+ seededBy: split.seededBy,
2565
+ publicInstances: split.publicInstances,
2566
+ privateInstances: split.privateInstances,
2567
+ },
2568
+ // GEN-5: activation-gate outcomes per candidate surface.
2569
+ activationGate:
2570
+ config.activationGate === true
2571
+ ? {
2572
+ enabled: true,
2573
+ byCandidate: [...activationBySurface.entries()].map(([surface, a]) => ({
2574
+ surface,
2575
+ present: a.present,
2576
+ fired: a.fired,
2577
+ description: a.description,
2578
+ })),
2579
+ }
2580
+ : { enabled: false },
2581
+ // GEN-5: MAP+TOOLBOX briefing provenance.
2582
+ briefing:
2583
+ briefingCtx === undefined
2584
+ ? null
2585
+ : { version: AUTHOR_BRIEFING_VERSION, indexPath: briefingCtx.indexPath, textSource: briefingCtx.briefingSource },
2586
+ // GEN-5: settle-time rollout ledger location (tangle.rollout.v1, label v2).
2587
+ rolloutLedger: settleCapture === null ? null : { path: settleCapture.path, capture: 'settle-time', labels: 'v2' },
1941
2588
  // Honest run-wide spend from the lib's CostLedger: per-channel rollups
1942
2589
  // (agent = arm cells, judge = official-judge calls, driver = proposer
1943
2590
  // shots), token totals, and accounting-completeness flags.
@@ -1969,6 +2616,18 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1969
2616
  const summaryPath = join(config.roundsDir, `round${config.round}-summary-${runId}.json`)
1970
2617
  await writeFile(summaryPath, JSON.stringify(summary, null, 2))
1971
2618
  log(`round summary → ${summaryPath}`)
2619
+
2620
+ // Round rollup at gate time: every cell's orchestration/economics in one table,
2621
+ // written next to the round summary and echoed into the run log.
2622
+ await reportSupervisorRound(join(config.outDir, 'arm-runs'), {
2623
+ appendHeadlineTo: join(config.outDir, 'run.log'),
2624
+ reportDir: config.roundsDir,
2625
+ title: `Round ${config.round} rollup — ${runId}`,
2626
+ echo: true,
2627
+ }).catch((err: unknown) => {
2628
+ log(`round rollup failed: ${err instanceof Error ? err.message : String(err)}`)
2629
+ })
2630
+
1972
2631
  log(`gate: ${result.decision} — ${loop.gateResult.reasons[0] ?? ''}`)
1973
2632
  } finally {
1974
2633
  await result.dispose()
@@ -2087,17 +2746,35 @@ if (isMain) {
2087
2746
  if (argv[0] === '--write-config') {
2088
2747
  const path = argv[1]
2089
2748
  if (!path || path.startsWith('--')) {
2090
- console.error('usage: outer-loop.mts --write-config <path> [--out-name <dirname>] [--gen3]')
2749
+ console.error('usage: outer-loop.mts --write-config <path> [--out-name <dirname>] [--gen3|--gen4|--gen5]')
2091
2750
  process.exit(2)
2092
2751
  }
2093
2752
  const outDirName = flag('--out-name')
2094
2753
  const gen3 = argv.includes('--gen3')
2095
- const make = gen3 ? defaultGen3Config : defaultRound4Config
2096
- await writeFile(
2097
- path,
2098
- JSON.stringify(make(undefined, outDirName ? { outDirName } : {}), null, 2) + '\n',
2099
- )
2100
- console.log(`default ${gen3 ? 'gen-3' : 'round-4'} config ${path}`)
2754
+ const gen4 = argv.includes('--gen4')
2755
+ const gen5 = argv.includes('--gen5')
2756
+ let config: OuterLoopConfig
2757
+ let flavor: string
2758
+ if (gen4 || gen5) {
2759
+ // The codex seat rides only when the CLI is actually present — a config
2760
+ // naming a missing harness would fail the whole launch at t=0.
2761
+ const codexProbe = await run('codex', ['--version'])
2762
+ const includeCodex = codexProbe.code === 0
2763
+ if (!includeCodex) {
2764
+ console.log(
2765
+ `codex CLI unavailable (rc=${codexProbe.code}) — ${gen5 ? 'gen-5' : 'gen-4'} config written WITHOUT the codex-author seat`,
2766
+ )
2767
+ }
2768
+ const make = gen5 ? defaultGen5Config : defaultGen4Config
2769
+ config = make(undefined, { ...(outDirName ? { outDirName } : {}), includeCodex })
2770
+ flavor = gen5 ? 'gen-5' : 'gen-4'
2771
+ } else {
2772
+ const make = gen3 ? defaultGen3Config : defaultRound4Config
2773
+ config = make(undefined, outDirName ? { outDirName } : {})
2774
+ flavor = gen3 ? 'gen-3' : 'round-4'
2775
+ }
2776
+ await writeFile(path, JSON.stringify(config, null, 2) + '\n')
2777
+ console.log(`default ${flavor} config → ${path}`)
2101
2778
  } else if (argv[0] === '--calibration-smoke') {
2102
2779
  const dir = argv[1] && !argv[1].startsWith('--') ? argv[1] : undefined
2103
2780
  const n = flag('--analysts')
@@ -2130,7 +2807,7 @@ if (isMain) {
2130
2807
  } else {
2131
2808
  console.error(
2132
2809
  'usage: tsx src/swe-arena/outer-loop.mts <config.json> # SPENDS: arms + judges + proposer\n' +
2133
- ' tsx src/swe-arena/outer-loop.mts --write-config <path> [--out-name <dirname>] [--gen3]\n' +
2810
+ ' tsx src/swe-arena/outer-loop.mts --write-config <path> [--out-name <dirname>] [--gen3|--gen4|--gen5]\n' +
2134
2811
  ' tsx src/swe-arena/outer-loop.mts --calibration-smoke [supRunDir] [--analysts N] [--model M] [--endpoint router|zai] [--retries N]',
2135
2812
  )
2136
2813
  process.exit(2)