@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
@@ -38,7 +38,7 @@
38
38
 
39
39
  import { createHash } from 'node:crypto'
40
40
  import { readFileSync } from 'node:fs'
41
- import { mkdir, rm, writeFile } from 'node:fs/promises'
41
+ import { mkdir, readFile as readFilePromise, rm, writeFile } from 'node:fs/promises'
42
42
  import { join } from 'node:path'
43
43
  import { fileURLToPath } from 'node:url'
44
44
  import {
@@ -58,6 +58,14 @@ import {
58
58
  round4BuildPrompt,
59
59
  type OuterLoopConfig,
60
60
  } from './outer-loop.mts'
61
+ import {
62
+ ACTIVATION_PREDICATE_RELPATH,
63
+ activationPredicateInstruction,
64
+ parseActivationPredicate,
65
+ } from './activation.mts'
66
+ import { briefingPromptSection, type BriefingContext } from './briefing.mts'
67
+ import { gepaSeatAuthor, isGepaSeat, validateGepaSeat, type GepaMethodFactory } from './gepa-seat.mts'
68
+ import type { ScoreSplit } from './score-split.mts'
61
69
  import { run, runOk } from './proc.ts'
62
70
 
63
71
  // ---------------------------------------------------------------------------
@@ -70,13 +78,42 @@ export interface ProposerSpec {
70
78
  /** Path to an `AgentProfile` JSON. Absolute, or relative to this module's
71
79
  * `profiles/` directory. Omitted = bare profile (legacy invocation). */
72
80
  profile?: string
73
- harness: 'claude' | 'codex' | 'opencode'
81
+ /** Required for harness-authored seats. ABSENT on a GEN-6 engine seat
82
+ * (`engine` set) — enforced both ways at generator construction. */
83
+ harness?: 'claude' | 'codex' | 'opencode'
84
+ /** GEN-4 pinned model id, threaded to the harness CLI as `-m <model>` via
85
+ * the author profile's `model.default` (harnessInvocation maps it for all
86
+ * three harnesses). Unset = the CLI's own resolved model (its login/settings
87
+ * default) — recorded per run by the proposer-provenance capture, so the
88
+ * seat identity is pinned in provenance even when the flag is absent. */
89
+ model?: string
90
+ /** GEN-4 merge seat: this proposer's task is to MERGE the configured
91
+ * Pareto parents' diffs into one coherent surface (see
92
+ * `mergeAuthorPrompt`). Requires ≥2 materialized parents — enforced at
93
+ * generator construction. */
94
+ merge?: boolean
74
95
  /** Free-text authoring lens appended to the task prompt (e.g. a mechanics
75
96
  * or reviewer stance). Never overrides the protocol change-space text. */
76
97
  lens?: string
77
98
  /** Which diagnosis findings this proposer sees. Protocol/steering and
78
99
  * raw-trace-context findings always pass through. Default 'all'. */
79
100
  diagnosisSlice?: 'all' | 'mechanics' | 'prompts'
101
+ /** GEN-6 GEPA seat: this seat is an ENGINE invocation (agent-eval's
102
+ * external-GEPA adapter), not a harness CLI. `gepa` = one bounded engine
103
+ * run; `omni` = GEPA's published best-of-then-continue shape. Validation +
104
+ * authoring live in gepa-seat.mts. */
105
+ engine?: 'gepa' | 'omni'
106
+ /** GEN-6 GEPA seat: the ONE repo-relative change-space file GEPA optimizes
107
+ * as a string; the rest of the loops repo stays at the incumbent commit. */
108
+ surface?: string
109
+ /** GEN-6 GEPA seat: total inner-evaluation budget (each inner call is one
110
+ * real smoke arm cell). Default 10. */
111
+ maxMetricCalls?: number
112
+ /** GEN-6 GEPA seat: requested GEPA proposer spend cap in USD (the adapter
113
+ * reports GEPA's own model/CLI spend without an agent-eval receipt). */
114
+ maxProposerCostUsd?: number
115
+ /** GEN-6 GEPA seat: python executable for the bridge. Default 'python3'. */
116
+ python?: string
80
117
  }
81
118
 
82
119
  export interface PrefilterConfig {
@@ -100,6 +137,9 @@ export interface SmokeVerdict {
100
137
  resolved: boolean | null
101
138
  patchLines: number
102
139
  wallS: number
140
+ /** Committed verify fixture passed (GEN-6: the GEPA seat's inner-score
141
+ * tiebreak). Absent on errored smokes and pre-gen-6 records. */
142
+ verifyPass?: boolean
103
143
  }
104
144
 
105
145
  export type SmokeRunner = (args: {
@@ -114,7 +154,7 @@ export interface PrefilterKill {
114
154
  candidateIndex: number
115
155
  proposer: string
116
156
  harness: ProposerSpec['harness']
117
- stage: 'change-space' | 'smoke'
157
+ stage: 'change-space' | 'activation-predicate' | 'smoke'
118
158
  reason: string
119
159
  diffSha256: string | null
120
160
  /** Persisted kill-forensics patch under `<outDir>/proposer-patches/`. */
@@ -153,6 +193,132 @@ export function defaultProposers(): ProposerSpec[] {
153
193
  return [{ name: 'default-author', profile: 'default-author.profile.json', harness: 'claude' }]
154
194
  }
155
195
 
196
+ /** The profile the author shot actually runs: the loaded profile (if any) with
197
+ * the spec's PINNED MODEL merged into `model.default` — harnessInvocation
198
+ * turns that into the CLI's `-m <model>` flag (claude/codex/opencode all map
199
+ * it). Without a pinned model this is byte-identical to `loadAuthorProfile`,
200
+ * so gen-3 seats keep their exact invocation. A pinned model with no profile
201
+ * path synthesizes a minimal named profile carrying only the pin. */
202
+ export function resolveAuthorProfile(spec: ProposerSpec): AgentProfile | undefined {
203
+ const profile = loadAuthorProfile(spec)
204
+ if (!spec.model) return profile
205
+ const base: AgentProfile = profile ?? { name: `${spec.name}-pinned` }
206
+ return { ...base, model: { ...base.model, default: spec.model } }
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // GEN-4 Pareto parents — cross-run seeding at the buildPrompt seam.
211
+ //
212
+ // The LIB's own `ctx.paretoParents` (runOptimization → SurfaceProposer) only
213
+ // accumulates surfaces scored WITHIN one run: the frontier starts from this
214
+ // run's baseline and generations, and a prior campaign's surfaces cannot be
215
+ // injected without replaying that campaign's runDir + cost ledger (the cached
216
+ // cells refuse to load without their ledger receipts). Gen-4 runs in a fresh
217
+ // outDir, so the gen-3 winners are seeded HERE — their diffs + per-instance
218
+ // results enter every author's task prompt, and the merge seat gets both
219
+ // diffs as its explicit merge input. This is our seam, not the lib path;
220
+ // noted in the gen-4 config docs.
221
+ // ---------------------------------------------------------------------------
222
+
223
+ /** One prior-run frontier member, as configured (commits live in loopsRepo). */
224
+ export interface ParetoParentSeed {
225
+ /** Loops commit of the parent candidate (must exist in `loopsRepo`). */
226
+ commit: string
227
+ /** Staircase label, e.g. 'default-author'. */
228
+ label: string
229
+ /** Instances the parent resolved under the fail-closed all-reps rule. */
230
+ resolvedInstances: string[]
231
+ /** Free-text evidence note (discordant replicates, mechanism summary). */
232
+ note?: string
233
+ }
234
+
235
+ /** A seed materialized against the loops repo: the parent's full diff. */
236
+ export interface ParetoParentContext extends ParetoParentSeed {
237
+ diff: string
238
+ }
239
+
240
+ export const PARENT_DIFF_MAX_CHARS = 60_000
241
+
242
+ /** Materialize parent seeds: verify each commit exists in `loopsRepo` and
243
+ * capture its full diff (`git show`). Fails loud on a missing commit — a
244
+ * silently absent parent would turn the merge seat into a no-op. */
245
+ export async function materializeParetoParents(
246
+ loopsRepo: string,
247
+ seeds: ParetoParentSeed[],
248
+ maxDiffChars = PARENT_DIFF_MAX_CHARS,
249
+ ): Promise<ParetoParentContext[]> {
250
+ const parents: ParetoParentContext[] = []
251
+ for (const seed of seeds) {
252
+ const exists = await run('git', ['-C', loopsRepo, 'cat-file', '-e', `${seed.commit}^{commit}`])
253
+ if (exists.code !== 0) {
254
+ throw new Error(`pareto parent ${seed.label}: commit ${seed.commit} not found in ${loopsRepo}`)
255
+ }
256
+ const show = await runOk('git', ['-C', loopsRepo, 'show', '--no-color', seed.commit])
257
+ const diff =
258
+ show.stdout.length > maxDiffChars
259
+ ? `${show.stdout.slice(0, maxDiffChars)}\n[... diff truncated at ${maxDiffChars} chars ...]`
260
+ : show.stdout
261
+ parents.push({ ...seed, diff })
262
+ }
263
+ return parents
264
+ }
265
+
266
+ function parentEvidenceLine(parent: ParetoParentContext): string {
267
+ const resolved = parent.resolvedInstances.length > 0 ? parent.resolvedInstances.join(', ') : 'none'
268
+ return `${parent.label} (${parent.commit.slice(0, 10)}) — resolved: ${resolved}${parent.note ? `; ${parent.note}` : ''}`
269
+ }
270
+
271
+ /** The parents section appended to every NON-merge author's prompt: measured
272
+ * evidence of what worked, never an instruction to copy. */
273
+ export function parentsPromptSection(parents: ParetoParentContext[]): string {
274
+ const lines: string[] = [
275
+ 'PARETO PARENTS — the prior generation’s frontier candidates, measured on the SAME 6-instance set.',
276
+ 'Each beat the baseline on different instances; treat their diffs as measured evidence of what works.',
277
+ 'You may build on either (or both), but your candidate is measured on the full set — do not blindly copy.',
278
+ ]
279
+ for (const parent of parents) {
280
+ lines.push('', `--- parent: ${parentEvidenceLine(parent)}`, parent.diff.trimEnd())
281
+ }
282
+ return lines.join('\n')
283
+ }
284
+
285
+ /** The merge seat's task prompt: the change-space contract + findings context
286
+ * stay (round4BuildPrompt), and the TASK is replaced with an explicit
287
+ * coherent-union merge of the parents' diffs. */
288
+ export function mergeAuthorPrompt(
289
+ args: { report: unknown; findings: Array<Record<string, unknown>> },
290
+ spec: ProposerSpec,
291
+ parents: ParetoParentContext[],
292
+ ): string {
293
+ if (parents.length < 2) {
294
+ throw new Error(`merge proposer ${spec.name}: needs >=2 materialized pareto parents, got ${parents.length}`)
295
+ }
296
+ const lines: string[] = [
297
+ round4BuildPrompt(args),
298
+ '',
299
+ `YOUR TASK (${spec.name} — MERGE SEAT):`,
300
+ 'The prior generation produced the frontier candidates below. Each beat the baseline on DIFFERENT',
301
+ 'instances, so their lessons are complementary. Produce ONE coherent surface that is the UNION of the',
302
+ 'parent diffs:',
303
+ ...parents.map((p) => ` - ${parentEvidenceLine(p)}`),
304
+ '',
305
+ 'Merge rules:',
306
+ '- Apply BOTH parents’ behaviors. Where the diffs touch the same file/section (e.g. the supervisor',
307
+ ' reviewer bullet or the worker self-test rules), resolve the conflict by KEEPING BOTH BEHAVIORS —',
308
+ ' write one merged passage that carries every constraint from each side, never by dropping one side.',
309
+ '- Keep each parent’s mechanical changes intact (code paths, exported helpers, threading) alongside the',
310
+ ' other parent’s prompt/goal-authoring changes.',
311
+ '- Do NOT invent new mechanisms beyond the union; the smallest coherent merge wins.',
312
+ '- The merged surface must typecheck and stay inside the declared change-space above.',
313
+ '',
314
+ 'Parent diffs (full):',
315
+ ]
316
+ for (const parent of parents) {
317
+ lines.push('', `=== parent ${parent.label} (${parent.commit.slice(0, 10)}) ===`, parent.diff.trimEnd())
318
+ }
319
+ return lines.join('\n')
320
+ }
321
+
156
322
  // ---------------------------------------------------------------------------
157
323
  // Diagnosis slicing + prompt lens.
158
324
  // ---------------------------------------------------------------------------
@@ -182,15 +348,37 @@ export function sliceFindings(findings: AnalystFinding[], slice: ProposerSpec['d
182
348
  })
183
349
  }
184
350
 
351
+ /** GEN-5 prompt extensions shared by every seat (merge included): the
352
+ * MAP+TOOLBOX+PERMISSION briefing (briefing.mts) and the activation-predicate
353
+ * deliverable contract (activation.mts). */
354
+ export interface Gen5PromptExtras {
355
+ briefing?: BriefingContext
356
+ activationGate?: boolean
357
+ }
358
+
359
+ function appendGen5Sections(prompt: string, extras: Gen5PromptExtras): string {
360
+ let out = prompt
361
+ if (extras.briefing) out = `${out}\n\n${briefingPromptSection(extras.briefing)}`
362
+ if (extras.activationGate) out = `${out}\n\n${activationPredicateInstruction()}`
363
+ return out
364
+ }
365
+
185
366
  /** The proposer's task prompt: the shared round prompt plus this proposer's
186
- * authoring lens (appended so the protocol change-space text stays intact). */
367
+ * authoring lens (appended so the protocol change-space text stays intact),
368
+ * plus the gen-4 Pareto-parents section when parents are seeded, plus the
369
+ * gen-5 briefing/activation sections when configured. A merge-seat spec gets
370
+ * the dedicated merge prompt instead (gen-5 sections still apply). */
187
371
  export function proposerBuildPrompt(
188
372
  args: { report: unknown; findings: Array<Record<string, unknown>> },
189
373
  spec: ProposerSpec,
374
+ parents: ParetoParentContext[] = [],
375
+ extras: Gen5PromptExtras = {},
190
376
  ): string {
191
- const base = round4BuildPrompt(args)
192
- if (!spec.lens) return base
193
- return `${base}\n\nYOUR AUTHORING LENS (${spec.name}):\n${spec.lens}`
377
+ if (spec.merge) return appendGen5Sections(mergeAuthorPrompt(args, spec, parents), extras)
378
+ let prompt = round4BuildPrompt(args)
379
+ if (spec.lens) prompt = `${prompt}\n\nYOUR AUTHORING LENS (${spec.name}):\n${spec.lens}`
380
+ if (parents.length > 0) prompt = `${prompt}\n\n${parentsPromptSection(parents)}`
381
+ return appendGen5Sections(prompt, extras)
194
382
  }
195
383
 
196
384
  // ---------------------------------------------------------------------------
@@ -292,6 +480,21 @@ export type AuthorFn = (
292
480
  export interface FanOutDeps {
293
481
  author?: AuthorFn
294
482
  smokeRunner?: SmokeRunner
483
+ /** GEN-4: materialized Pareto parents seeded into every author's prompt (and
484
+ * the merge seat's explicit merge input). Empty/omitted = gen-3 behavior. */
485
+ parents?: ParetoParentContext[]
486
+ /** GEN-5: MAP+TOOLBOX briefing context threaded into every author prompt. */
487
+ briefing?: BriefingContext
488
+ /** GEN-6: the resolved PUBLIC smoke instance — the GEPA seat's inner
489
+ * evaluator target. Required (with `smokeRunner`) when a gepa seat is
490
+ * configured and no custom `author` is injected. */
491
+ smokeInstanceId?: string
492
+ /** GEN-6: the gen-5 score split; private instance ids never reach the GEPA
493
+ * bridge (gepa-seat.mts asserts, fail-closed). Null/omitted = no split. */
494
+ scoreSplit?: Pick<ScoreSplit, 'privateInstances'> | null
495
+ /** GEN-6 test seam: the adapter factory (default: checked dynamic import of
496
+ * agent-eval's gepaOptimizationMethod, loud when the install predates it). */
497
+ gepaMethodFactory?: GepaMethodFactory
295
498
  log?: (msg: string) => void
296
499
  }
297
500
 
@@ -306,23 +509,51 @@ function defaultAuthor(config: OuterLoopConfig, deps: FanOutDeps): AuthorFn {
306
509
  // its own profile, lens prompt, and shot-receipt home.
307
510
  const inners = new Map<string, CandidateGenerator>()
308
511
  const ledgers = new Map<string, { ledger?: CostLedgerHandle; phase?: string }>()
512
+ // GEN-6: the GEPA seat authors through the agent-eval adapter, not a
513
+ // harness CLI. Its inner evaluator is the SAME injected smoke runner the
514
+ // pre-filter uses (presence enforced at generator construction).
515
+ let gepaAuthor: AuthorFn | undefined
309
516
  return (proposer, args) => {
517
+ if (isGepaSeat(proposer)) {
518
+ gepaAuthor ??= gepaSeatAuthor(config, {
519
+ smokeRunner: deps.smokeRunner!,
520
+ smokeInstanceId: deps.smokeInstanceId!,
521
+ scoreSplit: deps.scoreSplit ?? null,
522
+ ...(deps.gepaMethodFactory ? { methodFactory: deps.gepaMethodFactory } : {}),
523
+ ...(deps.log ? { log: deps.log } : {}),
524
+ })
525
+ return gepaAuthor(proposer, args)
526
+ }
527
+ const harness = proposer.harness
528
+ if (harness === undefined) {
529
+ throw new Error(`proposer ${proposer.name}: harness is required for a non-engine seat`)
530
+ }
310
531
  let inner = inners.get(proposer.name)
311
532
  const slot = ledgers.get(proposer.name) ?? {}
312
533
  ledgers.set(proposer.name, slot)
313
534
  if (!inner) {
314
- const profile = loadAuthorProfile(proposer)
535
+ // GEN-4: the resolved profile carries the spec's pinned model
536
+ // (`model.default` → the harness CLI's `-m` flag).
537
+ const profile = resolveAuthorProfile(proposer)
315
538
  inner = agenticGenerator({
316
- harness: proposer.harness,
539
+ harness,
317
540
  ...(profile ? { profile } : {}),
318
541
  timeoutMs: config.proposerTimeoutMs,
319
542
  buildPrompt: (a) =>
320
- proposerBuildPrompt(a as unknown as { report: unknown; findings: Array<Record<string, unknown>> }, proposer),
543
+ proposerBuildPrompt(
544
+ a as unknown as { report: unknown; findings: Array<Record<string, unknown>> },
545
+ proposer,
546
+ deps.parents ?? [],
547
+ {
548
+ ...(deps.briefing ? { briefing: deps.briefing } : {}),
549
+ ...(config.activationGate === true ? { activationGate: true } : {}),
550
+ },
551
+ ),
321
552
  verify: loopsCandidateVerifier(config.loopsRepo),
322
- runHarness: (options) => runLocalHarness({ ...options, env: proposerShotEnv(proposer.harness) }),
553
+ runHarness: (options) => runLocalHarness({ ...options, env: proposerShotEnv(harness) }),
323
554
  onShotCompleted: proposerShotHooks({
324
555
  shotDir: join(config.outDir, 'proposer-shots', sanitize(proposer.name)),
325
- harness: proposer.harness,
556
+ harness,
326
557
  ledger: () => slot.ledger,
327
558
  phase: () => slot.phase,
328
559
  }),
@@ -348,6 +579,30 @@ export function fanOutLoopsGenerator(config: OuterLoopConfig, deps: FanOutDeps =
348
579
  if (proposers.length === 0) throw new Error('fanOutLoopsGenerator: config.proposers is empty')
349
580
  const names = new Set(proposers.map((p) => p.name))
350
581
  if (names.size !== proposers.length) throw new Error('fanOutLoopsGenerator: duplicate proposer names')
582
+ const mergeSeats = proposers.filter((p) => p.merge === true)
583
+ if (mergeSeats.length > 0 && (deps.parents ?? []).length < 2) {
584
+ throw new Error(
585
+ `fanOutLoopsGenerator: merge proposer(s) ${mergeSeats.map((p) => p.name).join(', ')} configured but ` +
586
+ `only ${(deps.parents ?? []).length} pareto parent(s) materialized — a merge seat needs >=2`,
587
+ )
588
+ }
589
+ // GEN-6: engine seats are validated fail-closed at construction, and the
590
+ // default author path requires the pre-filter smoke runner — it IS the GEPA
591
+ // seat's inner evaluator (spec: score = smoke resolve + verify-pass
592
+ // tiebreak). A custom injected `author` owns its own evaluator.
593
+ const gepaSeats = proposers.filter(isGepaSeat)
594
+ for (const seat of gepaSeats) validateGepaSeat(seat)
595
+ for (const seat of proposers) {
596
+ if (!isGepaSeat(seat) && seat.harness === undefined) {
597
+ throw new Error(`fanOutLoopsGenerator: proposer ${seat.name} has neither a harness nor an engine`)
598
+ }
599
+ }
600
+ if (gepaSeats.length > 0 && deps.author === undefined && (deps.smokeRunner === undefined || deps.smokeInstanceId === undefined)) {
601
+ throw new Error(
602
+ `fanOutLoopsGenerator: gepa seat(s) ${gepaSeats.map((p) => p.name).join(', ')} need the pre-filter smoke ` +
603
+ 'cell as their inner evaluator — enable config.prefilter and provide smokeRunner + smokeInstanceId',
604
+ )
605
+ }
351
606
  const author = deps.author ?? defaultAuthor(config, deps)
352
607
  const log = deps.log ?? (() => {})
353
608
  const kills: PrefilterKill[] = []
@@ -410,6 +665,32 @@ export function fanOutLoopsGenerator(config: OuterLoopConfig, deps: FanOutDeps =
410
665
  return { proposer, applied: false, summary: authored.summary, patch: null, kill }
411
666
  }
412
667
 
668
+ // Pre-filter stage A.5 — GEN-5 activation gate: a candidate without
669
+ // a parseable machine-checkable predicate is killed before any
670
+ // evaluation spend (the post-eval quarantine needs it to exist).
671
+ if (config.activationGate === true) {
672
+ const raw = await readFilePromise(join(scratch, ACTIVATION_PREDICATE_RELPATH), 'utf8').catch(() => null)
673
+ const parsed = raw === null ? null : parseActivationPredicate(raw)
674
+ if (parsed === null || !parsed.ok) {
675
+ const kill: PrefilterKill = {
676
+ generation,
677
+ candidateIndex: index,
678
+ proposer: proposer.name,
679
+ harness: proposer.harness,
680
+ stage: 'activation-predicate',
681
+ reason:
682
+ parsed === null
683
+ ? `missing ${ACTIVATION_PREDICATE_RELPATH} (required activation predicate)`
684
+ : `invalid ${ACTIVATION_PREDICATE_RELPATH}: ${parsed.error}`,
685
+ diffSha256,
686
+ patchPath,
687
+ }
688
+ kills.push(kill)
689
+ log(`prefilter KILL gen ${generation} ${proposer.name}: ${kill.reason}`)
690
+ return { proposer, applied: false, summary: authored.summary, patch: null, kill }
691
+ }
692
+ }
693
+
413
694
  // Pre-filter stage B — one smoke cell before any full-arm spend.
414
695
  if (config.prefilter?.enabled && deps.smokeRunner) {
415
696
  const smoke = await deps.smokeRunner({
@@ -445,7 +726,7 @@ export function fanOutLoopsGenerator(config: OuterLoopConfig, deps: FanOutDeps =
445
726
  }
446
727
 
447
728
  return {
448
- kind: `gen3-fanout:${proposers.map((p) => `${p.name}@${p.harness}`).join('+')}`,
729
+ kind: `gen3-fanout:${proposers.map((p) => `${p.name}@${p.engine ? `engine:${p.engine}` : p.harness}${p.model ? `:${p.model}` : ''}${p.merge ? ':merge' : ''}`).join('+')}`,
449
730
  proposesWithoutFindings: true,
450
731
  drainPrefilterKills() {
451
732
  return kills.splice(0, kills.length)
@@ -479,7 +760,7 @@ export function fanOutLoopsGenerator(config: OuterLoopConfig, deps: FanOutDeps =
479
760
  summary: outcome.summary,
480
761
  label: outcome.proposer.name,
481
762
  rationale:
482
- `proposer ${outcome.proposer.name} (${outcome.proposer.harness}` +
763
+ `proposer ${outcome.proposer.name} (${outcome.proposer.engine ? `engine ${outcome.proposer.engine}` : outcome.proposer.harness}` +
483
764
  `${outcome.proposer.profile ? `, profile ${outcome.proposer.profile}` : ''}` +
484
765
  `${outcome.proposer.diagnosisSlice && outcome.proposer.diagnosisSlice !== 'all' ? `, slice ${outcome.proposer.diagnosisSlice}` : ''}` +
485
766
  `): ${outcome.summary || 'authored change'}`,