@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,606 @@
1
+ /**
2
+ * swe-arena → rollout backfill — DOMAIN GLUE ONLY. Bench owns the JOIN
3
+ * (manifest + cached cells + judge.json + result.json workerCwds + proposer
4
+ * shot receipts → harness stores); the `tangle.rollout.v1` schema, line
5
+ * validation, serialization, readers, exporters, and release pipeline are
6
+ * owned by `@tangle-network/agent-eval/rollout` — bench never writes a
7
+ * rollout row through anything but that API.
8
+ *
9
+ * Pure READ over one round's outDir
10
+ * (the same artifact tree `swe-arena/manifest.mts` joins) plus the harness
11
+ * stores, emitting `tangle.rollout.v1` lines with capture:"backfill":
12
+ *
13
+ * - one SUPERVISOR line per campaign cell (instance × rep × candidate):
14
+ * reward = the official-judge verdict off cached-result.json, verdict =
15
+ * raw judge.json, cost = the durable episode receipt. The loops brain
16
+ * persists per-request STATS only (brain.jsonl has no message content),
17
+ * so supervisor transcripts are honest gap lines until settle-time
18
+ * capture lands.
19
+ * - one WORKER line per opencode worker session, joined via result.json
20
+ * recoveredSpend.workerCwds → opencode sqlite session.directory, with the
21
+ * FULL transcript inlined and reward inherited from the episode.
22
+ * - one PROPOSER line per proposer shot receipt, joined to the Claude
23
+ * project transcript of the shot's worktree cwd by time window.
24
+ *
25
+ * A failed join is a labeled gap line (messages: [], provenance.gap) — never
26
+ * a silent drop: the ledger must account for every invocation it knows of.
27
+ *
28
+ * tsx src/rollout-ledger/backfill-swe-arena.mts <outDir> <out.jsonl>
29
+ */
30
+
31
+ import { randomUUID } from 'node:crypto'
32
+ import { readdir, readFile } from 'node:fs/promises'
33
+ import { join } from 'node:path'
34
+ import { pathToFileURL } from 'node:url'
35
+ import type { DatabaseSync } from 'node:sqlite'
36
+ import { loadCampaignCellRecords } from '../swe-arena/cell-evidence.mts'
37
+ import { buildRolloutManifest, type RolloutEntry } from '../swe-arena/manifest.mts'
38
+ import {
39
+ DEFAULT_CLAUDE_PROJECTS_DIR,
40
+ DEFAULT_OPENCODE_DB,
41
+ findClaudeTranscripts,
42
+ findOpencodeSessionsByDirectory,
43
+ openOpencodeDb,
44
+ readClaudeTranscript,
45
+ readOpencodeSessionMessages,
46
+ ROLLOUT_SCHEMA,
47
+ type OpencodeSessionRow,
48
+ type RolloutLine,
49
+ writeRolloutLedger,
50
+ } from '@tangle-network/agent-eval/rollout'
51
+ import { candidateId } from './settle-capture.mts'
52
+
53
+ const OFFICIAL_JUDGE = 'swe-arena-official-judge'
54
+
55
+ const isRecord = (v: unknown): v is Record<string, unknown> =>
56
+ typeof v === 'object' && v !== null && !Array.isArray(v)
57
+
58
+ /** Full cached-result.json record (superset of the EvidenceCell slice). */
59
+ interface CellRecord {
60
+ scenarioId: string
61
+ rep: number
62
+ artifact: Record<string, unknown> | null
63
+ error?: string
64
+ judgeScores?: Record<string, { composite?: number }>
65
+ costUsd?: number
66
+ tokenUsage?: { input?: number; output?: number }
67
+ resolvedModel?: string
68
+ seed?: number
69
+ cached?: boolean
70
+ }
71
+
72
+ /** The same cell caches `loadCampaignCells` reads, kept verbatim: the backfill
73
+ * needs fields (judgeScores, resolvedModel, seed) the EvidenceCell slice drops.
74
+ * Traversal and the fail-loud identity check are owned by cell-evidence — one
75
+ * rule for what counts as a campaign cell.
76
+ *
77
+ * Only scenarioId and rep are proven by that check; `artifact` is normalized to
78
+ * a record-or-null here so the `artifact !== null` guards downstream cannot be
79
+ * handed an `undefined` that passes them. Every remaining field is optional and
80
+ * read behind its own typeof guard. */
81
+ async function readCellRecords(campaignDir: string): Promise<CellRecord[]> {
82
+ const records = (await loadCampaignCellRecords(campaignDir)).map((record) => ({
83
+ ...record,
84
+ artifact: isRecord(record.artifact) ? record.artifact : null,
85
+ })) as unknown as CellRecord[]
86
+ return records.sort((a, b) => a.scenarioId.localeCompare(b.scenarioId) || a.rep - b.rep)
87
+ }
88
+
89
+ async function readJson(path: string): Promise<unknown | null> {
90
+ const raw = await readFile(path, 'utf8').catch(() => null)
91
+ if (raw === null) return null
92
+ try {
93
+ return JSON.parse(raw) as unknown
94
+ } catch {
95
+ return null
96
+ }
97
+ }
98
+
99
+ function splitModelRef(ref: string | undefined): { provider: string | null; model: string | null } {
100
+ if (typeof ref !== 'string' || ref.length === 0) return { provider: null, model: null }
101
+ const slash = ref.indexOf('/')
102
+ if (slash === -1) return { provider: null, model: ref }
103
+ return { provider: ref.slice(0, slash), model: ref.slice(slash + 1) }
104
+ }
105
+
106
+ export interface BackfillStats {
107
+ lines: number
108
+ byRole: Record<string, number>
109
+ fullTranscripts: number
110
+ gapLines: number
111
+ rewardLabeled: number
112
+ workerSessionsJoined: number
113
+ /** Worker cwds with no opencode session row — a join failure. */
114
+ workerCwdsMissed: number
115
+ /** Sessions found but carrying no readable message parts — a store failure. */
116
+ workerSessionsEmpty: number
117
+ proposerShotsJoined: number
118
+ proposerShotsMissed: number
119
+ opencodeDbAvailable: boolean
120
+ }
121
+
122
+ export interface BackfillResult {
123
+ lines: RolloutLine[]
124
+ stats: BackfillStats
125
+ }
126
+
127
+ export interface BackfillOptions {
128
+ opencodeDb?: string
129
+ claudeProjectsDir?: string
130
+ /** Injected clock for deterministic tests. */
131
+ now?: () => Date
132
+ }
133
+
134
+ interface Ctx {
135
+ outDir: string
136
+ runId: string
137
+ instanceCount: number
138
+ db: DatabaseSync | null
139
+ claudeProjectsDir: string
140
+ capturedAt: string
141
+ stats: BackfillStats
142
+ }
143
+
144
+ function baseLine(ctx: Ctx): Pick<RolloutLine, 'schema' | 'run_id' | 'provenance'> {
145
+ return {
146
+ schema: ROLLOUT_SCHEMA,
147
+ run_id: ctx.runId,
148
+ provenance: { captured_at: ctx.capturedAt, capture: 'backfill' },
149
+ }
150
+ }
151
+
152
+ function push(ctx: Ctx, lines: RolloutLine[], line: RolloutLine): void {
153
+ lines.push(line)
154
+ ctx.stats.lines += 1
155
+ ctx.stats.byRole[line.role] = (ctx.stats.byRole[line.role] ?? 0) + 1
156
+ if (line.messages.length > 0) ctx.stats.fullTranscripts += 1
157
+ else ctx.stats.gapLines += 1
158
+ if (line.outcome.reward !== null) ctx.stats.rewardLabeled += 1
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Supervisor episode + worker sessions for one campaign cell.
163
+ // ---------------------------------------------------------------------------
164
+
165
+ async function emitCellLines(
166
+ ctx: Ctx,
167
+ lines: RolloutLine[],
168
+ entry: RolloutEntry,
169
+ cell: CellRecord,
170
+ ): Promise<void> {
171
+ const artifact = cell.artifact
172
+ const runDir = artifact !== null && typeof artifact.runDir === 'string' ? artifact.runDir : null
173
+ const judgeVerdict = runDir !== null ? await readJson(join(runDir, 'judge.json')) : null
174
+ const resultJson = runDir !== null ? await readJson(join(runDir, 'result.json')) : null
175
+ const result = isRecord(resultJson) ? resultJson : null
176
+
177
+ const composite = cell.judgeScores?.[OFFICIAL_JUDGE]?.composite
178
+ const reward =
179
+ typeof composite === 'number'
180
+ ? composite
181
+ : artifact !== null && typeof artifact.resolved === 'boolean'
182
+ ? (artifact.resolved ? 1 : 0)
183
+ : null
184
+
185
+ const { provider, model } = splitModelRef(cell.resolvedModel)
186
+ const supStatus = result !== null && typeof result.sup_status === 'string' ? result.sup_status : null
187
+ const supervisorId = randomUUID()
188
+
189
+ push(ctx, lines, {
190
+ ...baseLine(ctx),
191
+ rollout_id: supervisorId,
192
+ parent_rollout_id: null,
193
+ candidate_id: candidateId(entry.state.generation, entry.state.candidateIndex),
194
+ generation: entry.state.generation,
195
+ candidate_index: entry.state.candidateIndex,
196
+ role: 'supervisor',
197
+ task: {
198
+ suite: 'swe-bench-verified',
199
+ instance_id: cell.scenarioId,
200
+ split: 'search',
201
+ seed: typeof cell.seed === 'number' ? cell.seed : null,
202
+ rep: cell.rep,
203
+ },
204
+ policy: {
205
+ harness: 'pi-loops',
206
+ harness_version: null,
207
+ model,
208
+ provider,
209
+ profile_commit:
210
+ artifact !== null && typeof artifact.commit === 'string' ? artifact.commit : entry.state.commit,
211
+ sampling: null,
212
+ },
213
+ messages: [],
214
+ tool_defs: [],
215
+ outcome: {
216
+ reward,
217
+ reward_source: reward === null ? null : OFFICIAL_JUDGE,
218
+ verdict: judgeVerdict,
219
+ metrics: {
220
+ resolved: artifact?.resolved ?? null,
221
+ verify_pass: artifact?.verifyPass ?? null,
222
+ patch_lines: artifact?.patchLines ?? null,
223
+ judge_attempts: artifact?.judgeAttempts ?? null,
224
+ judge_wall_s: artifact?.judgeWallS ?? null,
225
+ // Supervisor-tree spend (state.json spend tree); the durable episode
226
+ // receipt usd is on cost.usd. Worker-session tokens live on the
227
+ // worker lines — recovered_tokens here is the EPISODE total, so do
228
+ // not sum tokens across roles without dedup.
229
+ spent_tokens: artifact?.spentTokens ?? null,
230
+ spent_usd: artifact?.spentUsd ?? null,
231
+ recovered_tokens: artifact?.recoveredTokens ?? null,
232
+ sup_status: supStatus,
233
+ sup_verdict: result !== null && typeof result.sup_verdict === 'string' ? result.sup_verdict : null,
234
+ spawned: result !== null && typeof result.spawned === 'number' ? result.spawned : null,
235
+ workers: result !== null && typeof result.workers === 'number' ? result.workers : null,
236
+ settled: result !== null && typeof result.settled === 'number' ? result.settled : null,
237
+ subtasks: result !== null && Array.isArray(result.subtasks) ? result.subtasks : null,
238
+ cached: cell.cached ?? null,
239
+ },
240
+ is_completed: artifact !== null && cell.error === undefined,
241
+ is_truncated: supStatus === 'running',
242
+ error: cell.error ?? null,
243
+ },
244
+ cost: {
245
+ // The campaign CostLedger receipt for the whole episode.
246
+ usd: typeof cell.costUsd === 'number' ? cell.costUsd : null,
247
+ // Brain-side token split is not recorded (brain.jsonl is stats-only);
248
+ // totals live in metrics.spent_tokens / recovered_tokens.
249
+ tokens_in: null,
250
+ tokens_out: null,
251
+ tokens_reasoning: null,
252
+ cache_read: null,
253
+ cache_write: null,
254
+ wall_s: artifact !== null && typeof artifact.wallS === 'number' ? artifact.wallS : null,
255
+ },
256
+ artifacts: {
257
+ patch_path: artifact !== null && typeof artifact.patchPath === 'string' ? artifact.patchPath : null,
258
+ run_dir: runDir,
259
+ transcript_ref: runDir !== null ? join(runDir, 'brain.jsonl') : null,
260
+ },
261
+ provenance: {
262
+ captured_at: ctx.capturedAt,
263
+ capture: 'backfill',
264
+ gap: 'supervisor brain transcript not persisted (brain.jsonl carries per-request stats only)',
265
+ },
266
+ })
267
+
268
+ // Worker sessions: result.json recoveredSpend.workerCwds → opencode store.
269
+ const recovered = result !== null && isRecord(result.recoveredSpend) ? result.recoveredSpend : null
270
+ // A respawned worker reuses its clone cwd, so the recovered list can name the
271
+ // same directory twice. Joining it twice would mint two rollout ids over one
272
+ // opencode session — duplicate training signal, inflated worker counts.
273
+ const workerCwds =
274
+ recovered !== null && Array.isArray(recovered.workerCwds)
275
+ ? [...new Set(recovered.workerCwds.filter((c): c is string => typeof c === 'string'))]
276
+ : []
277
+ for (const cwd of workerCwds) {
278
+ const sessions = ctx.db === null ? [] : findOpencodeSessionsByDirectory(ctx.db, cwd)
279
+ if (sessions.length === 0) {
280
+ ctx.stats.workerCwdsMissed += 1
281
+ push(ctx, lines, workerLine(ctx, entry, cell, supervisorId, reward, cwd, null, []))
282
+ continue
283
+ }
284
+ for (const session of sessions) {
285
+ const messages = ctx.db === null ? [] : readOpencodeSessionMessages(ctx.db, session.id)
286
+ // Two distinct failures: no session row for the cwd (join failure) versus
287
+ // a session row whose parts are unreadable (store integrity). One counter
288
+ // for both would hide store corruption behind a cwd-join metric.
289
+ if (messages.length > 0) ctx.stats.workerSessionsJoined += 1
290
+ else ctx.stats.workerSessionsEmpty += 1
291
+ push(ctx, lines, workerLine(ctx, entry, cell, supervisorId, reward, cwd, session, messages))
292
+ }
293
+ }
294
+ }
295
+
296
+ function workerLine(
297
+ ctx: Ctx,
298
+ entry: RolloutEntry,
299
+ cell: CellRecord,
300
+ supervisorId: string,
301
+ reward: number | null,
302
+ cwd: string,
303
+ session: OpencodeSessionRow | null,
304
+ messages: RolloutLine['messages'],
305
+ ): RolloutLine {
306
+ const artifact = cell.artifact
307
+ return {
308
+ ...baseLine(ctx),
309
+ rollout_id: randomUUID(),
310
+ parent_rollout_id: supervisorId,
311
+ candidate_id: candidateId(entry.state.generation, entry.state.candidateIndex),
312
+ generation: entry.state.generation,
313
+ candidate_index: entry.state.candidateIndex,
314
+ role: 'worker',
315
+ task: {
316
+ suite: 'swe-bench-verified',
317
+ instance_id: cell.scenarioId,
318
+ split: 'search',
319
+ seed: typeof cell.seed === 'number' ? cell.seed : null,
320
+ rep: cell.rep,
321
+ },
322
+ policy: {
323
+ harness: 'opencode',
324
+ harness_version: null,
325
+ model: session?.model?.id ?? null,
326
+ provider: session?.model?.providerID ?? null,
327
+ profile_commit:
328
+ artifact !== null && typeof artifact.commit === 'string' ? artifact.commit : entry.state.commit,
329
+ sampling: null,
330
+ },
331
+ messages,
332
+ tool_defs: [],
333
+ outcome: {
334
+ // Episode-level credit: the worker has no verdict of its own.
335
+ reward,
336
+ reward_source: reward === null ? null : `${OFFICIAL_JUDGE}/inherited`,
337
+ verdict: null,
338
+ metrics: {
339
+ worker_cwd: cwd,
340
+ session_agent: session?.agent ?? null,
341
+ session_parent_id: session?.parentId ?? null,
342
+ has_session: session !== null,
343
+ },
344
+ // A session row with no readable parts is a gap line, not a completed
345
+ // invocation — training filters that key on is_completed alone would
346
+ // otherwise pull empty transcripts.
347
+ is_completed: session !== null && messages.length > 0,
348
+ is_truncated: false,
349
+ error: null,
350
+ },
351
+ cost: {
352
+ usd: session?.costUsd ?? null,
353
+ tokens_in: session?.tokensInput ?? null,
354
+ tokens_out: session?.tokensOutput ?? null,
355
+ tokens_reasoning: session?.tokensReasoning ?? null,
356
+ cache_read: session?.tokensCacheRead ?? null,
357
+ cache_write: session?.tokensCacheWrite ?? null,
358
+ wall_s: session !== null ? Math.round((session.timeUpdated - session.timeCreated) / 1000) : null,
359
+ },
360
+ artifacts: {
361
+ patch_path: null,
362
+ run_dir: artifact !== null && typeof artifact.runDir === 'string' ? artifact.runDir : null,
363
+ transcript_ref: session !== null ? `opencode:${session.id}` : null,
364
+ },
365
+ provenance: {
366
+ captured_at: ctx.capturedAt,
367
+ capture: 'backfill',
368
+ ...(messages.length === 0
369
+ ? {
370
+ gap:
371
+ session === null
372
+ ? ctx.db === null
373
+ ? `opencode store unavailable; worker cwd ${cwd} unrecoverable`
374
+ : `no opencode session found for worker cwd ${cwd}`
375
+ : `opencode session ${session.id} has no readable message parts`,
376
+ }
377
+ : {}),
378
+ },
379
+ }
380
+ }
381
+
382
+ // ---------------------------------------------------------------------------
383
+ // Proposer shots — receipts under proposer-shots/ (flat or per-author dirs),
384
+ // joined to Claude transcripts of the shot worktree by time window.
385
+ // ---------------------------------------------------------------------------
386
+
387
+ interface ShotReceiptFile {
388
+ path: string
389
+ author: string | null
390
+ generation: number
391
+ candidateIndex: number
392
+ shot: number
393
+ }
394
+
395
+ async function listShotReceipts(outDir: string): Promise<ShotReceiptFile[]> {
396
+ const shotDir = join(outDir, 'proposer-shots')
397
+ const found: ShotReceiptFile[] = []
398
+ const parse = (name: string, author: string | null, path: string): void => {
399
+ const match = /^gen(\d+)-cand(\d+)-shot(\d+)\.json$/.exec(name)
400
+ if (match === null) return
401
+ found.push({
402
+ path,
403
+ author,
404
+ generation: Number(match[1]),
405
+ candidateIndex: Number(match[2]),
406
+ shot: Number(match[3]),
407
+ })
408
+ }
409
+ for (const entry of await readdir(shotDir, { withFileTypes: true }).catch(() => [])) {
410
+ if (entry.isDirectory()) {
411
+ const authorDir = join(shotDir, entry.name)
412
+ for (const name of await readdir(authorDir).catch(() => [])) {
413
+ parse(name, entry.name, join(authorDir, name))
414
+ }
415
+ } else {
416
+ parse(entry.name, null, join(shotDir, entry.name))
417
+ }
418
+ }
419
+ return found.sort((a, b) => a.path.localeCompare(b.path))
420
+ }
421
+
422
+ async function emitProposerLines(ctx: Ctx, lines: RolloutLine[], entries: RolloutEntry[]): Promise<void> {
423
+ const receipts = await listShotReceipts(ctx.outDir)
424
+ const entryFor = new Map<string, RolloutEntry>()
425
+ for (const entry of entries) {
426
+ entryFor.set(`${entry.state.generation}:${entry.state.candidateIndex}`, entry)
427
+ }
428
+
429
+ for (const shot of receipts) {
430
+ const parsed = await readJson(shot.path)
431
+ const receipt = isRecord(parsed) && isRecord(parsed.receipt) ? parsed.receipt : null
432
+ const entry = entryFor.get(`${shot.generation}:${shot.candidateIndex}`) ?? null
433
+
434
+ // The proposer worktree the arena dispatched this shot into.
435
+ const worktreeCwd =
436
+ shot.author !== null
437
+ ? join(ctx.outDir, 'proposer-wt', `gen${shot.generation}-cand${shot.candidateIndex}-${shot.author}`)
438
+ : join(ctx.outDir, 'proposer-wt', `gen${shot.generation}-cand${shot.candidateIndex}`)
439
+
440
+ const startedAt = receipt !== null && typeof receipt.startedAt === 'string' ? Date.parse(receipt.startedAt) : NaN
441
+ const completedAt =
442
+ receipt !== null && typeof receipt.completedAt === 'string' ? Date.parse(receipt.completedAt) : NaN
443
+
444
+ let transcript: Awaited<ReturnType<typeof readClaudeTranscript>> | null = null
445
+ let transcriptRef: string | null = null
446
+ const candidates = await findClaudeTranscripts(worktreeCwd, ctx.claudeProjectsDir)
447
+ const windowed: Array<{ path: string; startMs: number }> = []
448
+ for (const ref of candidates) {
449
+ const t = await readClaudeTranscript(ref.path)
450
+ if (t.startedAt === null) continue
451
+ const startMs = Date.parse(t.startedAt)
452
+ const inWindow =
453
+ Number.isNaN(startedAt) || Number.isNaN(completedAt)
454
+ ? candidates.length === 1
455
+ : startMs >= startedAt - 120_000 && startMs <= completedAt + 120_000
456
+ if (inWindow) windowed.push({ path: ref.path, startMs })
457
+ }
458
+ if (windowed.length > 0) {
459
+ windowed.sort(
460
+ (a, b) => Math.abs(a.startMs - (startedAt || a.startMs)) - Math.abs(b.startMs - (startedAt || b.startMs)),
461
+ )
462
+ transcriptRef = windowed[0].path
463
+ transcript = await readClaudeTranscript(transcriptRef)
464
+ ctx.stats.proposerShotsJoined += 1
465
+ } else {
466
+ ctx.stats.proposerShotsMissed += 1
467
+ }
468
+
469
+ // The proposer's reward is its candidate's official resolved fraction —
470
+ // fail-closed null when the candidate has no scored entry.
471
+ const reward =
472
+ entry !== null && ctx.instanceCount > 0 ? entry.outcome.resolvedCount / ctx.instanceCount : null
473
+
474
+ push(ctx, lines, {
475
+ ...baseLine(ctx),
476
+ rollout_id: randomUUID(),
477
+ parent_rollout_id: null,
478
+ candidate_id: candidateId(shot.generation, shot.candidateIndex),
479
+ generation: shot.generation,
480
+ candidate_index: shot.candidateIndex,
481
+ role: 'proposer',
482
+ task: {
483
+ suite: 'swe-arena-proposer',
484
+ instance_id:
485
+ shot.author !== null
486
+ ? `${candidateId(shot.generation, shot.candidateIndex)}-${shot.author}`
487
+ : candidateId(shot.generation, shot.candidateIndex),
488
+ split: 'search',
489
+ seed: null,
490
+ rep: shot.shot,
491
+ },
492
+ policy: {
493
+ harness: receipt !== null && typeof receipt.harness === 'string' ? receipt.harness : null,
494
+ harness_version: null,
495
+ model:
496
+ transcript?.model ?? (receipt !== null && typeof receipt.model === 'string' ? receipt.model : null),
497
+ provider: null,
498
+ profile_commit: entry?.state.commit ?? null,
499
+ sampling: null,
500
+ },
501
+ messages: transcript?.messages ?? [],
502
+ tool_defs: [],
503
+ outcome: {
504
+ reward,
505
+ reward_source: reward === null ? null : `${OFFICIAL_JUDGE}/candidate-resolved-fraction`,
506
+ verdict: null,
507
+ metrics: {
508
+ resolved_count: entry?.outcome.resolvedCount ?? null,
509
+ instance_count: ctx.instanceCount,
510
+ exit_code: receipt !== null && typeof receipt.exitCode === 'number' ? receipt.exitCode : null,
511
+ timed_out: receipt !== null && typeof receipt.timedOut === 'boolean' ? receipt.timedOut : null,
512
+ author: shot.author,
513
+ max_shots: receipt !== null && typeof receipt.maxShots === 'number' ? receipt.maxShots : null,
514
+ prompt_sha256: receipt !== null && typeof receipt.promptSha256 === 'string' ? receipt.promptSha256 : null,
515
+ },
516
+ is_completed: receipt !== null && receipt.exitCode === 0,
517
+ is_truncated: receipt !== null && receipt.timedOut === true,
518
+ error: receipt !== null && typeof receipt.error === 'string' ? receipt.error : null,
519
+ },
520
+ cost: {
521
+ usd: receipt !== null && typeof receipt.costUsd === 'number' ? receipt.costUsd : null,
522
+ tokens_in: transcript !== null ? transcript.usage.tokensIn : null,
523
+ tokens_out: transcript !== null ? transcript.usage.tokensOut : null,
524
+ tokens_reasoning: null,
525
+ cache_read: transcript !== null ? transcript.usage.cacheRead : null,
526
+ cache_write: transcript !== null ? transcript.usage.cacheWrite : null,
527
+ wall_s:
528
+ receipt !== null && typeof receipt.durationMs === 'number' ? Math.round(receipt.durationMs / 1000) : null,
529
+ },
530
+ artifacts: {
531
+ patch_path: entry?.action.diffPath ?? null,
532
+ run_dir: worktreeCwd,
533
+ transcript_ref: transcriptRef ?? shot.path,
534
+ },
535
+ provenance: {
536
+ captured_at: ctx.capturedAt,
537
+ capture: 'backfill',
538
+ ...(transcript === null
539
+ ? { gap: `no claude transcript matched worktree ${worktreeCwd} in the shot's time window` }
540
+ : {}),
541
+ },
542
+ })
543
+ }
544
+ }
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // Entry point.
548
+ // ---------------------------------------------------------------------------
549
+
550
+ export async function backfillSweArena(outDir: string, opts: BackfillOptions = {}): Promise<BackfillResult> {
551
+ const manifest = await buildRolloutManifest(outDir)
552
+ const provenance = isRecord(manifest.provenance) ? manifest.provenance : null
553
+ const runId = provenance !== null && typeof provenance.runId === 'string' ? provenance.runId : outDir
554
+
555
+ const db = await openOpencodeDb(opts.opencodeDb ?? DEFAULT_OPENCODE_DB)
556
+ const ctx: Ctx = {
557
+ outDir,
558
+ runId,
559
+ instanceCount: manifest.instances.length,
560
+ db,
561
+ claudeProjectsDir: opts.claudeProjectsDir ?? DEFAULT_CLAUDE_PROJECTS_DIR,
562
+ capturedAt: (opts.now?.() ?? new Date()).toISOString(),
563
+ stats: {
564
+ lines: 0,
565
+ byRole: {},
566
+ fullTranscripts: 0,
567
+ gapLines: 0,
568
+ rewardLabeled: 0,
569
+ workerSessionsJoined: 0,
570
+ workerCwdsMissed: 0,
571
+ workerSessionsEmpty: 0,
572
+ proposerShotsJoined: 0,
573
+ proposerShotsMissed: 0,
574
+ opencodeDbAvailable: db !== null,
575
+ },
576
+ }
577
+
578
+ const lines: RolloutLine[] = []
579
+ const entries: RolloutEntry[] = manifest.generations.flatMap((g) => g.rollouts)
580
+ try {
581
+ for (const entry of entries) {
582
+ for (const cell of await readCellRecords(entry.state.campaignDir)) {
583
+ await emitCellLines(ctx, lines, entry, cell)
584
+ }
585
+ }
586
+ await emitProposerLines(ctx, lines, entries)
587
+ } finally {
588
+ db?.close()
589
+ }
590
+
591
+ return { lines, stats: ctx.stats }
592
+ }
593
+
594
+ const isMain = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href
595
+
596
+ if (isMain) {
597
+ const [outDir, ledgerPath] = process.argv.slice(2)
598
+ if (!outDir || !ledgerPath) {
599
+ console.error('usage: tsx src/rollout-ledger/backfill-swe-arena.mts <outDir> <out.jsonl>')
600
+ process.exit(2)
601
+ }
602
+ const { lines, stats } = await backfillSweArena(outDir)
603
+ await writeRolloutLedger(ledgerPath, lines)
604
+ console.log(JSON.stringify(stats, null, 2))
605
+ console.log(`rollout ledger → ${ledgerPath} (${lines.length} lines)`)
606
+ }