@tangle-network/agent-bench 0.3.7 → 0.3.8

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 (191) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/HARNESS.md +43 -0
  3. package/dist/adapters.js +23 -23
  4. package/dist/benchmarks/_harness.d.ts +1 -1
  5. package/dist/benchmarks/_harness.js +1 -1
  6. package/dist/benchmarks/aec-bench.js +2 -2
  7. package/dist/benchmarks/agentbench.js +2 -2
  8. package/dist/benchmarks/appworld.js +2 -2
  9. package/dist/benchmarks/bfcl.js +2 -2
  10. package/dist/benchmarks/commit0.js +2 -2
  11. package/dist/benchmarks/crag.js +2 -2
  12. package/dist/benchmarks/dabstep.js +2 -2
  13. package/dist/benchmarks/enterpriseops-gym.js +2 -2
  14. package/dist/benchmarks/finresearchbench.js +2 -2
  15. package/dist/benchmarks/nomiracl.js +2 -2
  16. package/dist/benchmarks/open-rag-bench.js +2 -2
  17. package/dist/benchmarks/programbench.js +2 -2
  18. package/dist/benchmarks/ragbench.js +2 -2
  19. package/dist/benchmarks/swe-bench.js +2 -2
  20. package/dist/benchmarks/t2-ragbench.js +2 -2
  21. package/dist/benchmarks/tau-bench-shared.js +2 -2
  22. package/dist/benchmarks/tau2-bench.js +3 -3
  23. package/dist/benchmarks/tau3-banking.js +3 -3
  24. package/dist/benchmarks/terminal-bench.js +2 -2
  25. package/dist/benchmarks/toollm.js +2 -2
  26. package/dist/benchmarks/webarena-verified.js +2 -2
  27. package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
  28. package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
  29. package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
  30. package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
  31. package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
  32. package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
  33. package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
  34. package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
  35. package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
  36. package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
  37. package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
  38. package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
  39. package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
  40. package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
  41. package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
  42. package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
  43. package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
  44. package/dist/{chunk-PWQVGAJB.js → chunk-V7AEBY6U.js} +21 -21
  45. package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
  46. package/dist/chunk-WSKWVEQB.js.map +1 -0
  47. package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
  48. package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
  49. package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
  50. package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
  51. package/dist/index.js +23 -23
  52. package/package.json +4 -4
  53. package/scripts/verify-packed-consumer.mjs +1 -1
  54. package/src/benchmarks/_harness.ts +20 -2
  55. package/src/benchmarks/humaneval.test.mts +2 -2
  56. package/src/quant-arena/README.md +144 -0
  57. package/src/quant-arena/backtest.test.mts +135 -0
  58. package/src/quant-arena/backtest.ts +218 -0
  59. package/src/quant-arena/data.test.mts +44 -0
  60. package/src/quant-arena/data.ts +141 -0
  61. package/src/quant-arena/driver.test.mts +253 -0
  62. package/src/quant-arena/driver.ts +219 -0
  63. package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
  64. package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
  65. package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
  66. package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
  67. package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
  68. package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
  69. package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
  70. package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
  71. package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
  72. package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
  73. package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
  74. package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
  75. package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
  76. package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
  77. package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
  78. package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
  79. package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
  80. package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
  81. package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
  82. package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
  83. package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
  84. package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
  85. package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
  86. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
  87. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
  88. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
  89. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
  90. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
  91. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
  92. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
  93. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
  94. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
  95. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
  96. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
  97. package/src/quant-arena/holdout-certify.mts +206 -0
  98. package/src/quant-arena/holdout-certify.test.mts +82 -0
  99. package/src/quant-arena/leak-audit.test.mts +79 -0
  100. package/src/quant-arena/leak-audit.ts +95 -0
  101. package/src/quant-arena/make-fixtures.mts +161 -0
  102. package/src/quant-arena/multiplicity.test.mts +68 -0
  103. package/src/quant-arena/multiplicity.ts +87 -0
  104. package/src/quant-arena/nautilus-certify.ts +31 -0
  105. package/src/quant-arena/oms.ts +90 -0
  106. package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
  107. package/src/quant-arena/python/pyproject.toml +8 -0
  108. package/src/quant-arena/python/uv.lock +1297 -0
  109. package/src/quant-arena/python/vbt-worker.py +192 -0
  110. package/src/quant-arena/quant-loop.mts +813 -0
  111. package/src/quant-arena/quant-loop.test.mts +75 -0
  112. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
  113. package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
  114. package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
  115. package/src/quant-arena/types.ts +133 -0
  116. package/src/quant-arena/vbt-client.ts +321 -0
  117. package/src/quant-arena/vbt-parity.test.mts +183 -0
  118. package/src/quant-arena/windows.test.mts +45 -0
  119. package/src/quant-arena/windows.ts +54 -0
  120. package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
  121. package/src/rollout-ledger/backfill-swe-arena.test.mts +338 -0
  122. package/src/rollout-ledger/settle-capture.mts +442 -0
  123. package/src/rollout-ledger/settle-capture.test.mts +270 -0
  124. package/src/swe-arena/activation.mts +228 -0
  125. package/src/swe-arena/activation.test.mts +303 -0
  126. package/src/swe-arena/arms.ts +30 -14
  127. package/src/swe-arena/briefing.mts +217 -0
  128. package/src/swe-arena/briefing.test.mts +178 -0
  129. package/src/swe-arena/calibrate.ts +102 -1
  130. package/src/swe-arena/cell-evidence.mts +41 -9
  131. package/src/swe-arena/factory-command-container.ts +284 -0
  132. package/src/swe-arena/factory-judge-child.mts +228 -0
  133. package/src/swe-arena/factory.test.mts +643 -0
  134. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
  135. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
  136. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
  137. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
  138. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
  139. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
  140. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
  141. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
  142. package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
  143. package/src/swe-arena/fixtures/run-report/README.md +43 -0
  144. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
  145. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
  146. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
  147. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
  148. package/src/swe-arena/fixtures.ts +135 -2
  149. package/src/swe-arena/gepa-seat.mts +583 -0
  150. package/src/swe-arena/gepa-seat.test.mts +635 -0
  151. package/src/swe-arena/ledger-orphans.mts +77 -0
  152. package/src/swe-arena/ledger-orphans.test.mts +147 -0
  153. package/src/swe-arena/lineage-record.mts +164 -0
  154. package/src/swe-arena/lineage-record.test.mts +115 -0
  155. package/src/swe-arena/outer-loop.mts +677 -27
  156. package/src/swe-arena/premeasured-from-cells.mts +281 -0
  157. package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
  158. package/src/swe-arena/proposer-fanout.mts +295 -14
  159. package/src/swe-arena/proposer-fanout.test.mts +247 -0
  160. package/src/swe-arena/proposer-provenance.mts +177 -0
  161. package/src/swe-arena/proposer-provenance.test.mts +106 -0
  162. package/src/swe-arena/run-experiment.mts +367 -1
  163. package/src/swe-arena/run-report.mts +75 -0
  164. package/src/swe-arena/run-supervisor.test.mts +5 -3
  165. package/src/swe-arena/score-split.mts +140 -0
  166. package/src/swe-arena/score-split.test.mts +123 -0
  167. package/src/swe-arena/types.ts +52 -0
  168. package/src/swe-structural.mts +245 -837
  169. package/dist/chunk-LRRD7NAG.js.map +0 -1
  170. /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
  171. /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
  172. /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
  173. /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
  174. /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
  175. /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
  176. /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
  177. /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
  178. /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
  179. /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
  180. /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
  181. /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
  182. /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
  183. /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
  184. /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
  185. /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
  186. /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
  187. /package/dist/{chunk-PWQVGAJB.js.map → chunk-V7AEBY6U.js.map} +0 -0
  188. /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
  189. /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
  190. /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
  191. /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
