@tangle-network/agent-bench 0.3.6 → 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 (265) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/HARNESS.md +43 -0
  3. package/dist/adapters.js +24 -24
  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/humaneval.d.ts +10 -1
  16. package/dist/benchmarks/humaneval.js +5 -3
  17. package/dist/benchmarks/nomiracl.js +2 -2
  18. package/dist/benchmarks/open-rag-bench.js +2 -2
  19. package/dist/benchmarks/programbench.js +2 -2
  20. package/dist/benchmarks/ragbench.js +2 -2
  21. package/dist/benchmarks/swe-bench.js +2 -2
  22. package/dist/benchmarks/t2-ragbench.js +2 -2
  23. package/dist/benchmarks/tau-bench-shared.js +2 -2
  24. package/dist/benchmarks/tau2-bench.js +3 -3
  25. package/dist/benchmarks/tau3-banking.js +3 -3
  26. package/dist/benchmarks/terminal-bench.js +2 -2
  27. package/dist/benchmarks/toollm.js +2 -2
  28. package/dist/benchmarks/webarena-verified.js +2 -2
  29. package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
  30. package/dist/{chunk-PPYSEKFM.js → chunk-5H5XV76F.js} +73 -15
  31. package/dist/chunk-5H5XV76F.js.map +1 -0
  32. package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
  33. package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
  34. package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
  35. package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
  36. package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
  37. package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
  38. package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
  39. package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
  40. package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
  41. package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
  42. package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
  43. package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
  44. package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
  45. package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
  46. package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
  47. package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
  48. package/dist/{chunk-5SBJCB6W.js → chunk-V7AEBY6U.js} +22 -22
  49. package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
  50. package/dist/chunk-WSKWVEQB.js.map +1 -0
  51. package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
  52. package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
  53. package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
  54. package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
  55. package/dist/index.js +24 -24
  56. package/package.json +6 -5
  57. package/scripts/run-package-tests.mjs +30 -8
  58. package/scripts/verify-packed-consumer.mjs +1 -1
  59. package/scripts/verify-pier-agent.mts +1 -0
  60. package/src/benchmarks/_harness.ts +20 -2
  61. package/src/benchmarks/humaneval.test.mts +122 -0
  62. package/src/benchmarks/humaneval.ts +100 -27
  63. package/src/david-attribution.mts +78 -0
  64. package/src/david-goliath.mts +149 -0
  65. package/src/hev-improve.mts +25 -6
  66. package/src/humaneval-object-ablation.mts +201 -0
  67. package/src/live-improve-campaign-mbpp.mts +641 -0
  68. package/src/live-improve-campaign.mts +500 -0
  69. package/src/mbpp-structural.mts +12 -7
  70. package/src/quant-arena/README.md +144 -0
  71. package/src/quant-arena/backtest.test.mts +135 -0
  72. package/src/quant-arena/backtest.ts +218 -0
  73. package/src/quant-arena/data.test.mts +44 -0
  74. package/src/quant-arena/data.ts +141 -0
  75. package/src/quant-arena/driver.test.mts +253 -0
  76. package/src/quant-arena/driver.ts +219 -0
  77. package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
  78. package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
  79. package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
  80. package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
  81. package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
  82. package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
  83. package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
  84. package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
  85. package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
  86. package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
  87. package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
  88. package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
  89. package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
  90. package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
  91. package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
  92. package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
  93. package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
  94. package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
  95. package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
  96. package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
  97. package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
  98. package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
  99. package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
  100. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
  101. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
  102. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
  103. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
  104. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
  105. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
  106. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
  107. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
  108. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
  109. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
  110. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
  111. package/src/quant-arena/holdout-certify.mts +206 -0
  112. package/src/quant-arena/holdout-certify.test.mts +82 -0
  113. package/src/quant-arena/leak-audit.test.mts +79 -0
  114. package/src/quant-arena/leak-audit.ts +95 -0
  115. package/src/quant-arena/make-fixtures.mts +161 -0
  116. package/src/quant-arena/multiplicity.test.mts +68 -0
  117. package/src/quant-arena/multiplicity.ts +87 -0
  118. package/src/quant-arena/nautilus-certify.ts +31 -0
  119. package/src/quant-arena/oms.ts +90 -0
  120. package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
  121. package/src/quant-arena/python/pyproject.toml +8 -0
  122. package/src/quant-arena/python/uv.lock +1297 -0
  123. package/src/quant-arena/python/vbt-worker.py +192 -0
  124. package/src/quant-arena/quant-loop.mts +813 -0
  125. package/src/quant-arena/quant-loop.test.mts +75 -0
  126. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
  127. package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
  128. package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
  129. package/src/quant-arena/types.ts +133 -0
  130. package/src/quant-arena/vbt-client.ts +321 -0
  131. package/src/quant-arena/vbt-parity.test.mts +183 -0
  132. package/src/quant-arena/windows.test.mts +45 -0
  133. package/src/quant-arena/windows.ts +54 -0
  134. package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
  135. package/src/rollout-ledger/backfill-swe-arena.test.mts +338 -0
  136. package/src/rollout-ledger/settle-capture.mts +442 -0
  137. package/src/rollout-ledger/settle-capture.test.mts +270 -0
  138. package/src/stream-observe.py +45 -0
  139. package/src/stream-observe.tpl.html +247 -0
  140. package/src/supervisor-arena.mts +816 -0
  141. package/src/swe-arena/activation.mts +228 -0
  142. package/src/swe-arena/activation.test.mts +303 -0
  143. package/src/swe-arena/analyze.ts +211 -0
  144. package/src/swe-arena/arms.ts +804 -0
  145. package/src/swe-arena/bootstrap-meta.mts +188 -0
  146. package/src/swe-arena/bootstrap-meta.test.mts +51 -0
  147. package/src/swe-arena/briefing.mts +217 -0
  148. package/src/swe-arena/briefing.test.mts +178 -0
  149. package/src/swe-arena/calibrate.ts +217 -0
  150. package/src/swe-arena/capabilities.mts +76 -0
  151. package/src/swe-arena/capabilities.test.mts +57 -0
  152. package/src/swe-arena/capacity.ts +194 -0
  153. package/src/swe-arena/cell-evidence.mts +437 -0
  154. package/src/swe-arena/cell-evidence.test.mts +248 -0
  155. package/src/swe-arena/diagnosis-ensemble.test.mts +210 -0
  156. package/src/swe-arena/diagnosis-ensemble.ts +520 -0
  157. package/src/swe-arena/execution.test.mts +1170 -0
  158. package/src/swe-arena/factory-command-container.ts +284 -0
  159. package/src/swe-arena/factory-judge-child.mts +228 -0
  160. package/src/swe-arena/factory.test.mts +643 -0
  161. package/src/swe-arena/fixtures/analyze.py +80 -0
  162. package/src/swe-arena/fixtures/excludes.txt +8 -0
  163. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
  164. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
  165. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
  166. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
  167. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
  168. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
  169. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
  170. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
  171. package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
  172. package/src/swe-arena/fixtures/gen1-salvage/README.md +45 -0
  173. package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +116 -0
  174. package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +293 -0
  175. package/src/swe-arena/fixtures/holdout-preregister.log +12 -0
  176. package/src/swe-arena/fixtures/holdout.json +44 -0
  177. package/src/swe-arena/fixtures/instances.json +146 -0
  178. package/src/swe-arena/fixtures/ledger.jsonl +12 -0
  179. package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +36 -0
  180. package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +33 -0
  181. package/src/swe-arena/fixtures/rejudge.jsonl +15 -0
  182. package/src/swe-arena/fixtures/rematch.jsonl +3 -0
  183. package/src/swe-arena/fixtures/rematch2.jsonl +3 -0
  184. package/src/swe-arena/fixtures/rematch3.jsonl +3 -0
  185. package/src/swe-arena/fixtures/run-report/README.md +43 -0
  186. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
  187. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
  188. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
  189. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
  190. package/src/swe-arena/fixtures/sup-journal-true.json +19 -0
  191. package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +48 -0
  192. package/src/swe-arena/fixtures/verify/django__django-11532.sh +50 -0
  193. package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +76 -0
  194. package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +44 -0
  195. package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +32 -0
  196. package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +51 -0
  197. package/src/swe-arena/fixtures/worker-tokens.json +42 -0
  198. package/src/swe-arena/fixtures.ts +237 -0
  199. package/src/swe-arena/gepa-seat.mts +583 -0
  200. package/src/swe-arena/gepa-seat.test.mts +635 -0
  201. package/src/swe-arena/holdout-certify.mts +408 -0
  202. package/src/swe-arena/holdout-certify.test.mts +160 -0
  203. package/src/swe-arena/judge-child.mts +37 -0
  204. package/src/swe-arena/ledger-orphans.mts +77 -0
  205. package/src/swe-arena/ledger-orphans.test.mts +147 -0
  206. package/src/swe-arena/lineage-record.mts +164 -0
  207. package/src/swe-arena/lineage-record.test.mts +115 -0
  208. package/src/swe-arena/manifest.mts +293 -0
  209. package/src/swe-arena/manifest.test.mts +169 -0
  210. package/src/swe-arena/materialize.ts +142 -0
  211. package/src/swe-arena/outer-loop.mts +2795 -0
  212. package/src/swe-arena/outer-loop.test.mts +696 -0
  213. package/src/swe-arena/parity.test.mts +87 -0
  214. package/src/swe-arena/premeasured-from-cells.mts +281 -0
  215. package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
  216. package/src/swe-arena/proc.test.mts +174 -0
  217. package/src/swe-arena/proc.ts +260 -0
  218. package/src/swe-arena/profiles/default-author.profile.json +4 -0
  219. package/src/swe-arena/proposer-fanout.mts +770 -0
  220. package/src/swe-arena/proposer-fanout.test.mts +619 -0
  221. package/src/swe-arena/proposer-provenance.mts +177 -0
  222. package/src/swe-arena/proposer-provenance.test.mts +106 -0
  223. package/src/swe-arena/reconcile.ts +0 -0
  224. package/src/swe-arena/replay.mts +183 -0
  225. package/src/swe-arena/replay.test.mts +300 -0
  226. package/src/swe-arena/run-experiment.mts +727 -0
  227. package/src/swe-arena/run-report.mts +75 -0
  228. package/src/swe-arena/run-supervisor.mjs +297 -0
  229. package/src/swe-arena/run-supervisor.test.mts +500 -0
  230. package/src/swe-arena/score-split.mts +140 -0
  231. package/src/swe-arena/score-split.test.mts +123 -0
  232. package/src/swe-arena/serialized-judge.ts +414 -0
  233. package/src/swe-arena/types.ts +218 -0
  234. package/src/swe-code-improve.mts +328 -0
  235. package/src/swe-emit-patch.mts +104 -0
  236. package/src/swe-improve.mts +232 -0
  237. package/src/swe-jail.ts +2 -2
  238. package/src/swe-local-proof.mts +169 -0
  239. package/src/swe-repro-calibrate.mts +446 -0
  240. package/src/swe-stream.mts +1497 -0
  241. package/src/swe-structural.mts +245 -837
  242. package/dist/chunk-LRRD7NAG.js.map +0 -1
  243. package/dist/chunk-PPYSEKFM.js.map +0 -1
  244. /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
  245. /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
  246. /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
  247. /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
  248. /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
  249. /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
  250. /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
  251. /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
  252. /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
  253. /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
  254. /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
  255. /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
  256. /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
  257. /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
  258. /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
  259. /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
  260. /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
  261. /package/dist/{chunk-5SBJCB6W.js.map → chunk-V7AEBY6U.js.map} +0 -0
  262. /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
  263. /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
  264. /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
  265. /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
