@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,123 @@
1
+ /**
2
+ * Gen-5 public/private score split: determinism (seeded by runId), persisted
3
+ * resume stability, and the never-surfaced invariant's checkable pieces.
4
+ */
5
+
6
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
7
+ import { tmpdir } from 'node:os'
8
+ import { join } from 'node:path'
9
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
10
+ import {
11
+ leaksPrivateInstance,
12
+ loadOrCreateScoreSplit,
13
+ publicOnly,
14
+ SCORE_SPLIT_FILENAME,
15
+ splitInstances,
16
+ subScores,
17
+ } from './score-split.mts'
18
+
19
+ const SIX = [
20
+ 'astropy__astropy-13033',
21
+ 'django__django-11532',
22
+ 'matplotlib__matplotlib-20826',
23
+ 'pydata__xarray-4687',
24
+ 'pytest-dev__pytest-6197',
25
+ 'sphinx-doc__sphinx-9658',
26
+ ]
27
+
28
+ describe('splitInstances', () => {
29
+ it('is deterministic: same (runId, instances, publicCount) → identical split', () => {
30
+ const a = splitInstances('r4-abc123', SIX, 4)
31
+ const b = splitInstances('r4-abc123', SIX, 4)
32
+ expect(a).toEqual(b)
33
+ })
34
+
35
+ it('is instance-ORDER-insensitive (ranking is content-derived)', () => {
36
+ const a = splitInstances('r4-abc123', SIX, 4)
37
+ const b = splitInstances('r4-abc123', [...SIX].reverse(), 4)
38
+ expect(a).toEqual(b)
39
+ })
40
+
41
+ it('respects publicCount and partitions exactly (4 public / 2 private of 6)', () => {
42
+ const split = splitInstances('r4-abc123', SIX, 4)
43
+ expect(split.publicInstances).toHaveLength(4)
44
+ expect(split.privateInstances).toHaveLength(2)
45
+ expect([...split.publicInstances, ...split.privateInstances].sort()).toEqual([...SIX].sort())
46
+ expect(split.publicInstances.filter((i) => split.privateInstances.includes(i))).toEqual([])
47
+ })
48
+
49
+ it('is SEEDED by runId: different runIds produce different splits', () => {
50
+ const splits = new Set(
51
+ Array.from({ length: 10 }, (_, i) => splitInstances(`r4-run${i}`, SIX, 4).privateInstances.join('|')),
52
+ )
53
+ expect(splits.size).toBeGreaterThan(1)
54
+ })
55
+
56
+ it('rejects publicCount outside (0, n) and duplicate instance ids', () => {
57
+ expect(() => splitInstances('r', SIX, 0)).toThrow(/publicCount/)
58
+ expect(() => splitInstances('r', SIX, 6)).toThrow(/publicCount/)
59
+ expect(() => splitInstances('r', SIX, 2.5)).toThrow(/publicCount/)
60
+ expect(() => splitInstances('r', ['a', 'a', 'b'], 1)).toThrow(/duplicate/)
61
+ })
62
+ })
63
+
64
+ describe('loadOrCreateScoreSplit (resume stability)', () => {
65
+ let outDir: string
66
+ beforeEach(async () => {
67
+ outDir = await mkdtemp(join(tmpdir(), 'split-'))
68
+ })
69
+ afterEach(async () => {
70
+ await rm(outDir, { recursive: true, force: true })
71
+ })
72
+
73
+ it('persists on first computation and RELOADS verbatim under a NEW runId (a resume cannot rotate private into view)', async () => {
74
+ const first = await loadOrCreateScoreSplit({ outDir, runId: 'r4-first', instances: SIX, publicCount: 4 })
75
+ expect(JSON.parse(await readFile(join(outDir, SCORE_SPLIT_FILENAME), 'utf8'))).toEqual(first)
76
+ const resumed = await loadOrCreateScoreSplit({ outDir, runId: 'r4-DIFFERENT', instances: SIX, publicCount: 4 })
77
+ expect(resumed).toEqual(first)
78
+ expect(resumed.seededBy).toBe('r4-first')
79
+ })
80
+
81
+ it('fails loud when the persisted split covers different instances or a different publicCount', async () => {
82
+ await loadOrCreateScoreSplit({ outDir, runId: 'r4-first', instances: SIX, publicCount: 4 })
83
+ await expect(
84
+ loadOrCreateScoreSplit({ outDir, runId: 'r4-x', instances: [...SIX.slice(0, 5), 'other__iid-1'], publicCount: 4 }),
85
+ ).rejects.toThrow(/refusing to silently re-split/)
86
+ await expect(
87
+ loadOrCreateScoreSplit({ outDir, runId: 'r4-x', instances: SIX, publicCount: 3 }),
88
+ ).rejects.toThrow(/refusing to silently re-split/)
89
+ })
90
+
91
+ it('fails loud on an unknown schema in the persisted file', async () => {
92
+ await writeFile(join(outDir, SCORE_SPLIT_FILENAME), JSON.stringify({ schema: 'nope' }))
93
+ await expect(
94
+ loadOrCreateScoreSplit({ outDir, runId: 'r', instances: SIX, publicCount: 4 }),
95
+ ).rejects.toThrow(/unknown schema/)
96
+ })
97
+ })
98
+
99
+ describe('never-surfaced invariant helpers', () => {
100
+ const split = splitInstances('r4-abc123', SIX, 4)
101
+
102
+ it('leaksPrivateInstance names every private id present in a text', () => {
103
+ expect(leaksPrivateInstance('nothing to see', split)).toEqual([])
104
+ const leakText = `evidence for ${split.privateInstances[0]} here`
105
+ expect(leaksPrivateInstance(leakText, split)).toEqual([split.privateInstances[0]])
106
+ })
107
+
108
+ it('publicOnly drops private-instance records and is identity without a split', () => {
109
+ const records = SIX.map((iid) => ({ iid, x: 1 }))
110
+ const filtered = publicOnly(records, (r) => r.iid, split)
111
+ expect(filtered.map((r) => r.iid).sort()).toEqual(split.publicInstances)
112
+ expect(publicOnly(records, (r) => r.iid, null)).toEqual(records)
113
+ })
114
+
115
+ it('subScores counts fail-closed AND-verdicts per half (missing verdict = 0)', () => {
116
+ const verdicts: Record<string, boolean> = {}
117
+ verdicts[split.publicInstances[0]!] = true
118
+ verdicts[split.publicInstances[1]!] = false
119
+ verdicts[split.privateInstances[0]!] = true
120
+ // privateInstances[1] has NO verdict — counts 0.
121
+ expect(subScores(verdicts, split)).toEqual({ publicResolvedCount: 1, privateResolvedCount: 1 })
122
+ })
123
+ })
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Serialized official judge — the typed port of the experiment's `judge.sh`,
3
+ * wrapping `adapter.judge` (via the tracked judge-child.mts) with the three
4
+ * protections the bash experiment had to learn the hard way:
5
+ *
6
+ * 1. ONE JUDGE AT A TIME. The stale-container pre-clean does `docker rm -f`
7
+ * by instance short-name; two overlapping judges of the same instance nuke
8
+ * each other's container mid-run and each records a spurious
9
+ * `resolved:false` (proven false-negative on psf__requests-1766). The lock
10
+ * here is two-layer: an in-process promise chain plus a kernel-backed
11
+ * `flock`, so the pre-clean can only ever remove a genuinely stale
12
+ * container — never a live grade.
13
+ *
14
+ * 2. CEILING ≥ 1800s. The original 700s ceiling caused 3 spurious timeouts on
15
+ * psf__requests-2317 (a successful grade took 1181s). The floor is
16
+ * enforced, not advisory: a shorter ceiling for the REAL judge is a
17
+ * protocol violation and throws.
18
+ *
19
+ * 3. ONE RETRY on empty/unparseable judge output (single-run judge flake was
20
+ * observed live: byte-identical patches split-verdicted). A second failure
21
+ * returns `resolved: null` — inconclusive, never a fabricated verdict —
22
+ * matching the RejudgeRow semantics M1 pinned.
23
+ */
24
+
25
+ import { spawn } from 'node:child_process'
26
+ import { open, readFile, type FileHandle } from 'node:fs/promises'
27
+ import { tmpdir } from 'node:os'
28
+ import { join } from 'node:path'
29
+ import { fileURLToPath } from 'node:url'
30
+ import { run } from './proc'
31
+
32
+ /** Verdict shape — field names match the experiment's JUDGE_RESULT/RejudgeRow rows. */
33
+ export interface JudgeVerdict {
34
+ iid: string
35
+ /** `null` = inconclusive (judge failed twice); never a guessed boolean. */
36
+ resolved: boolean | null
37
+ score?: number
38
+ secs?: number
39
+ patch_bytes?: number
40
+ note?: string
41
+ error?: string
42
+ /** First 200 chars of the failing output, for post-mortem (mirrors rejudge rows). */
43
+ raw?: string
44
+ /** How many judge child runs it took (1 = clean, 2 = retried). */
45
+ attempts?: number
46
+ }
47
+
48
+ export interface JudgeCommand {
49
+ bin: string
50
+ argv: string[]
51
+ cwd: string
52
+ }
53
+
54
+ export interface SerializedJudgeOptions {
55
+ /**
56
+ * Hard ceiling per judge child. Default 1_800_000 ms; values below the floor
57
+ * throw unless `unsafeAllowShortTimeout` (test hook) is set.
58
+ */
59
+ timeoutMs?: number
60
+ /** Maximum time spent waiting to acquire both judge locks. */
61
+ lockWaitTimeoutMs?: number
62
+ /** Hard ceiling for each stale-container pre-clean command. */
63
+ preCleanTimeoutMs?: number
64
+ /** Test-only escape hatch for the 1800s floor. NEVER set on a real judge. */
65
+ unsafeAllowShortTimeout?: boolean
66
+ /** Cross-process kernel lock file. One per docker daemon. */
67
+ lockFile?: string
68
+ /** SWEBENCH_CACHE_LEVEL for the child. `instance` = we manage image rotation. */
69
+ cacheLevel?: string
70
+ /** Override the judge child invocation (tests inject fakes here). */
71
+ command?: (iid: string, patchPath: string) => JudgeCommand
72
+ }
73
+
74
+ export const JUDGE_TIMEOUT_FLOOR_MS = 1_800_000
75
+ export const JUDGE_PRE_CLEAN_TIMEOUT_MS = 60_000
76
+ /** Process-group settlement and scheduler margin beyond all command ceilings. */
77
+ export const JUDGE_LOCK_SETTLEMENT_MARGIN_MS = 30_000
78
+ /** One live holder may use two attempts, each with docker ps + docker rm. */
79
+ export const JUDGE_LOCK_WAIT_TIMEOUT_MS =
80
+ 2 * (JUDGE_TIMEOUT_FLOOR_MS + 2 * JUDGE_PRE_CLEAN_TIMEOUT_MS) + JUDGE_LOCK_SETTLEMENT_MARGIN_MS
81
+
82
+ const benchRoot = fileURLToPath(new URL('../..', import.meta.url))
83
+ const judgeChild = fileURLToPath(new URL('./judge-child.mts', import.meta.url))
84
+
85
+ const defaultCommand = (iid: string, patchPath: string): JudgeCommand => ({
86
+ bin: 'node',
87
+ argv: ['--import', 'tsx', judgeChild, iid, patchPath],
88
+ cwd: benchRoot,
89
+ })
90
+
91
+ /** judge.sh's `sed 's/.*__//'` — the docker container filter for pre-clean. */
92
+ export function instanceShortName(iid: string): string {
93
+ return iid.replace(/.*__/, '')
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Locking: in-process promise chain + cross-process kernel flock.
98
+ // ---------------------------------------------------------------------------
99
+
100
+ let inProcessChain: Promise<unknown> = Promise.resolve()
101
+
102
+ export interface JudgeLockOptions {
103
+ /** Maximum total wait across the in-process queue and kernel lock. */
104
+ timeoutMs?: number
105
+ signal?: AbortSignal
106
+ }
107
+
108
+ function abortReason(signal: AbortSignal): Error {
109
+ if (signal.reason instanceof Error) return signal.reason
110
+ const error = new Error('serialized-judge: aborted while waiting for the judge lock')
111
+ error.name = 'AbortError'
112
+ return error
113
+ }
114
+
115
+ function throwIfAborted(signal?: AbortSignal): void {
116
+ if (signal?.aborted) throw abortReason(signal)
117
+ }
118
+
119
+ function lockTimeoutError(timeoutMs: number): Error {
120
+ return new Error(`serialized-judge: lock wait exceeded ${timeoutMs}ms`)
121
+ }
122
+
123
+ function waitFor<T>(promise: Promise<T>, deadline: number, timeoutMs: number, signal?: AbortSignal): Promise<T> {
124
+ throwIfAborted(signal)
125
+ const remainingMs = deadline - Date.now()
126
+ if (remainingMs <= 0) return Promise.reject(lockTimeoutError(timeoutMs))
127
+
128
+ return new Promise<T>((resolve, reject) => {
129
+ let settled = false
130
+ const finish = (callback: () => void) => {
131
+ if (settled) return
132
+ settled = true
133
+ clearTimeout(timer)
134
+ signal?.removeEventListener('abort', onAbort)
135
+ callback()
136
+ }
137
+ const onAbort = () => finish(() => reject(abortReason(signal!)))
138
+ const timer = setTimeout(() => finish(() => reject(lockTimeoutError(timeoutMs))), remainingMs)
139
+ signal?.addEventListener('abort', onAbort, { once: true })
140
+ if (signal?.aborted) onAbort()
141
+ promise.then(
142
+ (value) => finish(() => resolve(value)),
143
+ (error: unknown) => finish(() => reject(error)),
144
+ )
145
+ })
146
+ }
147
+
148
+ /**
149
+ * Ask util-linux `flock` to lock an fd inherited from this process. Linux
150
+ * associates a flock with the open file description, so the lock remains held
151
+ * by `handle` after the short helper exits and is released atomically on close.
152
+ * There is no owner record to initialize, inspect, or unlink: process death and
153
+ * fd close are the only stale-owner recovery path, both enforced by the kernel.
154
+ */
155
+ function tryAcquireFlock(
156
+ handle: FileHandle,
157
+ deadline: number,
158
+ timeoutMs: number,
159
+ signal?: AbortSignal,
160
+ ): Promise<boolean> {
161
+ throwIfAborted(signal)
162
+ const remainingMs = deadline - Date.now()
163
+ if (remainingMs <= 0) return Promise.reject(lockTimeoutError(timeoutMs))
164
+
165
+ return new Promise<boolean>((resolve, reject) => {
166
+ const waitSeconds = Math.max(0.001, remainingMs / 1_000).toFixed(3)
167
+ const child = spawn('flock', ['--exclusive', '--wait', waitSeconds, '3'], {
168
+ stdio: ['ignore', 'ignore', 'pipe', handle.fd],
169
+ })
170
+ let stderr = ''
171
+ let pendingError: Error | undefined
172
+ let settled = false
173
+ const finish = (callback: () => void) => {
174
+ if (settled) return
175
+ settled = true
176
+ clearTimeout(timer)
177
+ signal?.removeEventListener('abort', onAbort)
178
+ callback()
179
+ }
180
+ const stopWith = (error: Error) => {
181
+ if (pendingError || settled) return
182
+ pendingError = error
183
+ child.kill('SIGKILL')
184
+ }
185
+ const onAbort = () => stopWith(abortReason(signal!))
186
+ const timer = setTimeout(() => stopWith(lockTimeoutError(timeoutMs)), remainingMs)
187
+
188
+ signal?.addEventListener('abort', onAbort, { once: true })
189
+ if (signal?.aborted) onAbort()
190
+ child.stderr?.on('data', (chunk: Buffer) => {
191
+ if (stderr.length < 2_000) stderr += chunk.toString('utf8')
192
+ })
193
+ child.on('error', (error) => finish(() => reject(pendingError ?? error)))
194
+ child.on('close', (code, closeSignal) => {
195
+ if (pendingError) {
196
+ finish(() => reject(pendingError!))
197
+ return
198
+ }
199
+ if (code === 0) {
200
+ finish(() => resolve(true))
201
+ return
202
+ }
203
+ // util-linux flock uses rc=1 when --wait expires without ownership.
204
+ if (code === 1) {
205
+ finish(() => resolve(false))
206
+ return
207
+ }
208
+ const detail = stderr.trim()
209
+ finish(() => reject(new Error(
210
+ `serialized-judge: flock failed with ${closeSignal ?? `exit ${code ?? 'unknown'}`}${detail ? `: ${detail}` : ''}`,
211
+ )))
212
+ })
213
+ })
214
+ }
215
+
216
+ async function acquireFlock(
217
+ lockFile: string,
218
+ deadline: number,
219
+ timeoutMs: number,
220
+ signal?: AbortSignal,
221
+ ): Promise<() => Promise<void>> {
222
+ for (;;) {
223
+ throwIfAborted(signal)
224
+ if (Date.now() >= deadline) throw lockTimeoutError(timeoutMs)
225
+ const handle = await open(lockFile, 'a')
226
+ try {
227
+ if (await tryAcquireFlock(handle, deadline, timeoutMs, signal)) {
228
+ let released = false
229
+ return async () => {
230
+ if (released) return
231
+ released = true
232
+ await handle.close()
233
+ }
234
+ }
235
+ } catch (error) {
236
+ await handle.close().catch(() => {})
237
+ throw error
238
+ }
239
+ await handle.close()
240
+ if (Date.now() >= deadline) throw lockTimeoutError(timeoutMs)
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Run `fn` holding BOTH locks. Exported so calibrate/parity paths can pin any
246
+ * docker-touching critical section to the same mutex the judge uses.
247
+ */
248
+ export function withJudgeLock<T>(
249
+ lockFile: string,
250
+ fn: () => Promise<T>,
251
+ opts: JudgeLockOptions = {},
252
+ ): Promise<T> {
253
+ const timeoutMs = opts.timeoutMs ?? JUDGE_LOCK_WAIT_TIMEOUT_MS
254
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
255
+ return Promise.reject(new Error('serialized-judge: lock timeoutMs must be a positive finite number'))
256
+ }
257
+ const deadline = Date.now() + timeoutMs
258
+ const predecessor = inProcessChain.catch(() => {})
259
+ const task = (async () => {
260
+ // Cancellation stops this waiter, not the predecessor. A later waiter
261
+ // still has to acquire the kernel lock, so the live holder is never bypassed.
262
+ await waitFor(predecessor, deadline, timeoutMs, opts.signal)
263
+ const release = await acquireFlock(lockFile, deadline, timeoutMs, opts.signal)
264
+ try {
265
+ throwIfAborted(opts.signal)
266
+ return await fn()
267
+ } finally {
268
+ await release()
269
+ }
270
+ })()
271
+ inProcessChain = task.catch(() => {})
272
+ return task
273
+ }
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // The judge itself.
277
+ // ---------------------------------------------------------------------------
278
+
279
+ export interface SerializedJudge {
280
+ judge(iid: string, patchPath: string, tag?: string, signal?: AbortSignal): Promise<JudgeVerdict>
281
+ readonly lockFile: string
282
+ }
283
+
284
+ function parseJudgeResult(stdout: string): Omit<JudgeVerdict, 'attempts'> | undefined {
285
+ const line = stdout.split('\n').find((l) => l.includes('JUDGE_RESULT'))
286
+ if (!line) return undefined
287
+ try {
288
+ const parsed = JSON.parse(line.slice(line.indexOf('JUDGE_RESULT') + 'JUDGE_RESULT'.length).trim())
289
+ if (typeof parsed !== 'object' || parsed === null || typeof parsed.resolved !== 'boolean') return undefined
290
+ return parsed as Omit<JudgeVerdict, 'attempts'>
291
+ } catch {
292
+ return undefined
293
+ }
294
+ }
295
+
296
+ export function createSerializedJudge(opts: SerializedJudgeOptions = {}): SerializedJudge {
297
+ const timeoutMs = opts.timeoutMs ?? JUDGE_TIMEOUT_FLOOR_MS
298
+ const usingRealJudge = opts.command === undefined
299
+ if (timeoutMs < JUDGE_TIMEOUT_FLOOR_MS && (usingRealJudge || !opts.unsafeAllowShortTimeout)) {
300
+ throw new Error(
301
+ `serialized-judge: timeoutMs=${timeoutMs} is below the ${JUDGE_TIMEOUT_FLOOR_MS}ms floor ` +
302
+ `(700s provably caused 3 spurious timeouts on psf__requests-2317)`,
303
+ )
304
+ }
305
+ const lockFile = opts.lockFile ?? join(tmpdir(), 'swe-arena-judge.lock')
306
+ const preCleanTimeoutMs = opts.preCleanTimeoutMs ?? JUDGE_PRE_CLEAN_TIMEOUT_MS
307
+ const lockWaitTimeoutMs =
308
+ opts.lockWaitTimeoutMs ??
309
+ 2 * (timeoutMs + 2 * preCleanTimeoutMs) + JUDGE_LOCK_SETTLEMENT_MARGIN_MS
310
+ if (!Number.isFinite(lockWaitTimeoutMs) || lockWaitTimeoutMs <= 0) {
311
+ throw new Error('serialized-judge: lockWaitTimeoutMs must be a positive finite number')
312
+ }
313
+ if (!Number.isFinite(preCleanTimeoutMs) || preCleanTimeoutMs <= 0) {
314
+ throw new Error('serialized-judge: preCleanTimeoutMs must be a positive finite number')
315
+ }
316
+ const cacheLevel = opts.cacheLevel ?? 'instance'
317
+ const command = opts.command ?? defaultCommand
318
+ // A command override is a hermetic test hook and does not touch Docker.
319
+ // Tests that exercise cleanup opt in by setting preCleanTimeoutMs.
320
+ const shouldPreClean = usingRealJudge || opts.preCleanTimeoutMs !== undefined
321
+
322
+ async function runDockerCleanup(argv: string[], signal?: AbortSignal): Promise<Awaited<ReturnType<typeof run>>> {
323
+ let result: Awaited<ReturnType<typeof run>>
324
+ try {
325
+ result = await run('docker', argv, { timeoutMs: preCleanTimeoutMs, signal })
326
+ } catch (error) {
327
+ throw new Error(`serialized-judge: docker pre-clean could not start: ${(error as Error).message}`, {
328
+ cause: error,
329
+ })
330
+ }
331
+ throwIfAborted(signal)
332
+ if (result.timedOut) {
333
+ throw new Error(`serialized-judge: docker ${argv[0]} pre-clean timed out after ${preCleanTimeoutMs}ms`)
334
+ }
335
+ if (result.code !== 0) {
336
+ const detail = (result.stderr || result.stdout).trim().slice(0, 1_500)
337
+ throw new Error(
338
+ `serialized-judge: docker ${argv[0]} pre-clean exited ${result.code}${detail ? `: ${detail}` : ''}`,
339
+ )
340
+ }
341
+ return result
342
+ }
343
+
344
+ async function preClean(iid: string, signal?: AbortSignal): Promise<void> {
345
+ throwIfAborted(signal)
346
+ const short = instanceShortName(iid)
347
+ const ps = await runDockerCleanup(['ps', '-aq', '--filter', `name=${short}`], signal)
348
+ throwIfAborted(signal)
349
+ const ids = ps.stdout.split('\n').map((s) => s.trim()).filter(Boolean)
350
+ if (ids.length > 0) {
351
+ await runDockerCleanup(['rm', '-f', ...ids], signal)
352
+ throwIfAborted(signal)
353
+ }
354
+ }
355
+
356
+ async function attempt(
357
+ iid: string,
358
+ patchPath: string,
359
+ signal?: AbortSignal,
360
+ ): Promise<{ verdict?: Omit<JudgeVerdict, 'attempts'>; raw: string }> {
361
+ // Pre-clean INSIDE the mutex: with judging serialized this can only ever
362
+ // remove a stale container, never a live grade.
363
+ if (shouldPreClean) await preClean(iid, signal)
364
+ throwIfAborted(signal)
365
+ const { bin, argv, cwd } = command(iid, patchPath)
366
+ const res = await run(bin, argv, {
367
+ cwd,
368
+ timeoutMs,
369
+ signal,
370
+ env: { ...process.env, SWEBENCH_CACHE_LEVEL: cacheLevel },
371
+ })
372
+ throwIfAborted(signal)
373
+ const raw = res.timedOut
374
+ ? `timeout after ${timeoutMs}ms`
375
+ : res.code === 0
376
+ ? res.stdout || res.stderr
377
+ : `exit ${res.code}: ${res.stdout || res.stderr}`
378
+ return {
379
+ verdict: !res.timedOut && res.code === 0 ? parseJudgeResult(res.stdout) : undefined,
380
+ raw,
381
+ }
382
+ }
383
+
384
+ return {
385
+ lockFile,
386
+ async judge(iid, patchPath, _tag = 'x', signal) {
387
+ throwIfAborted(signal)
388
+ // Empty patch never reaches docker — same short-circuit as judge.sh.
389
+ const patch = await readFile(patchPath, 'utf8')
390
+ throwIfAborted(signal)
391
+ if (patch.trim().length === 0) {
392
+ return { iid, resolved: false, score: 0, note: 'empty-patch', attempts: 0 }
393
+ }
394
+ return withJudgeLock(
395
+ lockFile,
396
+ async () => {
397
+ const first = await attempt(iid, patchPath, signal)
398
+ if (first.verdict) return { ...first.verdict, attempts: 1 }
399
+ throwIfAborted(signal)
400
+ const second = await attempt(iid, patchPath, signal)
401
+ if (second.verdict) return { ...second.verdict, attempts: 2 }
402
+ return {
403
+ iid,
404
+ resolved: null,
405
+ error: 'parse-or-timeout',
406
+ raw: second.raw.slice(0, 200),
407
+ attempts: 2,
408
+ }
409
+ },
410
+ { timeoutMs: lockWaitTimeoutMs, signal },
411
+ )
412
+ },
413
+ }
414
+ }