@@ -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'
@@ -122,13 +122,47 @@ import {
122
122
  import {
123
123
  defaultProposers,
124
124
  fanOutLoopsGenerator,
125
+ materializeParetoParents,
125
126
  proposerShotHooks,
127
+ type ParetoParentContext,
128
+ type ParetoParentSeed,
126
129
  type PrefilterConfig,
127
130
  type PrefilterKill,
128
131
  type ProposerSpec,
129
132
  type SmokeRunner,
130
133
  type SmokeVerdict,
131
134
  } from './proposer-fanout.mts'
135
+ import { captureProposerProvenance } from './proposer-provenance.mts'
136
+ import { CRASH_ORPHAN_REASON, reconcileCrashOrphansOnDisk } from './ledger-orphans.mts'
137
+ import {
138
+ AUTHOR_BRIEFING_VERSION,
139
+ resolveAuthorBriefing,
140
+ writeEvidenceIndex,
141
+ type BriefingContext,
142
+ } from './briefing.mts'
143
+ import {
144
+ readCommittedPredicate,
145
+ runActivationPredicate,
146
+ ACTIVATION_PREDICATE_RELPATH,
147
+ type ActivationRecord,
148
+ } from './activation.mts'
149
+ import {
150
+ loadOrCreateScoreSplit,
151
+ subScores,
152
+ type ScoreSplit,
153
+ type ScoreSplitConfig,
154
+ } from './score-split.mts'
155
+ import { recordLineageGeneration, type LineageCandidateInput } from './lineage-record.mts'
156
+ import {
157
+ reportSupervisorRound,
158
+ writeSupervisorRunReportSafe,
159
+ } from '@tangle-network/agent-eval/supervisor-run'
160
+ import {
161
+ campaignCoordsFromCellPath,
162
+ createSettleCapture,
163
+ type SettleCapture,
164
+ } from '../rollout-ledger/settle-capture.mts'
165
+ import { findSupervisorRunDir } from './arms.ts'
132
166
  import { installProcessSignalAbort, run, runOk } from './proc.ts'
133
167
  import { loadInstanceImages } from './run-experiment.mts'
134
168
  import {
@@ -480,6 +514,16 @@ export interface StaircaseRow {
480
514
  armProvenance: { repo: string; commit: string } | null
481
515
  diffPath: string | null
482
516
  diffSha256: string | null
517
+ /** GEN-5 public/private sub-scores (selection stays on the combined count;
518
+ * the private sub-score is never surfaced to proposers). */
519
+ split?: {
520
+ publicInstances: string[]
521
+ privateInstances: string[]
522
+ publicResolvedCount: number
523
+ privateResolvedCount: number
524
+ }
525
+ /** GEN-5 activation-gate outcome for this candidate. */
526
+ activation?: ActivationRecord
483
527
  }
484
528
 
485
529
  const STAIRCASE_VERDICTS: ReadonlySet<string> = new Set([
@@ -489,6 +533,7 @@ const STAIRCASE_VERDICTS: ReadonlySet<string> = new Set([
489
533
  'rejected-out-of-space',
490
534
  'rejected-incomplete',
491
535
  'rejected-prefilter',
536
+ 'quarantined-inactive',
492
537
  ])
493
538
 
494
539
  /** Parse + validate one staircase JSONL row. Throws on schema drift. */
@@ -598,12 +643,22 @@ export interface OuterLoopConfig {
598
643
  * an AgentProfile-pinned harness invocation (see proposer-fanout.mts).
599
644
  * When set, `populationSize` MUST equal `proposers.length` (one candidate
600
645
  * slot per proposer — enforced at launch). Unset = the legacy
601
- * single-author generator (`proposerHarness` + bare invocation). */
646
+ * single-author generator (`proposerHarness` + bare invocation).
647
+ * GEN-6: a spec with `engine` set is a GEPA seat (gepa-seat.mts) — the
648
+ * agent-eval external-GEPA adapter optimizes ONE change-space file as a
649
+ * string against the pre-filter smoke cell; requires `prefilter.enabled`. */
602
650
  proposers?: ProposerSpec[]
603
651
  /** GEN-3 cheap pre-filter: per candidate, change-space + tsc (the authoring
604
652
  * verifier) plus ONE smoke arm cell before any full-evaluation spend.
605
653
  * Killed candidates become `rejected-prefilter` staircase dots. */
606
654
  prefilter?: PrefilterConfig
655
+ /** GEN-4 Pareto parents: prior-run frontier candidates (loops commits +
656
+ * measured per-instance results) seeded into every author's prompt and
657
+ * the merge seat's explicit input. Seeded at OUR buildPrompt seam, not the
658
+ * lib's `ctx.paretoParents` — the lib frontier is within-run only and a
659
+ * prior campaign cannot be injected without its runDir + ledger receipts
660
+ * (see proposer-fanout.mts). */
661
+ paretoParents?: ParetoParentSeed[]
607
662
  /** Replicates per holdout instance in the operator-approved certification
608
663
  * run (holdout-certify.mts). Default 2 — the gen-2 winner failed 3/6 vs
609
664
  * 4/6 on a 1-rep holdout with exactly one discordant cell, a known
@@ -614,6 +669,29 @@ export interface OuterLoopConfig {
614
669
  * reps/fail-closed protocol, or 'measure' — the incumbent runs the same
615
670
  * 2-rep holdout first in the certification run. */
616
671
  holdoutBaseline?: Record<string, boolean> | 'measure'
672
+ /** GEN-5 public/private score split (score-split.mts): proposers + the
673
+ * pre-filter see only PUBLIC instances' scores/evidence; selection stays
674
+ * on the combined set. Unset = everything public (pre-gen-5 behavior). */
675
+ scoreSplit?: ScoreSplitConfig
676
+ /** GEN-5 MAP+TOOLBOX briefing (briefing.mts): write the per-run evidence
677
+ * index and append the toolbox/permission briefing (change-space
678
+ * overridable) to every author prompt. */
679
+ briefing?: typeof AUTHOR_BRIEFING_VERSION
680
+ /** GEN-5 activation gate (activation.mts): require a machine-checkable
681
+ * activation predicate per candidate (prefilter-enforced) and quarantine
682
+ * candidates whose mechanism never fired in their own campaign traces. */
683
+ activationGate?: boolean
684
+ /** GEN-5 settle-time rollout-ledger capture (rollout-ledger/settle-capture.mts):
685
+ * emit tangle.rollout.v1 lines live after each cell judges, with label-v2
686
+ * rewards. Default path: <outDir>/rollout-ledger.jsonl. */
687
+ rolloutLedger?: { enabled: boolean; path?: string; opencodeDb?: string }
688
+ /** GEN-5 lineage DAG (lineage-record.mts): record every candidate as a
689
+ * LineageNode at <outDir>/.evolve/lineage.jsonl and put the governor's
690
+ * continuation decision in the round summary. */
691
+ lineage?: boolean
692
+ /** GEN-5 evidence map: prior run outDirs whose arm-runs/judge/candidate
693
+ * evidence the authors may mine (rendered into the evidence index). */
694
+ priorEvidenceDirs?: string[]
617
695
  /** Router model ids for the blind diagnosis ensemble (config, never a
618
696
  * hardcoded unrouted model). */
619
697
  analystModels: string[]
@@ -804,6 +882,118 @@ export function defaultGen3Config(
804
882
  }
805
883
  }
806
884
 
885
+ // ---------------------------------------------------------------------------
886
+ // GEN-4 configuration — pinned per-proposer models (recorded in provenance),
887
+ // Pareto-parent seeding from the gen-3 frontier, and a dedicated merge seat.
888
+ // ---------------------------------------------------------------------------
889
+
890
+ /** The gen-3 frontier (run r4-mrwc0awe): winner + runner-up, both 2/6 vs the
891
+ * 1/6 baseline on DIFFERENT instances — complementary lessons, the merge
892
+ * seat's input. Per-instance verdicts are the fail-closed all-reps values
893
+ * from `.evolve/rounds/gen-0.jsonl`. */
894
+ export const GEN3_PARETO_PARENTS: ParetoParentSeed[] = [
895
+ {
896
+ commit: 'cc0d95584c7ea14324cd57c21fe946c7c0f53827',
897
+ label: 'default-author',
898
+ resolvedInstances: ['pydata__xarray-4687', 'sphinx-doc__sphinx-9658'],
899
+ note:
900
+ 'mechanical patch-risk scan (patchRiskWarnings in src/worker-evidence.ts, threaded through ' +
901
+ 'extensions/pi/loops.ts) + never-reword / test-seam / run-the-neighbors worker rules + 3-check ' +
902
+ 'reviewer; pytest-dev__pytest-6197 split 0/1 across reps (near-miss)',
903
+ },
904
+ {
905
+ commit: 'a7a2a982e51551de3a8e796ee2efc448ed405e6a',
906
+ label: 'prompts-author',
907
+ resolvedInstances: ['django__django-11532', 'pydata__xarray-4687'],
908
+ note:
909
+ 'prompt-only: hidden-suite bullet in the supervisor GOAL-authoring section (the django seam), ' +
910
+ 'frozen-behavior worker section + run-the-repo-tests discipline, 2-check reviewer; ' +
911
+ 'sphinx-doc__sphinx-9658 split 0/1 across reps (near-miss)',
912
+ },
913
+ ]
914
+
915
+ /**
916
+ * The gen-4 config: protocol round 4 continues (frozen arm, same holdout
917
+ * registry, same roundsDir staircase) with three changes as a unit:
918
+ *
919
+ * 1. PINNED PER-PROPOSER MODELS — four seats: claude-author (claude CLI on
920
+ * its own login; the resolved model + CLI version are captured into
921
+ * `<outDir>/proposer-provenance.json` at t=0), glm-author (opencode
922
+ * pinned to zai-coding-plan/glm-5.2 via `-m`), codex-author (codex CLI on
923
+ * its ChatGPT login, auth provenance-gated at launch; drop the seat via
924
+ * `includeCodex: false` when the CLI is absent), and merge-author (claude,
925
+ * merge seat).
926
+ * 2. PARETO PARENTS — the gen-3 winner + runner-up diffs and their measured
927
+ * per-instance results seed every author's prompt; the merge seat's task
928
+ * is their coherent union. Seeded at the buildPrompt seam (our seam): the
929
+ * lib's `ctx.paretoParents` frontier is within-run only, and a prior
930
+ * campaign cannot cross runs without its runDir + ledger receipts.
931
+ * 3. PINNED BASELINE — the premeasured artifact at `hh/gen4/` is BUILT from
932
+ * gen-3's measured baseline cells (premeasured-from-cells.mts; gen-3
933
+ * measured astropy F, django F, matplotlib F, xarray F, pytest F,
934
+ * sphinx T — matplotlib/django false under current weather), so gen-4
935
+ * spends nothing re-measuring and fails loud if the loops tip moved.
936
+ */
937
+ export function defaultGen4Config(
938
+ hh = DEFAULT_HH_SCRATCHPAD,
939
+ opts: { outDirName?: string; includeCodex?: boolean } = {},
940
+ ): OuterLoopConfig {
941
+ const base = defaultGen3Config(hh, { outDirName: opts.outDirName ?? 'gen4' })
942
+ const proposers: ProposerSpec[] = [
943
+ { name: 'claude-author', profile: 'default-author.profile.json', harness: 'claude' },
944
+ { name: 'glm-author', harness: 'opencode', model: 'zai-coding-plan/glm-5.2' },
945
+ ...(opts.includeCodex === false ? [] : [{ name: 'codex-author', harness: 'codex' } satisfies ProposerSpec]),
946
+ { name: 'merge-author', profile: 'default-author.profile.json', harness: 'claude', merge: true },
947
+ ]
948
+ return {
949
+ ...base,
950
+ populationSize: proposers.length,
951
+ proposers,
952
+ paretoParents: [...GEN3_PARETO_PARENTS],
953
+ }
954
+ }
955
+
956
+ // ---------------------------------------------------------------------------
957
+ // GEN-5 configuration — gen-4's shape (4 proposers incl. the merge seat,
958
+ // Pareto parents, premeasured baseline carried forward per the same
959
+ // cell-derivation, 2 reps, deferred holdout) PLUS the gen-5 integration
960
+ // bundle as a unit:
961
+ //
962
+ // 1. MAP+TOOLBOX briefing — per-run evidence index + toolbox/permission
963
+ // briefing (change-space overridable at extensions/pi/author-briefing.md);
964
+ // the 3-analyst diagnosis stays as ONE input among the named tools.
965
+ // 2. PUBLIC/PRIVATE SPLIT — 4 public / 2 private of the 6 instances,
966
+ // deterministically seeded by runId and persisted per outDir; proposers +
967
+ // prefilter see public only, selection stays combined. Small-n caveat
968
+ // documented in score-split.mts.
969
+ // 3. ACTIVATION GATE — required machine-checkable predicate per candidate;
970
+ // never-fired mechanisms are quarantined even on an improved score.
971
+ // 4. SETTLE-TIME ROLLOUT LEDGER — tangle.rollout.v1 lines live per cell,
972
+ // label v2 (contribution-aware workers, baseline-relative proposers).
973
+ // 5. LINEAGE DAG — agent-eval Lineage at <outDir>/.evolve/lineage.jsonl +
974
+ // governor continuation decision in the round summary; staircase rows
975
+ // unchanged (observatory contract).
976
+ // ---------------------------------------------------------------------------
977
+
978
+ export function defaultGen5Config(
979
+ hh = DEFAULT_HH_SCRATCHPAD,
980
+ opts: { outDirName?: string; includeCodex?: boolean } = {},
981
+ ): OuterLoopConfig {
982
+ const base = defaultGen4Config(hh, {
983
+ outDirName: opts.outDirName ?? 'gen5',
984
+ ...(opts.includeCodex !== undefined ? { includeCodex: opts.includeCodex } : {}),
985
+ })
986
+ return {
987
+ ...base,
988
+ scoreSplit: { publicCount: 4 },
989
+ briefing: AUTHOR_BRIEFING_VERSION,
990
+ activationGate: true,
991
+ rolloutLedger: { enabled: true },
992
+ lineage: true,
993
+ priorEvidenceDirs: [join(hh, 'gen4'), join(hh, 'gen3')],
994
+ }
995
+ }
996
+
807
997
  // ---------------------------------------------------------------------------
808
998
  // Round recorder — dispatch-time change-space fail-closed + diff writing ONLY.
809
999
  // NOT a scoring source: scoring reads the lib's campaign cells
@@ -1059,11 +1249,21 @@ export function loopsCandidateVerifier(loopsRepo: string): Verifier {
1059
1249
  * only — the rest of the run keeps its env untouched. */
1060
1250
  const CLAUDE_AMBIENT_AUTH_VARS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL'] as const
1061
1251
 
1252
+ /** Same failure class for the gen-4 codex seat: agent-state.env injects an
1253
+ * OPENAI_API_KEY meant for other tooling, and the codex CLI prefers env-key
1254
+ * auth over its ChatGPT login. The codex author shot must run on the CLI's
1255
+ * own login (`codex login status` is provenance-gated at launch), so the
1256
+ * leaked auth is stripped for the shot subprocess only. */
1257
+ const CODEX_AMBIENT_AUTH_VARS = ['OPENAI_API_KEY', 'OPENAI_BASE_URL'] as const
1258
+
1062
1259
  export function proposerShotEnv(harness: OuterLoopConfig['proposerHarness']): NodeJS.ProcessEnv {
1063
1260
  const env: NodeJS.ProcessEnv = { ...process.env }
1064
1261
  if (harness === 'claude') {
1065
1262
  for (const name of CLAUDE_AMBIENT_AUTH_VARS) delete env[name]
1066
1263
  }
1264
+ if (harness === 'codex') {
1265
+ for (const name of CODEX_AMBIENT_AUTH_VARS) delete env[name]
1266
+ }
1067
1267
  return env
1068
1268
  }
1069
1269
 
@@ -1151,6 +1351,33 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1151
1351
  // premeasured baseline and pin the depth dial (see capabilities.mts).
1152
1352
  assertSubstratePassthroughs(log)
1153
1353
 
1354
+ // GEN-4 model-identity provenance at t=0: harness CLI versions, the claude
1355
+ // seat's resolved settings model, codex auth, and every explicit model pin.
1356
+ // Fails loud on a missing/unauthed harness binary — populationSize equals
1357
+ // proposers.length, so a dead seat cannot be skipped mid-run.
1358
+ if (config.proposers !== undefined) {
1359
+ const provenance = await captureProposerProvenance(config.proposers)
1360
+ await mkdir(config.outDir, { recursive: true })
1361
+ await writeFile(join(config.outDir, 'proposer-provenance.json'), JSON.stringify(provenance, null, 2))
1362
+ for (const p of provenance.proposers) {
1363
+ log(
1364
+ `proposer ${p.name} (${p.harness}${p.merge ? ', merge seat' : ''}): ` +
1365
+ `model=${p.pinnedModel ?? `cli-default${p.settingsModel ? `:${p.settingsModel}` : ''}`} ` +
1366
+ `version=${p.harnessVersion.split('\n')[0]}`,
1367
+ )
1368
+ }
1369
+ }
1370
+
1371
+ // GEN-4 Pareto parents: materialize the configured prior-run frontier
1372
+ // (commit existence + full diffs) before any authoring.
1373
+ const paretoParents: ParetoParentContext[] =
1374
+ config.paretoParents !== undefined && config.paretoParents.length > 0
1375
+ ? await materializeParetoParents(config.loopsRepo, config.paretoParents)
1376
+ : []
1377
+ if (paretoParents.length > 0) {
1378
+ log(`pareto parents: ${paretoParents.map((p) => `${p.label}@${p.commit.slice(0, 10)}`).join(', ')}`)
1379
+ }
1380
+
1154
1381
  // The gate's only denominator: a stored prior baseline campaign the LIB
1155
1382
  // validates (surface hash, seed, reps, split digest, coverage) before
1156
1383
  // skipping the baseline campaign. A missing artifact = the BOOTSTRAP run —
@@ -1179,14 +1406,41 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1179
1406
  const excludes = await loadExcludes()
1180
1407
  const images = await loadInstanceImages(config.instanceImagesPath)
1181
1408
  const adapter = createSweBenchAdapter()
1409
+ const runId = `r${config.round}-${Date.now().toString(36)}`
1410
+ await mkdir(config.outDir, { recursive: true })
1411
+
1412
+ // GEN-5 public/private split — deterministic (seeded by runId), PERSISTED
1413
+ // per outDir so a resume can never rotate private instances into view.
1414
+ // Scored identically; selection stays combined; proposers + prefilter see
1415
+ // public only.
1416
+ const split: ScoreSplit | null =
1417
+ config.scoreSplit !== undefined
1418
+ ? await loadOrCreateScoreSplit({
1419
+ outDir: config.outDir,
1420
+ runId,
1421
+ instances: config.instances,
1422
+ publicCount: config.scoreSplit.publicCount,
1423
+ })
1424
+ : null
1425
+ const privateIids = new Set(split?.privateInstances ?? [])
1426
+ if (split !== null) {
1427
+ log(
1428
+ `score split (seeded by ${split.seededBy}): public [${split.publicInstances.join(', ')}] + ` +
1429
+ `${split.privateInstances.length} private instance(s) (identities withheld from proposers; ` +
1430
+ `selection uses public+private combined; small-n caveat: 2 private of 6 is a direction check, not certification)`,
1431
+ )
1432
+ }
1433
+
1182
1434
  // The pre-filter's smoke instance may sit outside the improvement set (e.g.
1183
1435
  // a designated cheap instance) — it needs the same problem/image/verify
1184
- // validation and rides the same loaded-task map.
1436
+ // validation and rides the same loaded-task map. Under the gen-5 split the
1437
+ // smoke choice is restricted to PUBLIC instances (the prefilter surfaces
1438
+ // its verdict to the kill log the authors can mine).
1185
1439
  const smokeIid =
1186
1440
  config.proposers !== undefined && config.prefilter?.enabled
1187
1441
  ? resolveSmokeInstance(
1188
1442
  config.prefilter.smokeInstance,
1189
- config.instances,
1443
+ split !== null ? split.publicInstances : config.instances,
1190
1444
  premeasured ? cellsFromCampaign(premeasured.campaign) : null,
1191
1445
  )
1192
1446
  : null
@@ -1208,12 +1462,57 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1208
1462
  const judge: SerializedJudge = createSerializedJudge(
1209
1463
  config.judgeTimeoutMs !== undefined ? { timeoutMs: config.judgeTimeoutMs } : {},
1210
1464
  )
1211
- const runId = `r${config.round}-${Date.now().toString(36)}`
1212
- await mkdir(config.outDir, { recursive: true })
1213
1465
  await mkdir(config.roundsDir, { recursive: true })
1214
1466
  const recorder = new RoundRecorder(config.loopsRepo, join(config.outDir, 'candidates'))
1215
1467
  const analysts: AnalystSpec[] = config.analystModels.map((model, i) => ({ id: `${model}#${i + 1}`, model }))
1216
1468
 
1469
+ // GEN-5 MAP+TOOLBOX briefing: persist the Pareto parent diffs, write the
1470
+ // per-run evidence index (a map — one line per evidence path, private
1471
+ // instances excluded), and resolve the briefing text (the change-space
1472
+ // override at extensions/pi/author-briefing.md wins over the default).
1473
+ let briefingCtx: BriefingContext | undefined
1474
+ if (config.briefing === AUTHOR_BRIEFING_VERSION) {
1475
+ const parentPatches: Array<{ label: string; path: string }> = []
1476
+ if (paretoParents.length > 0) {
1477
+ const parentsDir = join(config.outDir, 'pareto-parents')
1478
+ await mkdir(parentsDir, { recursive: true })
1479
+ for (const parent of paretoParents) {
1480
+ const patchPath = join(parentsDir, `${parent.label}.patch`)
1481
+ await writeFile(patchPath, parent.diff)
1482
+ parentPatches.push({ label: parent.label, path: patchPath })
1483
+ }
1484
+ }
1485
+ const index = await writeEvidenceIndex({
1486
+ outDir: config.outDir,
1487
+ roundsDir: config.roundsDir,
1488
+ seedArtifactRuns: config.seedArtifactRuns,
1489
+ ...(config.priorEvidenceDirs !== undefined ? { priorEvidenceDirs: config.priorEvidenceDirs } : {}),
1490
+ paretoParentPatches: parentPatches,
1491
+ split,
1492
+ })
1493
+ const briefing = await resolveAuthorBriefing(config.loopsRepo, config.loopsBaseRef)
1494
+ briefingCtx = { indexPath: index.path, briefingText: briefing.text, briefingSource: briefing.source }
1495
+ log(
1496
+ `briefing ${AUTHOR_BRIEFING_VERSION}: evidence index → ${index.path} (${index.rows.length} row(s)); ` +
1497
+ `briefing text source: ${briefing.source}`,
1498
+ )
1499
+ }
1500
+
1501
+ // GEN-5 settle-time rollout ledger — tangle.rollout.v1 lines appended live
1502
+ // after each cell judges (label v2); capture failure logs loud but never
1503
+ // kills a cell.
1504
+ const settleCapture: SettleCapture | null =
1505
+ config.rolloutLedger?.enabled === true
1506
+ ? createSettleCapture({
1507
+ ledgerPath: config.rolloutLedger.path ?? join(config.outDir, 'rollout-ledger.jsonl'),
1508
+ runId,
1509
+ instanceCount: config.instances.length,
1510
+ ...(config.rolloutLedger.opencodeDb !== undefined ? { opencodeDb: config.rolloutLedger.opencodeDb } : {}),
1511
+ log,
1512
+ })
1513
+ : null
1514
+ if (settleCapture !== null) log(`rollout-ledger: settle-time capture ON → ${settleCapture.path}`)
1515
+
1217
1516
  const sweScenarios: Scenario[] = config.instances.map((iid) => ({ id: iid, kind: 'swe-instance' }))
1218
1517
 
1219
1518
  // Capacity gates on BOTH paths the supervisor arm rides (worker + router).
@@ -1337,6 +1636,8 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1337
1636
  resolved: outcome.resolved,
1338
1637
  patchLines: outcome.armRes.patch_lines,
1339
1638
  wallS,
1639
+ // GEN-6: the GEPA seat's inner-score tiebreak.
1640
+ verifyPass: outcome.armRes.verify_pass,
1340
1641
  }
1341
1642
  await mkdir(armOutDir, { recursive: true })
1342
1643
  await writeFile(join(armOutDir, 'smoke.json'), JSON.stringify(result, null, 2))
@@ -1431,6 +1732,14 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1431
1732
  await writeFile(join(runDir, 'judge.json'), JSON.stringify({ ...verdict, wallS: judgeWallS }, null, 1))
1432
1733
  log(`${config.armName} ${rec.tag} ${iid} judged: resolved=${verdict.resolved} (attempts=${verdict.attempts})`)
1433
1734
 
1735
+ // Deterministic run observability, per cell: steer count, waves, concurrency,
1736
+ // idle, evidence→respawn, cost by role. The headline lands in the run log so
1737
+ // the answers are in the tail without a follow-up command.
1738
+ await writeSupervisorRunReportSafe(runDir, {
1739
+ appendHeadlineTo: join(config.outDir, 'run.log'),
1740
+ patchPath: armRes.patchPath,
1741
+ })
1742
+
1434
1743
  const spend = armRes.recoveredSpend
1435
1744
  const recovered =
1436
1745
  armRes.spentTokens === null && (spend?.workerTokSqlite ?? null) === null
@@ -1454,7 +1763,61 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1454
1763
  recoveredTokens: recovered,
1455
1764
  }) + '\n',
1456
1765
  )
1457
- await ctx.artifacts.writeJson('arm-summary.json', { runDir, patchPath: armRes.patchPath, verdict })
1766
+ const summaryPath = await ctx.artifacts.writeJson('arm-summary.json', {
1767
+ runDir,
1768
+ patchPath: armRes.patchPath,
1769
+ verdict,
1770
+ })
1771
+
1772
+ // GEN-5 settle-time rollout capture: emit supervisor + worker lines
1773
+ // NOW, while the opencode store still holds the worker transcripts.
1774
+ // Attribution comes from the campaign cell path (never dispatch
1775
+ // order); a capture failure logs loud but never kills the cell.
1776
+ if (settleCapture !== null) {
1777
+ try {
1778
+ const coords = campaignCoordsFromCellPath(summaryPath)
1779
+ if (coords === null) {
1780
+ log(`rollout-ledger: cannot derive campaign coords from ${summaryPath} — cell ${iid} r${ctx.rep} skipped`)
1781
+ } else {
1782
+ const supRunDir = await findSupervisorRunDir(armRes.ws)
1783
+ const deliveredPatch = await readFile(armRes.patchPath, 'utf8').catch(() => '')
1784
+ await settleCapture.captureCell({
1785
+ generation: coords.generation,
1786
+ candidateIndex: coords.candidateIndex,
1787
+ iid,
1788
+ rep: ctx.rep,
1789
+ seed: ctx.seed,
1790
+ splitVisibility: split === null ? null : privateIids.has(iid) ? 'private' : 'public',
1791
+ commit: cs.candidateCommit,
1792
+ resolved: verdict.resolved,
1793
+ judgeVerdict: { ...verdict, wallS: judgeWallS },
1794
+ runDir,
1795
+ patchPath: armRes.patchPath,
1796
+ supRunDir,
1797
+ deliveredPatch,
1798
+ workerModel: config.arm.workerModel,
1799
+ metrics: {
1800
+ resolved: verdict.resolved,
1801
+ verify_pass: armRes.verify_pass,
1802
+ patch_lines: armRes.patch_lines,
1803
+ judge_attempts: verdict.attempts ?? null,
1804
+ judge_wall_s: judgeWallS,
1805
+ spent_tokens: armRes.spentTokens,
1806
+ spent_usd: armRes.spentUsd,
1807
+ recovered_tokens: recovered,
1808
+ sup_status: armRes.sup_status,
1809
+ sup_verdict: armRes.sup_verdict,
1810
+ spawned: armRes.spawned,
1811
+ workers: armRes.workers,
1812
+ settled: armRes.settled,
1813
+ },
1814
+ cost: { usd: armRes.spentUsd, wallS: armRes.wall_s, spentTokens: armRes.spentTokens },
1815
+ })
1816
+ }
1817
+ } catch (cause) {
1818
+ log(`rollout-ledger: settle-time capture FAILED for ${iid} r${ctx.rep}: ${(cause as Error).message}`)
1819
+ }
1820
+ }
1458
1821
 
1459
1822
  if (verdict.resolved === null) {
1460
1823
  // Inconclusive judge (double flake / infra) — the cell must FAIL, not
@@ -1557,6 +1920,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1557
1920
  const runs: SupRunArtifacts[] = []
1558
1921
  if (input.generation === -1) {
1559
1922
  for (const seed of config.seedArtifactRuns) {
1923
+ if (privateIids.has(seed.iid)) continue // gen-5 split: never surfaced to proposers
1560
1924
  if (!existsSync(seed.dir)) {
1561
1925
  // A wiped scratchpad (host reboot) must not feed EMPTY bundles to the
1562
1926
  // analysts as if they were real artifacts — skip loudly.
@@ -1581,6 +1945,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1581
1945
  for (const cell of cells) {
1582
1946
  const a = cell.artifact
1583
1947
  if (a === null || a.kind !== 'swe-arm' || !a.runDir) continue
1948
+ if (privateIids.has(a.iid)) continue // gen-5 split: never surfaced to proposers
1584
1949
  runs.push({
1585
1950
  iid: a.iid,
1586
1951
  arm: config.armName,
@@ -1618,8 +1983,25 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1618
1983
  log(`diagnosis ensemble FAILED for gen ${input.generation}: ${(cause as Error).message}`)
1619
1984
  }
1620
1985
  }
1986
+ // GEN-5 split: the raw-trace distiller must not hand private-instance
1987
+ // cells' path context to the authors either — censor them out of the
1988
+ // candidates' campaigns before distillation.
1989
+ const censoredInput =
1990
+ split === null
1991
+ ? input
1992
+ : {
1993
+ ...input,
1994
+ candidates: input.candidates.map((cand) => {
1995
+ const campaign = cand.campaign as { cells?: Array<{ scenarioId: string }> } | null
1996
+ if (campaign === null || typeof campaign !== 'object' || !Array.isArray(campaign.cells)) return cand
1997
+ return {
1998
+ ...cand,
1999
+ campaign: { ...campaign, cells: campaign.cells.filter((c) => !privateIids.has(c.scenarioId)) },
2000
+ }
2001
+ }),
2002
+ }
1621
2003
  signal?.throwIfAborted()
1622
- const rawFindings = (await rawTrace(input as Parameters<typeof rawTrace>[0])) as unknown[]
2004
+ const rawFindings = (await rawTrace(censoredInput as Parameters<typeof rawTrace>[0])) as unknown[]
1623
2005
  signal?.throwIfAborted()
1624
2006
  return [steeringFinding, ...ensembleFindings, ...rawFindings]
1625
2007
  }
@@ -1638,12 +2020,36 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1638
2020
  'tsx src/swe-arena/holdout-certify.mts <config.json> --candidate <winner-loops-commit>'
1639
2021
  const improveRunDir = join(config.outDir, 'improve-run')
1640
2022
 
2023
+ // ── crash recovery: a killed run leaves its in-flight paid call 'pending'
2024
+ // in the durable cost ledger, and the ledger's fail-closed guard then
2025
+ // refuses ALL new paid work on resume. Under the outDir instance lock
2026
+ // (sole runner), every pending call restored from disk is provably from a
2027
+ // dead process — settle each as a $0 failure receipt (reason
2028
+ // 'process-crash-orphan') so the guard passes without erasing the crash
2029
+ // from the durable record. ───────────────────────────────────────────
2030
+ for (const receipt of reconcileCrashOrphansOnDisk(improveRunDir)) {
2031
+ log(
2032
+ `cost-ledger: reconciled crash-orphaned call '${receipt.callId}' ` +
2033
+ `(${receipt.actor}, ${receipt.phase}) as ${CRASH_ORPHAN_REASON}`,
2034
+ )
2035
+ }
2036
+
1641
2037
  // ── generator: the gen-3 proposer fan-out (parallel AgentProfile-pinned
1642
2038
  // authors + pre-filter) when `proposers` is configured; the legacy
1643
2039
  // single-author generator otherwise. ─────────────────────────────────
1644
2040
  const fanout =
1645
2041
  config.proposers !== undefined
1646
- ? fanOutLoopsGenerator(config, { ...(smokeRunner ? { smokeRunner } : {}), log })
2042
+ ? fanOutLoopsGenerator(config, {
2043
+ ...(smokeRunner ? { smokeRunner } : {}),
2044
+ ...(paretoParents.length > 0 ? { parents: paretoParents } : {}),
2045
+ ...(briefingCtx !== undefined ? { briefing: briefingCtx } : {}),
2046
+ // GEN-6: the GEPA seat's inner evaluator rides the SAME public-only
2047
+ // smoke instance; the split guards the never-surfaced invariant at
2048
+ // the bridge boundary too.
2049
+ ...(smokeIid !== null ? { smokeInstanceId: smokeIid } : {}),
2050
+ scoreSplit: split,
2051
+ log,
2052
+ })
1647
2053
  : null
1648
2054
  const generator = withParentCancellation(fanout ?? constrainedLoopsGenerator(config), signal)
1649
2055
 
@@ -1737,10 +2143,25 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1737
2143
  }
1738
2144
  }
1739
2145
 
2146
+ // Collected per-candidate facts for the gen-5 machinery (activation by
2147
+ // surface hash for the winner brief, lineage nodes, proposer v2 rewards).
2148
+ const activationBySurface = new Map<string, ActivationRecord>()
2149
+ const lineageCandidates: LineageCandidateInput[] = []
2150
+ interface ProposerOutcomeFact {
2151
+ generation: number
2152
+ candidateIndex: number
2153
+ label: string
2154
+ commit: string | null
2155
+ resolvedCount: number
2156
+ diffPath: string | null
2157
+ }
2158
+ const proposerFacts: ProposerOutcomeFact[] = []
2159
+
1740
2160
  for (let g = 0; g < loop.generations.length; g++) {
1741
2161
  const gen = loop.generations[g]!
1742
2162
  const rows: StaircaseRow[] = []
1743
- for (const cand of gen.record.candidates) {
2163
+ for (let candIndex = 0; candIndex < gen.record.candidates.length; candIndex++) {
2164
+ const cand = gen.record.candidates[candIndex]!
1744
2165
  const surface = gen.surfaces.find((s) => s.surfaceHash === cand.surfaceHash)?.surface
1745
2166
  const cs = surface && typeof surface === 'object' && surface.kind === 'code' ? surface : null
1746
2167
  const desc = cs ? await recorder.ensure(cs) : undefined
@@ -1764,6 +2185,71 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1764
2185
  const parentResolvedCount =
1765
2186
  parentCampaign !== undefined ? resolvedCountOf(parentCampaign) : measuredBaselineCount
1766
2187
  const violations = desc?.violations ?? []
2188
+
2189
+ // GEN-5 activation gate: run the candidate's own committed predicate
2190
+ // over its own cell run dirs. Fail-closed — a missing/unparseable
2191
+ // predicate (the prefilter should have killed it) quarantines.
2192
+ let activation: ActivationRecord | undefined
2193
+ if (config.activationGate === true && cs !== null) {
2194
+ const committed = await readCommittedPredicate(config.loopsRepo, cs.candidateCommit)
2195
+ if (committed === null || !committed.parsed.ok) {
2196
+ const why =
2197
+ committed === null
2198
+ ? `no ${ACTIVATION_PREDICATE_RELPATH} at ${cs.candidateCommit.slice(0, 10)}`
2199
+ : `unparseable activation predicate: ${committed.parsed.ok ? '' : committed.parsed.error}`
2200
+ activation = {
2201
+ present: false,
2202
+ description: null,
2203
+ fired: false,
2204
+ evidence: [],
2205
+ warnings: [`${why} — fail-closed quarantine`],
2206
+ }
2207
+ } else {
2208
+ const runDirs = [
2209
+ ...new Set(
2210
+ cells
2211
+ .map((c) => (c.artifact !== null && c.artifact.kind === 'swe-arm' ? c.artifact.runDir : null))
2212
+ .filter((d): d is string => typeof d === 'string' && d.length > 0),
2213
+ ),
2214
+ ]
2215
+ const res = await runActivationPredicate(committed.parsed.predicate, runDirs)
2216
+ activation = {
2217
+ present: true,
2218
+ description: committed.parsed.predicate.description,
2219
+ fired: res.fired,
2220
+ evidence: res.evidence,
2221
+ warnings: res.warnings,
2222
+ }
2223
+ }
2224
+ activationBySurface.set(cand.surfaceHash, activation)
2225
+ log(
2226
+ `activation ${cand.label ?? cand.surfaceHash.slice(0, 10)}: present=${activation.present} ` +
2227
+ `fired=${activation.fired}${activation.fired ? ` — ${activation.evidence[0] ?? ''}` : ''}` +
2228
+ `${activation.warnings.length > 0 ? ` (warnings: ${activation.warnings.join('; ')})` : ''}`,
2229
+ )
2230
+ }
2231
+
2232
+ // GEN-5 split sub-scores: both halves logged per candidate; the
2233
+ // selection rule stays combined (candResolved over ALL instances).
2234
+ const verdicts = instanceVerdictsFromCells(cells, config.instances, reps)
2235
+ const splitScores = split !== null ? subScores(verdicts, split) : null
2236
+ if (split !== null && splitScores !== null) {
2237
+ log(
2238
+ `split scores ${cand.label ?? cand.surfaceHash.slice(0, 10)}: ` +
2239
+ `public ${splitScores.publicResolvedCount}/${split.publicInstances.length}, ` +
2240
+ `private ${splitScores.privateResolvedCount}/${split.privateInstances.length} (combined ${candResolved}/${config.instances.length})`,
2241
+ )
2242
+ }
2243
+
2244
+ const verdict = decideVerdict({
2245
+ violations,
2246
+ coverageComplete,
2247
+ resolvedCount: candResolved,
2248
+ parentResolvedCount,
2249
+ costRatio,
2250
+ costGuardRatio: config.costGuardRatio,
2251
+ ...(activation !== undefined ? { activationFired: activation.fired } : {}),
2252
+ })
1767
2253
  rows.push({
1768
2254
  schema: STAIRCASE_SCHEMA,
1769
2255
  round: config.round,
@@ -1786,18 +2272,39 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1786
2272
  costRatio,
1787
2273
  costGuardRatio: config.costGuardRatio,
1788
2274
  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
- }),
2275
+ verdict,
1797
2276
  holdout: 'operator-approval-required',
1798
2277
  armProvenance: desc?.armProvenance ?? null,
1799
2278
  diffPath: desc?.diffPath ?? null,
1800
2279
  diffSha256: desc?.diffSha256 ?? null,
2280
+ ...(split !== null && splitScores !== null
2281
+ ? {
2282
+ split: {
2283
+ publicInstances: split.publicInstances,
2284
+ privateInstances: split.privateInstances,
2285
+ ...splitScores,
2286
+ },
2287
+ }
2288
+ : {}),
2289
+ ...(activation !== undefined ? { activation } : {}),
2290
+ })
2291
+
2292
+ const label = cand.label ?? cs?.candidateCommit?.slice(0, 10) ?? cand.surfaceHash.slice(0, 10)
2293
+ lineageCandidates.push({
2294
+ label,
2295
+ commit: cs?.candidateCommit ?? null,
2296
+ resolvedCount: candResolved,
2297
+ verdicts,
2298
+ merge: config.proposers?.find((p) => p.name === cand.label)?.merge === true,
2299
+ verdict,
2300
+ })
2301
+ proposerFacts.push({
2302
+ generation: g,
2303
+ candidateIndex: candIndex,
2304
+ label,
2305
+ commit: cs?.candidateCommit ?? null,
2306
+ resolvedCount: candResolved,
2307
+ diffPath: desc?.diffPath ?? null,
1801
2308
  })
1802
2309
  }
1803
2310
  const genFile = join(config.roundsDir, `gen-${g}.jsonl`)
@@ -1823,7 +2330,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1823
2330
  parent: premeasured?.surfaceHash ?? 'baseline',
1824
2331
  parentResolvedCount: measuredBaselineCount,
1825
2332
  label: kill.proposer,
1826
- rationale: `prefilter kill at stage '${kill.stage}' (${kill.harness})`,
2333
+ rationale: `prefilter kill at stage '${kill.stage}' (${kill.harness ?? 'engine'})`,
1827
2334
  changedFiles: [],
1828
2335
  changeSpaceViolations: kill.stage === 'change-space' ? [kill.reason] : [],
1829
2336
  perInstance: [],
@@ -1847,6 +2354,74 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1847
2354
  }
1848
2355
  }
1849
2356
 
2357
+ // GEN-5 label-v2 proposer rewards: baseline-relative (candidate − baseline
2358
+ // resolved fraction, improvement positive), one settle-time ledger line
2359
+ // per evaluated candidate now that the round's scores are final.
2360
+ if (settleCapture !== null) {
2361
+ const sanitizeName = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_')
2362
+ for (const fact of proposerFacts) {
2363
+ try {
2364
+ const flatDir = join(config.outDir, 'proposer-shots')
2365
+ const pattern = new RegExp(`^gen${fact.generation}-cand${fact.candidateIndex}-shot\\d+\\.json$`)
2366
+ const receiptPaths: string[] = []
2367
+ for (const dir of [flatDir, join(flatDir, sanitizeName(fact.label))]) {
2368
+ for (const name of (await readdir(dir).catch(() => [])).sort()) {
2369
+ if (pattern.test(name)) receiptPaths.push(join(dir, name))
2370
+ }
2371
+ }
2372
+ await settleCapture.captureProposer({
2373
+ generation: fact.generation,
2374
+ candidateIndex: fact.candidateIndex,
2375
+ proposer: fact.label,
2376
+ harness: config.proposers?.find((p) => p.name === fact.label)?.harness ?? null,
2377
+ commit: fact.commit,
2378
+ candResolved: fact.resolvedCount,
2379
+ baselineResolved: measuredBaselineCount,
2380
+ shotReceiptPaths: receiptPaths,
2381
+ diffPath: fact.diffPath,
2382
+ })
2383
+ } catch (cause) {
2384
+ log(`rollout-ledger: proposer capture FAILED for ${fact.label}: ${(cause as Error).message}`)
2385
+ }
2386
+ }
2387
+ }
2388
+
2389
+ // GEN-5 lineage DAG: record baseline root + pareto parents + every
2390
+ // evaluated candidate (multi-parent for the merge seat) at the
2391
+ // .evolve-compatible store, and ask the governor for the continuation
2392
+ // decision (recorded below — never acted on inside this run).
2393
+ let lineageResult: Awaited<ReturnType<typeof recordLineageGeneration>> | null = null
2394
+ if (config.lineage === true) {
2395
+ try {
2396
+ const baselineCommit =
2397
+ baselineCells.find((c) => c.artifact !== null && c.artifact.kind === 'swe-arm')?.artifact?.commit ??
2398
+ config.loopsBaseRef
2399
+ lineageResult = await recordLineageGeneration({
2400
+ outDir: config.outDir,
2401
+ runId,
2402
+ instances: config.instances,
2403
+ baseline: {
2404
+ commit: baselineCommit,
2405
+ resolvedCount: measuredBaselineCount,
2406
+ verdicts: instanceVerdictsFromCells(baselineCells, config.instances, reps),
2407
+ },
2408
+ paretoParents: (config.paretoParents ?? []).map((p) => ({
2409
+ label: p.label,
2410
+ commit: p.commit,
2411
+ resolvedInstances: p.resolvedInstances,
2412
+ })),
2413
+ candidates: lineageCandidates,
2414
+ })
2415
+ log(
2416
+ `lineage: ${lineageResult.appended.length} node(s) appended (total ${lineageResult.nodesTotal}) → ` +
2417
+ `${lineageResult.path}; governor decision: ${JSON.stringify(lineageResult.governor)}` +
2418
+ `${lineageResult.skipped.length > 0 ? `; skipped (no commit): ${lineageResult.skipped.join(', ')}` : ''}`,
2419
+ )
2420
+ } catch (cause) {
2421
+ log(`lineage: recording FAILED: ${(cause as Error).message}`)
2422
+ }
2423
+ }
2424
+
1850
2425
  const winnerSurface = result.raw.winner.surface
1851
2426
  const winnerCs =
1852
2427
  typeof winnerSurface === 'object' && winnerSurface !== null && winnerSurface.kind === 'code'
@@ -1884,6 +2459,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1884
2459
  // is worth approving.
1885
2460
  const winnerHash = winnerCs ? surfaceHash(winnerCs) : null
1886
2461
  const winnerCampaign = winnerHash !== null ? campaignBySurface.get(winnerHash) : undefined
2462
+ const winnerActivation = winnerHash !== null ? activationBySurface.get(winnerHash) : undefined
1887
2463
  const improvementSet =
1888
2464
  winnerCampaign !== undefined && winnerRec !== undefined
1889
2465
  ? gateEvidenceFromCells({
@@ -1893,6 +2469,7 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1893
2469
  iids: config.instances,
1894
2470
  reps,
1895
2471
  costGuardRatio: config.costGuardRatio,
2472
+ ...(winnerActivation !== undefined ? { activationFired: winnerActivation.fired } : {}),
1896
2473
  })
1897
2474
  : null
1898
2475
  const wouldKeep = improvementSet !== null && improvementSet.verdict === 'accepted'
@@ -1938,6 +2515,49 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1938
2515
  gateReasons: loop.gateResult.reasons,
1939
2516
  // Improvement-set (search-split) evidence — NOT a held-out measurement.
1940
2517
  improvementSet: improvementSet === null ? null : { ...improvementSet, wouldKeep },
2518
+ // GEN-5: the public/private split (sub-scores live per candidate in the
2519
+ // staircase rows; selection stays combined; private never surfaced to
2520
+ // proposers — the 2-of-6 private half is a direction check, not a
2521
+ // certification).
2522
+ scoreSplit:
2523
+ split === null
2524
+ ? null
2525
+ : {
2526
+ seededBy: split.seededBy,
2527
+ publicInstances: split.publicInstances,
2528
+ privateInstances: split.privateInstances,
2529
+ },
2530
+ // GEN-5: activation-gate outcomes per candidate surface.
2531
+ activationGate:
2532
+ config.activationGate === true
2533
+ ? {
2534
+ enabled: true,
2535
+ byCandidate: [...activationBySurface.entries()].map(([surface, a]) => ({
2536
+ surface,
2537
+ present: a.present,
2538
+ fired: a.fired,
2539
+ description: a.description,
2540
+ })),
2541
+ }
2542
+ : { enabled: false },
2543
+ // GEN-5: MAP+TOOLBOX briefing provenance.
2544
+ briefing:
2545
+ briefingCtx === undefined
2546
+ ? null
2547
+ : { version: AUTHOR_BRIEFING_VERSION, indexPath: briefingCtx.indexPath, textSource: briefingCtx.briefingSource },
2548
+ // GEN-5: settle-time rollout ledger location (tangle.rollout.v1, label v2).
2549
+ rolloutLedger: settleCapture === null ? null : { path: settleCapture.path, capture: 'settle-time', labels: 'v2' },
2550
+ // GEN-5: lineage DAG + the governor's recorded continuation decision.
2551
+ lineage:
2552
+ lineageResult === null
2553
+ ? null
2554
+ : {
2555
+ path: lineageResult.path,
2556
+ nodesTotal: lineageResult.nodesTotal,
2557
+ appended: lineageResult.appended.length,
2558
+ skipped: lineageResult.skipped,
2559
+ governor: lineageResult.governor,
2560
+ },
1941
2561
  // Honest run-wide spend from the lib's CostLedger: per-channel rollups
1942
2562
  // (agent = arm cells, judge = official-judge calls, driver = proposer
1943
2563
  // shots), token totals, and accounting-completeness flags.
@@ -1969,6 +2589,18 @@ export async function runRound(config: OuterLoopConfig, signal?: AbortSignal): P
1969
2589
  const summaryPath = join(config.roundsDir, `round${config.round}-summary-${runId}.json`)
1970
2590
  await writeFile(summaryPath, JSON.stringify(summary, null, 2))
1971
2591
  log(`round summary → ${summaryPath}`)
2592
+
2593
+ // Round rollup at gate time: every cell's orchestration/economics in one table,
2594
+ // written next to the round summary and echoed into the run log.
2595
+ await reportSupervisorRound(join(config.outDir, 'arm-runs'), {
2596
+ appendHeadlineTo: join(config.outDir, 'run.log'),
2597
+ reportDir: config.roundsDir,
2598
+ title: `Round ${config.round} rollup — ${runId}`,
2599
+ echo: true,
2600
+ }).catch((err: unknown) => {
2601
+ log(`round rollup failed: ${err instanceof Error ? err.message : String(err)}`)
2602
+ })
2603
+
1972
2604
  log(`gate: ${result.decision} — ${loop.gateResult.reasons[0] ?? ''}`)
1973
2605
  } finally {
1974
2606
  await result.dispose()
@@ -2087,17 +2719,35 @@ if (isMain) {
2087
2719
  if (argv[0] === '--write-config') {
2088
2720
  const path = argv[1]
2089
2721
  if (!path || path.startsWith('--')) {
2090
- console.error('usage: outer-loop.mts --write-config <path> [--out-name <dirname>] [--gen3]')
2722
+ console.error('usage: outer-loop.mts --write-config <path> [--out-name <dirname>] [--gen3|--gen4|--gen5]')
2091
2723
  process.exit(2)
2092
2724
  }
2093
2725
  const outDirName = flag('--out-name')
2094
2726
  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}`)
2727
+ const gen4 = argv.includes('--gen4')
2728
+ const gen5 = argv.includes('--gen5')
2729
+ let config: OuterLoopConfig
2730
+ let flavor: string
2731
+ if (gen4 || gen5) {
2732
+ // The codex seat rides only when the CLI is actually present — a config
2733
+ // naming a missing harness would fail the whole launch at t=0.
2734
+ const codexProbe = await run('codex', ['--version'])
2735
+ const includeCodex = codexProbe.code === 0
2736
+ if (!includeCodex) {
2737
+ console.log(
2738
+ `codex CLI unavailable (rc=${codexProbe.code}) — ${gen5 ? 'gen-5' : 'gen-4'} config written WITHOUT the codex-author seat`,
2739
+ )
2740
+ }
2741
+ const make = gen5 ? defaultGen5Config : defaultGen4Config
2742
+ config = make(undefined, { ...(outDirName ? { outDirName } : {}), includeCodex })
2743
+ flavor = gen5 ? 'gen-5' : 'gen-4'
2744
+ } else {
2745
+ const make = gen3 ? defaultGen3Config : defaultRound4Config
2746
+ config = make(undefined, outDirName ? { outDirName } : {})
2747
+ flavor = gen3 ? 'gen-3' : 'round-4'
2748
+ }
2749
+ await writeFile(path, JSON.stringify(config, null, 2) + '\n')
2750
+ console.log(`default ${flavor} config → ${path}`)
2101
2751
  } else if (argv[0] === '--calibration-smoke') {
2102
2752
  const dir = argv[1] && !argv[1].startsWith('--') ? argv[1] : undefined
2103
2753
  const n = flag('--analysts')
@@ -2130,7 +2780,7 @@ if (isMain) {
2130
2780
  } else {
2131
2781
  console.error(
2132
2782
  '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' +
2783
+ ' tsx src/swe-arena/outer-loop.mts --write-config <path> [--out-name <dirname>] [--gen3|--gen4|--gen5]\n' +
2134
2784
  ' tsx src/swe-arena/outer-loop.mts --calibration-smoke [supRunDir] [--analysts N] [--model M] [--endpoint router|zai] [--retries N]',
2135
2785
  )
2136
2786
  process.exit(2)