@@ -0,0 +1,583 @@
1
+ /**
2
+ * GEN-6 GEPA proposer seat — agent-eval's external-GEPA adapter
3
+ * (`gepaOptimizationMethod`, tangle-network/agent-eval PRs #408/#409,
4
+ * main@58a28aa) wired as ONE seat in the swe-arena proposer fan-out.
5
+ *
6
+ * Two-tier evaluator, the critical shape:
7
+ *
8
+ * INNER (what GEPA's own loop calls, many times, budget-capped): the
9
+ * candidate is ONE change-space file's content as a string. Each inner call
10
+ * materializes the candidate string into the seat's scratch loops worktree
11
+ * (the rest of the repo stays at the incumbent commit) and runs the EXISTING
12
+ * pre-filter smoke cell — one PUBLIC instance, the cheap path — through the
13
+ * injected `SmokeRunner`. Score = smoke resolve (1/0) + verify-pass fraction
14
+ * as a bounded tiebreak. Inner calls are capped by `maxMetricCalls`
15
+ * (default 10; each smoke costs minutes of arm time).
16
+ *
17
+ * OUTER: GEPA's best candidate is written back to the surface file in the
18
+ * scratch worktree and the seat returns `applied: true` — from there the
19
+ * fan-out treats it EXACTLY like any other author's work: change-space
20
+ * check, activation-predicate gate, smoke pre-filter, then the full exam,
21
+ * with staircase label = the seat name (`gepa-author`).
22
+ *
23
+ * DATA BOUNDARIES (both fail-closed):
24
+ * - PUBLIC ONLY crosses the bridge: the only scenario ids serialized to the
25
+ * GEPA process name the public smoke instance; `assertNoPrivateLeak`
26
+ * re-checks every string headed to the bridge against the score split.
27
+ * - Holdout/final cases NEVER cross: the adapter's own API has no test-set
28
+ * field (`GepaBridgeInput` in agent-eval src/campaign/gepa-optimization-
29
+ * method.ts — "The final comparison cases are not accepted by this API and
30
+ * cannot be serialized here"), and its Python side hard-rejects one
31
+ * (`gepa_bridge.py` `_validate_input`: `if "testSet" in value ... raise`).
32
+ * This module never mentions holdout instances to begin with.
33
+ *
34
+ * RUNTIME SEAMS (both fail LOUD at provenance time, t=0, mirroring the codex
35
+ * seat's auth check — a dead seat cannot be silently skipped mid-run):
36
+ * - Node: the installed @tangle-network/agent-eval must export
37
+ * `gepaOptimizationMethod` (0.123.x predates it) — `loadGepaMethodFactory`
38
+ * throws with the exact upgrade instruction otherwise.
39
+ * - Python: `agent_eval_rpc.gepa_bridge` + a GEPA build with
40
+ * `optimize_anything`/`OptimizeAnythingConfig` must import —
41
+ * `probeGepaRuntime` throws with the pip install instruction otherwise.
42
+ */
43
+
44
+ import { createHash } from 'node:crypto'
45
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
46
+ import { dirname, join } from 'node:path'
47
+ import type {
48
+ DispatchContext,
49
+ JudgeConfig,
50
+ MutableSurface,
51
+ OptimizationMethod,
52
+ OptimizationMethodInput,
53
+ Scenario,
54
+ } from '@tangle-network/agent-eval/campaign'
55
+ import { ACTIVATION_PREDICATE_RELPATH, type ActivationPredicate } from './activation.mts'
56
+ import { changeSpaceViolations, type OuterLoopConfig } from './outer-loop.mts'
57
+ import type { AuthorFn, ProposerSpec, SmokeRunner, SmokeVerdict } from './proposer-fanout.mts'
58
+ import type { ScoreSplit } from './score-split.mts'
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Spec.
62
+ // ---------------------------------------------------------------------------
63
+
64
+ export const GEPA_ENGINES = ['gepa', 'omni'] as const
65
+ export type GepaEngineName = (typeof GEPA_ENGINES)[number]
66
+
67
+ export const DEFAULT_MAX_METRIC_CALLS = 10
68
+ export const DEFAULT_MAX_PROPOSER_COST_USD = 10
69
+ /** Omni = 3 bounded explore runs + 1 continuation (GEPA's published shape). */
70
+ export const OMNI_RUN_COUNT = 4
71
+
72
+ /** A `ProposerSpec` whose `engine` marks it as a GEPA seat. */
73
+ export type GepaSeatSpec = ProposerSpec & { engine: GepaEngineName; surface: string }
74
+
75
+ export function isGepaSeat(spec: ProposerSpec): spec is GepaSeatSpec {
76
+ return spec.engine !== undefined
77
+ }
78
+
79
+ /** Fail-closed spec validation, run at generator construction. A GEPA seat is
80
+ * an ENGINE invocation: harness/profile/model/merge belong to CLI-authored
81
+ * seats and are rejected here rather than silently ignored. */
82
+ export function validateGepaSeat(spec: ProposerSpec): asserts spec is GepaSeatSpec {
83
+ const label = `gepa seat '${spec.name}'`
84
+ if (spec.engine === undefined || !GEPA_ENGINES.includes(spec.engine)) {
85
+ throw new Error(`${label}: engine must be one of ${GEPA_ENGINES.join('|')}, got ${JSON.stringify(spec.engine)}`)
86
+ }
87
+ if (typeof spec.surface !== 'string' || spec.surface.length === 0) {
88
+ throw new Error(`${label}: surface is required — the ONE repo-relative file GEPA optimizes as a string`)
89
+ }
90
+ const violations = changeSpaceViolations([spec.surface])
91
+ if (violations.length > 0) {
92
+ throw new Error(`${label}: surface ${JSON.stringify(spec.surface)} is outside the declared change-space`)
93
+ }
94
+ for (const field of ['harness', 'profile', 'model', 'merge', 'lens', 'diagnosisSlice'] as const) {
95
+ if (spec[field] !== undefined) {
96
+ throw new Error(`${label}: field '${field}' belongs to harness-authored seats and must be unset on an engine seat`)
97
+ }
98
+ }
99
+ const calls = spec.maxMetricCalls ?? DEFAULT_MAX_METRIC_CALLS
100
+ if (!Number.isSafeInteger(calls) || calls <= 0) {
101
+ throw new Error(`${label}: maxMetricCalls must be a positive integer, got ${JSON.stringify(spec.maxMetricCalls)}`)
102
+ }
103
+ if (spec.engine === 'omni' && calls < OMNI_RUN_COUNT) {
104
+ throw new Error(`${label}: engine 'omni' runs ${OMNI_RUN_COUNT} bounded engine runs and needs maxMetricCalls >= ${OMNI_RUN_COUNT}, got ${calls}`)
105
+ }
106
+ const cost = spec.maxProposerCostUsd ?? DEFAULT_MAX_PROPOSER_COST_USD
107
+ if (!Number.isFinite(cost) || cost <= 0) {
108
+ throw new Error(`${label}: maxProposerCostUsd must be a positive finite number, got ${JSON.stringify(spec.maxProposerCostUsd)}`)
109
+ }
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Recipe — the adapter's own shape, mirrored structurally (the installed
114
+ // agent-eval may predate the export; see loadGepaMethodFactory).
115
+ // ---------------------------------------------------------------------------
116
+
117
+ export interface GepaEngineRun {
118
+ engine: string
119
+ maxEvaluations: number
120
+ maxProposerCostUsd: number
121
+ engineConfig?: Record<string, unknown>
122
+ }
123
+
124
+ export type GepaOptimizationRecipe =
125
+ | { kind: 'engine'; run: GepaEngineRun }
126
+ | { kind: 'best-of-then-continue'; explore: readonly GepaEngineRun[]; continueWith: GepaEngineRun }
127
+
128
+ /** Build the bounded recipe for a seat. The TOTAL inner-evaluation budget is
129
+ * exactly `maxMetricCalls` — the adapter's local callback enforces the sum
130
+ * of per-run limits, and the seat's own dispatch wrapper re-enforces it. */
131
+ export function recipeForSeat(spec: GepaSeatSpec): GepaOptimizationRecipe {
132
+ const calls = spec.maxMetricCalls ?? DEFAULT_MAX_METRIC_CALLS
133
+ const cost = spec.maxProposerCostUsd ?? DEFAULT_MAX_PROPOSER_COST_USD
134
+ if (spec.engine === 'gepa') {
135
+ return { kind: 'engine', run: { engine: 'gepa', maxEvaluations: calls, maxProposerCostUsd: cost } }
136
+ }
137
+ // Omni: explore {gepa, autoresearch, meta_harness} then continue with gepa,
138
+ // splitting the call budget so the four bounded runs sum to `calls`.
139
+ const perExplore = Math.max(1, Math.floor(calls / OMNI_RUN_COUNT))
140
+ const continueCalls = calls - 3 * perExplore
141
+ const perRunCost = cost / OMNI_RUN_COUNT
142
+ const explore = ['gepa', 'autoresearch', 'meta_harness'].map((engine) => ({
143
+ engine,
144
+ maxEvaluations: perExplore,
145
+ maxProposerCostUsd: perRunCost,
146
+ }))
147
+ return {
148
+ kind: 'best-of-then-continue',
149
+ explore,
150
+ continueWith: { engine: 'gepa', maxEvaluations: continueCalls, maxProposerCostUsd: perRunCost },
151
+ }
152
+ }
153
+
154
+ export function recipeEvaluationBudget(recipe: GepaOptimizationRecipe): number {
155
+ const runs = recipe.kind === 'engine' ? [recipe.run] : [...recipe.explore, recipe.continueWith]
156
+ return runs.reduce((sum, run) => sum + run.maxEvaluations, 0)
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Public-only bridge examples.
161
+ // ---------------------------------------------------------------------------
162
+
163
+ export interface GepaSeatScenario extends Scenario {
164
+ /** The PUBLIC smoke instance this scenario dispatches to. */
165
+ smokeIid: string
166
+ }
167
+
168
+ /** Throws when any private instance id appears in text headed to the bridge. */
169
+ export function assertNoPrivateLeak(
170
+ text: string,
171
+ split: Pick<ScoreSplit, 'privateInstances'> | null,
172
+ what: string,
173
+ ): void {
174
+ if (split === null) return
175
+ const leaked = split.privateInstances.filter((iid) => text.includes(iid))
176
+ if (leaked.length > 0) {
177
+ throw new Error(`gepa seat: ${what} would leak private instance id(s) [${leaked.join(', ')}] to the GEPA bridge`)
178
+ }
179
+ }
180
+
181
+ /** The ONLY scenarios the bridge ever sees: the public smoke instance as the
182
+ * train example plus a distinct-id alias as the selection example (the
183
+ * adapter requires disjoint train/selection ids; both dispatch to the same
184
+ * smoke cell). Fails loud when the smoke instance is private. */
185
+ export function gepaBridgeScenarios(
186
+ smokeIid: string,
187
+ split: Pick<ScoreSplit, 'privateInstances'> | null,
188
+ ): { train: GepaSeatScenario[]; selection: GepaSeatScenario[] } {
189
+ if (split !== null && split.privateInstances.includes(smokeIid)) {
190
+ throw new Error(
191
+ `gepa seat: smoke instance ${smokeIid} is PRIVATE under the score split — private ids never cross the bridge`,
192
+ )
193
+ }
194
+ assertNoPrivateLeak(smokeIid, split, `smoke instance id '${smokeIid}'`)
195
+ return {
196
+ train: [{ id: smokeIid, kind: 'swe-smoke', smokeIid }],
197
+ selection: [{ id: `${smokeIid}::selection`, kind: 'swe-smoke', smokeIid }],
198
+ }
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Inner score.
203
+ // ---------------------------------------------------------------------------
204
+
205
+ /** Resolve dominates; verify-pass is a bounded tiebreak that can never beat a
206
+ * resolve (0.25 < 1). Range {0, 0.25, 1, 1.25}. */
207
+ export function innerSmokeComposite(verdict: Pick<SmokeVerdict, 'resolved' | 'verifyPass'>): number {
208
+ return (verdict.resolved === true ? 1 : 0) + (verdict.verifyPass === true ? 0.25 : 0)
209
+ }
210
+
211
+ export function innerSmokeJudge(): JudgeConfig<SmokeVerdict, GepaSeatScenario> {
212
+ return {
213
+ name: 'gepa-inner-smoke',
214
+ judgeVersion: 'gepa-inner-smoke.v1',
215
+ dimensions: [
216
+ { key: 'resolved', description: 'Official SWE-bench judge verdict for the smoke cell (1 resolved / 0 not).' },
217
+ { key: 'verifyPass', description: 'Committed verify fixture passed for the smoke cell (tiebreak).' },
218
+ ],
219
+ score: ({ artifact }) => ({
220
+ dimensions: {
221
+ resolved: artifact.resolved === true ? 1 : 0,
222
+ verifyPass: artifact.verifyPass === true ? 1 : 0,
223
+ },
224
+ composite: innerSmokeComposite(artifact),
225
+ notes: artifact.reason,
226
+ }),
227
+ }
228
+ }
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // Runtime seams — Node adapter export + Python bridge, both loud.
232
+ // ---------------------------------------------------------------------------
233
+
234
+ export const GEPA_ADAPTER_UPGRADE_HINT =
235
+ "the installed @tangle-network/agent-eval does not export gepaOptimizationMethod — " +
236
+ 'upgrade to a release containing tangle-network/agent-eval PRs #408/#409 (merged at main@58a28aa; ' +
237
+ 'first release after 0.123.5), then reinstall bench deps'
238
+
239
+ export const GEPA_PYTHON_INSTALL_HINT =
240
+ "install the optional Python bridge: pip install 'agent-eval-rpc[gepa]' " +
241
+ '(the extra pins the GEPA source commit providing optimize_anything/OptimizeAnythingConfig; ' +
242
+ 'published gepa<=0.1.4 does not contain the multi-engine API — see agent-eval docs/campaign-proposers.md)'
243
+
244
+ /** Adapter config, mirrored structurally from agent-eval's
245
+ * `GepaOptimizationMethodConfig` (src/campaign/gepa-optimization-method.ts). */
246
+ export interface GepaMethodConfig {
247
+ name?: string
248
+ recipe: GepaOptimizationRecipe
249
+ objective: string
250
+ background?: string
251
+ maxCandidateChars?: number
252
+ timeoutMs?: number
253
+ describeScenario?: (scenario: GepaSeatScenario) => unknown
254
+ runner?: { command?: string; args?: readonly string[]; cwd?: string; env?: NodeJS.ProcessEnv }
255
+ }
256
+
257
+ export type GepaMethodFactory = (
258
+ config: GepaMethodConfig,
259
+ ) => OptimizationMethod<GepaSeatScenario, SmokeVerdict>
260
+
261
+ export type CampaignModuleImport = () => Promise<Record<string, unknown>>
262
+
263
+ const defaultImportCampaign: CampaignModuleImport = () =>
264
+ import('@tangle-network/agent-eval/campaign') as Promise<Record<string, unknown>>
265
+
266
+ /** Resolve the adapter factory from the installed agent-eval, or throw the
267
+ * exact upgrade instruction. Checked at provenance time (t=0) AND at author
268
+ * time, so a stale install can never silently skip the seat. */
269
+ export async function loadGepaMethodFactory(
270
+ importCampaign: CampaignModuleImport = defaultImportCampaign,
271
+ ): Promise<GepaMethodFactory> {
272
+ const mod = await importCampaign()
273
+ const factory = mod['gepaOptimizationMethod']
274
+ if (typeof factory !== 'function') {
275
+ throw new Error(`gepa seat: ${GEPA_ADAPTER_UPGRADE_HINT}`)
276
+ }
277
+ return factory as GepaMethodFactory
278
+ }
279
+
280
+ export type ProbeExec = (
281
+ command: string,
282
+ args: string[],
283
+ ) => Promise<{ code: number | null; stdout: string; stderr: string }>
284
+
285
+ export interface GepaRuntimeProbe {
286
+ pythonVersion: string
287
+ gepaVersion: string
288
+ }
289
+
290
+ export const DEFAULT_GEPA_PYTHON = 'python3'
291
+
292
+ /** Prove the Python side of the bridge can run, or throw install
293
+ * instructions. Mirrors the codex seat's login-status gate: run at t=0 so a
294
+ * dead seat fails the launch, never a mid-run candidate slot. */
295
+ export async function probeGepaRuntime(python: string, exec: ProbeExec, seatName: string): Promise<GepaRuntimeProbe> {
296
+ const version = await exec(python, ['--version'])
297
+ if (version.code !== 0) {
298
+ throw new Error(
299
+ `gepa seat '${seatName}': '${python} --version' failed (rc=${version.code}) — ${GEPA_PYTHON_INSTALL_HINT}`,
300
+ )
301
+ }
302
+ const bridge = await exec(python, ['-c', 'import agent_eval_rpc.gepa_bridge'])
303
+ if (bridge.code !== 0) {
304
+ throw new Error(
305
+ `gepa seat '${seatName}': GEPA Python runtime is not installed ` +
306
+ `(python=${python}; 'import agent_eval_rpc.gepa_bridge' failed: ${bridge.stderr.trim().slice(0, 300)}). ` +
307
+ GEPA_PYTHON_INSTALL_HINT,
308
+ )
309
+ }
310
+ const gepa = await exec(python, [
311
+ '-c',
312
+ "from gepa.optimize_anything import optimize_anything, OptimizeAnythingConfig; " +
313
+ "import gepa; print(getattr(gepa, '__version__', 'source'))",
314
+ ])
315
+ if (gepa.code !== 0) {
316
+ throw new Error(
317
+ `gepa seat '${seatName}': installed gepa lacks the multi-engine optimize_anything API ` +
318
+ `(${gepa.stderr.trim().slice(0, 300)}). ` +
319
+ GEPA_PYTHON_INSTALL_HINT,
320
+ )
321
+ }
322
+ return { pythonVersion: (version.stdout + version.stderr).trim(), gepaVersion: gepa.stdout.trim() }
323
+ }
324
+
325
+ // ---------------------------------------------------------------------------
326
+ // Inner-run provenance.
327
+ // ---------------------------------------------------------------------------
328
+
329
+ export interface GepaInnerCall {
330
+ call: number
331
+ scenarioId: string
332
+ smokeIid: string
333
+ candidateSha256: string
334
+ composite: number
335
+ resolved: boolean | null
336
+ verifyPass: boolean | null
337
+ pass: boolean
338
+ wallS: number
339
+ }
340
+
341
+ export interface GepaSeatInnerRun {
342
+ seat: string
343
+ engine: GepaEngineName
344
+ surface: string
345
+ generation: number
346
+ budget: number
347
+ innerCallCount: number
348
+ innerScores: GepaInnerCall[]
349
+ bestComposite: number | null
350
+ adapterReportedCostUsd: number | null
351
+ adapterCostAccountingComplete: boolean
352
+ durationMs: number
353
+ }
354
+
355
+ export const PROPOSER_PROVENANCE_FILENAME = 'proposer-provenance.json'
356
+
357
+ /** Merge one seat run's inner-call record into `proposer-provenance.json`
358
+ * under `gepaInnerRuns` (additive; the t=0 capture record is preserved). */
359
+ export async function recordGepaSeatInnerRun(outDir: string, run: GepaSeatInnerRun): Promise<void> {
360
+ const path = join(outDir, PROPOSER_PROVENANCE_FILENAME)
361
+ let record: Record<string, unknown> = {}
362
+ try {
363
+ record = JSON.parse(await readFile(path, 'utf8')) as Record<string, unknown>
364
+ } catch {
365
+ // No capture record yet (unit-test or crash-before-write): still persist.
366
+ }
367
+ const runs = Array.isArray(record.gepaInnerRuns) ? (record.gepaInnerRuns as unknown[]) : []
368
+ runs.push(run)
369
+ record.gepaInnerRuns = runs
370
+ await mkdir(dirname(path), { recursive: true })
371
+ await writeFile(path, JSON.stringify(record, null, 2))
372
+ }
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // Mechanical activation predicate (gen-5 activation gate).
376
+ // ---------------------------------------------------------------------------
377
+
378
+ const escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
379
+
380
+ export const MIN_PREDICATE_LINE_CHARS = 12
381
+
382
+ /** Derive a machine-checkable predicate from the surface change: the longest
383
+ * ADDED line must render in the candidate's own run artifacts (for a prompt
384
+ * surface, the changed text appearing in composed prompts IS the mechanism
385
+ * firing). Returns null when no added line is distinctive enough — the
386
+ * caller fails the candidate loud instead of shipping an unverifiable one. */
387
+ export function mechanicalActivationPredicate(
388
+ seed: string,
389
+ winner: string,
390
+ surface: string,
391
+ ): ActivationPredicate | null {
392
+ const seedLines = new Set(seed.split('\n').map((l) => l.trim()))
393
+ const added = winner
394
+ .split('\n')
395
+ .map((l) => l.trim())
396
+ .filter((l) => l.length >= MIN_PREDICATE_LINE_CHARS && !seedLines.has(l))
397
+ if (added.length === 0) return null
398
+ const line = added.reduce((a, b) => (b.length > a.length ? b : a))
399
+ return {
400
+ version: 'v1',
401
+ description: `gepa-author surface change fired: candidate text from ${surface} appears in run artifacts`,
402
+ kind: 'grep',
403
+ pattern: escapeRegExp(line),
404
+ }
405
+ }
406
+
407
+ // ---------------------------------------------------------------------------
408
+ // The seat author.
409
+ // ---------------------------------------------------------------------------
410
+
411
+ export interface GepaSeatDeps {
412
+ smokeRunner: SmokeRunner
413
+ /** Resolved PUBLIC smoke instance (outer-loop restricts the choice to the
414
+ * split's public set; re-asserted here fail-closed). */
415
+ smokeInstanceId: string
416
+ scoreSplit: Pick<ScoreSplit, 'privateInstances'> | null
417
+ /** Test seam. Default: checked dynamic import of the installed adapter. */
418
+ methodFactory?: GepaMethodFactory
419
+ log?: (msg: string) => void
420
+ }
421
+
422
+ const sha256 = (s: string): string => `sha256:${createHash('sha256').update(s).digest('hex')}`
423
+
424
+ /** Build the seat's `AuthorFn`. The fan-out calls it with the seat's scratch
425
+ * worktree (checked out at the incumbent commit); everything this function
426
+ * leaves in that worktree becomes the candidate diff. */
427
+ export function gepaSeatAuthor(config: OuterLoopConfig, deps: GepaSeatDeps): AuthorFn {
428
+ const log = deps.log ?? (() => {})
429
+ return async (proposer, args) => {
430
+ validateGepaSeat(proposer)
431
+ const spec: GepaSeatSpec = proposer
432
+ const generation = args.generation ?? 0
433
+ const budget = spec.maxMetricCalls ?? DEFAULT_MAX_METRIC_CALLS
434
+ const recipe = recipeForSeat(spec)
435
+ const scenarios = gepaBridgeScenarios(deps.smokeInstanceId, deps.scoreSplit)
436
+ const surfacePath = join(args.worktreePath, spec.surface)
437
+ const seed = await readFile(surfacePath, 'utf8').catch(() => {
438
+ throw new Error(`gepa seat '${spec.name}': surface ${spec.surface} does not exist at the incumbent commit`)
439
+ })
440
+ const runDir = join(config.outDir, 'gepa-seat', `gen${generation}-${spec.name.replace(/[^a-zA-Z0-9_-]/g, '_')}`)
441
+ await mkdir(runDir, { recursive: true })
442
+
443
+ const objective =
444
+ `Improve the supervisor-loop file '${spec.surface}' (returned as the COMPLETE new file content) so the ` +
445
+ 'SWE-bench smoke evaluation scores higher. Score = 1 for an officially resolved instance plus 0.25 when ' +
446
+ 'the verify fixture passes. Keep the file coherent and self-contained; only its content is applied.'
447
+ const background =
448
+ `The candidate string replaces ${spec.surface} in a checkout of the loops supervisor repo; every other ` +
449
+ 'file stays at the incumbent commit. Each evaluation runs one real SWE-bench instance end-to-end and ' +
450
+ 'takes minutes — spend evaluations deliberately.'
451
+ assertNoPrivateLeak(objective + background + JSON.stringify([...scenarios.train, ...scenarios.selection]),
452
+ deps.scoreSplit, 'bridge payload')
453
+
454
+ const innerScores: GepaInnerCall[] = []
455
+ const dispatchWithSurface = async (
456
+ surface: MutableSurface,
457
+ scenario: GepaSeatScenario,
458
+ _ctx: DispatchContext,
459
+ ): Promise<SmokeVerdict> => {
460
+ if (typeof surface !== 'string') {
461
+ throw new Error(`gepa seat '${spec.name}': candidate surface must be a string`)
462
+ }
463
+ if (innerScores.length >= budget) {
464
+ // Defense-in-depth: the adapter's callback enforces the same cap.
465
+ throw new Error(`gepa seat '${spec.name}': inner-call budget ${budget} exhausted`)
466
+ }
467
+ await writeFile(surfacePath, surface)
468
+ const verdict = await deps.smokeRunner({
469
+ scratchPath: args.worktreePath,
470
+ generation,
471
+ proposer: spec,
472
+ ...(args.costLedger ? { costLedger: args.costLedger } : {}),
473
+ })
474
+ if (deps.scoreSplit !== null && deps.scoreSplit.privateInstances.includes(verdict.iid)) {
475
+ throw new Error(
476
+ `gepa seat '${spec.name}': smoke ran PRIVATE instance ${verdict.iid} — refusing to feed its score to the bridge`,
477
+ )
478
+ }
479
+ innerScores.push({
480
+ call: innerScores.length + 1,
481
+ scenarioId: scenario.id,
482
+ smokeIid: verdict.iid,
483
+ candidateSha256: sha256(surface),
484
+ composite: innerSmokeComposite(verdict),
485
+ resolved: verdict.resolved,
486
+ verifyPass: verdict.verifyPass ?? null,
487
+ pass: verdict.pass,
488
+ wallS: verdict.wallS,
489
+ })
490
+ log(
491
+ `gepa seat ${spec.name} inner call ${innerScores.length}/${budget}: ` +
492
+ `composite=${innerSmokeComposite(verdict)} (${verdict.reason})`,
493
+ )
494
+ return verdict
495
+ }
496
+
497
+ const factory = deps.methodFactory ?? (await loadGepaMethodFactory())
498
+ const method = factory({
499
+ name: `gepa-seat:${spec.name}`,
500
+ recipe,
501
+ objective,
502
+ background,
503
+ describeScenario: (scenario) => ({ id: scenario.id }),
504
+ // Ceiling, not expectation: every inner call is a real arm cell.
505
+ timeoutMs: budget * config.dispatchTimeoutMs,
506
+ runner: { command: spec.python ?? DEFAULT_GEPA_PYTHON },
507
+ })
508
+
509
+ const input: OptimizationMethodInput<GepaSeatScenario, SmokeVerdict> = {
510
+ baselineSurface: seed,
511
+ trainScenarios: scenarios.train,
512
+ selectionScenarios: scenarios.selection,
513
+ dispatchWithSurface,
514
+ judges: [innerSmokeJudge()],
515
+ runDir,
516
+ seed: config.round * 1000 + generation,
517
+ runOptions: {
518
+ maxConcurrency: 1,
519
+ dispatchTimeoutMs: config.dispatchTimeoutMs,
520
+ labeledStore: 'off',
521
+ tracing: 'off',
522
+ expectUsage: 'off',
523
+ resumable: false,
524
+ },
525
+ }
526
+
527
+ const started = Date.now()
528
+ const result = await method.optimize(input)
529
+ const winner = result.winnerSurface
530
+ if (typeof winner !== 'string' || winner.trim().length === 0) {
531
+ throw new Error(`gepa seat '${spec.name}': adapter returned a non-string winner surface`)
532
+ }
533
+
534
+ const innerRun: GepaSeatInnerRun = {
535
+ seat: spec.name,
536
+ engine: spec.engine,
537
+ surface: spec.surface,
538
+ generation,
539
+ budget,
540
+ innerCallCount: innerScores.length,
541
+ innerScores,
542
+ bestComposite: innerScores.length > 0 ? Math.max(...innerScores.map((s) => s.composite)) : null,
543
+ adapterReportedCostUsd: result.cost.totalCostUsd,
544
+ adapterCostAccountingComplete: result.cost.accountingComplete,
545
+ durationMs: Date.now() - started,
546
+ }
547
+ await writeFile(join(runDir, 'inner-provenance.json'), JSON.stringify(innerRun, null, 2))
548
+ await recordGepaSeatInnerRun(config.outDir, innerRun)
549
+
550
+ if (winner === seed) {
551
+ // Restore the seed (the last inner call may have left another candidate)
552
+ // and decline the slot — an unchanged surface has no candidate diff.
553
+ await writeFile(surfacePath, seed)
554
+ return {
555
+ applied: false,
556
+ summary: `gepa ${spec.engine}: best candidate equals the seed after ${innerScores.length} inner call(s)`,
557
+ }
558
+ }
559
+
560
+ await writeFile(surfacePath, winner)
561
+ if (config.activationGate === true) {
562
+ const predicate = mechanicalActivationPredicate(seed, winner, spec.surface)
563
+ if (predicate === null) {
564
+ await writeFile(surfacePath, seed)
565
+ return {
566
+ applied: false,
567
+ summary:
568
+ `gepa ${spec.engine}: winner adds no line of >=${MIN_PREDICATE_LINE_CHARS} chars — ` +
569
+ 'cannot derive a machine-checkable activation predicate; candidate declined',
570
+ }
571
+ }
572
+ const predicatePath = join(args.worktreePath, ACTIVATION_PREDICATE_RELPATH)
573
+ await mkdir(dirname(predicatePath), { recursive: true })
574
+ await writeFile(predicatePath, JSON.stringify(predicate, null, 2))
575
+ }
576
+ return {
577
+ applied: true,
578
+ summary:
579
+ `gepa ${spec.engine} optimized ${spec.surface} over ${innerScores.length}/${budget} inner smoke call(s); ` +
580
+ `best inner composite ${innerRun.bestComposite}`,
581
+ }
582
+ }
583
+ }