@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,217 @@
1
+ /**
2
+ * Dual calibration — an instance may enter an experiment only if BOTH gates
3
+ * hold, mirroring the experiment's `calibrate.sh` (repro gate) plus the
4
+ * gold-family judge rows M1 reconciles on (official-judge gate):
5
+ *
6
+ * 1. REPRO GATE (`verifyCalibrated`): on a pristine image-materialized
7
+ * workspace the self-repro verify command must FAIL at base_commit and
8
+ * PASS once the official gold patch is applied (git apply, then
9
+ * `patch --fuzz=3` fallback — several Verified gold patches only apply
10
+ * fuzzily to their own base). A verify that can't see the gold fix can't
11
+ * grade an arm's fix.
12
+ *
13
+ * 2. OFFICIAL-JUDGE GOLD GATE (`goldOfficialResolved`): the official swebench
14
+ * judge (via serialized-judge → adapter.judge) must resolve the gold patch
15
+ * itself. psf__requests-2931/-2317 proved a judge can be blind on an
16
+ * instance whose verify calibrates fine — those became the excluded
17
+ * "gold-ungradeable" rows in the M1 denominator.
18
+ */
19
+
20
+ import { rm, writeFile } from 'node:fs/promises'
21
+ import { join } from 'node:path'
22
+ import { pathToFileURL } from 'node:url'
23
+ import { judgeFactoryPatch } from './factory-judge-child.mts'
24
+ import { loadFactoryInstance, loadFactoryInstances, type LoadedFactoryInstance } from './fixtures'
25
+ import { materializeWorkspace } from './materialize'
26
+ import { run, runOk, shq } from './proc'
27
+ import type { SerializedJudge } from './serialized-judge'
28
+
29
+ export interface CalibrateOptions {
30
+ instanceId: string
31
+ image: string
32
+ baseCommit: string
33
+ /** The official gold patch text (task-meta `patch`). */
34
+ goldPatch: string
35
+ /**
36
+ * Self-repro verify command, run via `bash -c` with cwd = the workspace
37
+ * (calibrate.sh ran `bash verify/<iid>.sh` from inside the tree).
38
+ */
39
+ verifyCmd: string
40
+ /** Scratch root; two throwaway workspaces are created and removed under it. */
41
+ workDir: string
42
+ /** Judge for the official gold gate. */
43
+ judge: SerializedJudge
44
+ /** Ceiling for one verify run (repro scripts self-limit at 180s; this is a backstop). */
45
+ verifyTimeoutMs?: number
46
+ /** Keep the calibration workspaces for post-mortem. Default: removed. */
47
+ keepWorkspaces?: boolean
48
+ }
49
+
50
+ export interface CalibrationResult {
51
+ iid: string
52
+ /** rc of verify on pristine base (must be nonzero). */
53
+ baseRc: number
54
+ /** rc of applying the gold patch (0 via git apply or the fuzz fallback). */
55
+ goldApplyRc: number
56
+ /** rc of verify with gold applied (must be zero). */
57
+ goldRc: number
58
+ /** base FAILS and gold PASSES. */
59
+ verifyCalibrated: boolean
60
+ /** Official judge resolves the gold patch. */
61
+ goldOfficialResolved: boolean
62
+ /** verifyCalibrated && goldOfficialResolved — the experiment admission bar. */
63
+ experimentValid: boolean
64
+ }
65
+
66
+ async function runVerify(verifyCmd: string, ws: string, timeoutMs: number): Promise<number> {
67
+ const res = await run('bash', ['-c', verifyCmd], { cwd: ws, timeoutMs })
68
+ return res.code
69
+ }
70
+
71
+ /**
72
+ * Apply a patch file with calibrate.sh's exact fallback chain:
73
+ * `git apply --whitespace=nowarn`, then `patch -p1 --fuzz=3` on failure.
74
+ * Returns the rc of the LAST attempt (0 = applied).
75
+ */
76
+ export async function applyPatchWithFallback(ws: string, patchFile: string): Promise<number> {
77
+ const gitApply = await run('git', ['apply', '--whitespace=nowarn', patchFile], { cwd: ws })
78
+ if (gitApply.code === 0) return 0
79
+ const fuzz = await run('bash', ['-c', `patch -p1 --fuzz=3 < ${shq(patchFile)}`], { cwd: ws })
80
+ return fuzz.code
81
+ }
82
+
83
+ export async function calibrateInstance(opts: CalibrateOptions): Promise<CalibrationResult> {
84
+ const { instanceId, image, baseCommit, verifyCmd, judge } = opts
85
+ const verifyTimeoutMs = opts.verifyTimeoutMs ?? 600_000
86
+ const baseWs = join(opts.workDir, `cal-base-${instanceId}`)
87
+ const goldWs = join(opts.workDir, `cal-gold-${instanceId}`)
88
+ const goldPatchFile = join(opts.workDir, `${instanceId}.gold.patch`)
89
+
90
+ try {
91
+ await materializeWorkspace({ instanceId, image, baseCommit, dest: baseWs })
92
+ const baseRc = await runVerify(verifyCmd, baseWs, verifyTimeoutMs)
93
+
94
+ await materializeWorkspace({ instanceId, image, baseCommit, dest: goldWs })
95
+ await writeFile(goldPatchFile, opts.goldPatch)
96
+ const goldApplyRc = await applyPatchWithFallback(goldWs, goldPatchFile)
97
+ const goldRc = await runVerify(verifyCmd, goldWs, verifyTimeoutMs)
98
+
99
+ const verifyCalibrated = baseRc !== 0 && goldRc === 0
100
+
101
+ const goldVerdict = await judge.judge(instanceId, goldPatchFile, 'gold')
102
+ const goldOfficialResolved = goldVerdict.resolved === true
103
+
104
+ return {
105
+ iid: instanceId,
106
+ baseRc,
107
+ goldApplyRc,
108
+ goldRc,
109
+ verifyCalibrated,
110
+ goldOfficialResolved,
111
+ experimentValid: verifyCalibrated && goldOfficialResolved,
112
+ }
113
+ } finally {
114
+ if (!opts.keepWorkspaces) {
115
+ await rm(baseWs, { recursive: true, force: true })
116
+ await rm(goldWs, { recursive: true, force: true })
117
+ }
118
+ }
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Factory-bench admission gate — the same "calibrate through the OFFICIAL
123
+ // judge" lesson, generalized: gold (the real PR's impl-only diff) must judge
124
+ // resolved, and the bare base (empty patch) must judge unresolved. Both runs
125
+ // go through the SAME judge code the arena uses (judgeFactoryPatch — the
126
+ // factory-judge-child body), deliberately bypassing serialized-judge's
127
+ // empty-patch short-circuit so the base direction really executes the judge
128
+ // tests on the bare tree instead of trivially returning false.
129
+ // ---------------------------------------------------------------------------
130
+
131
+ export interface FactoryCalibrationResult {
132
+ iid: string
133
+ /** Gold = impl-only PR diff. Must be resolved with full score. */
134
+ goldResolved: boolean
135
+ goldPassed: number
136
+ /** Base = empty patch. Must be unresolved. */
137
+ baseResolved: boolean
138
+ basePassed: number
139
+ total: number
140
+ /** goldResolved && !baseResolved — the pool admission bar. */
141
+ admitted: boolean
142
+ }
143
+
144
+ /**
145
+ * The real PR's impl-only patch: full first-parent diff base→judge_ref minus
146
+ * the judge test files (they are the hidden judge, not the deliverable).
147
+ */
148
+ export async function goldImplPatch(inst: LoadedFactoryInstance): Promise<string> {
149
+ const res = await runOk('git', [
150
+ '-C', inst.repo_local_mirror,
151
+ 'diff', inst.base_commit, inst.judge_ref,
152
+ '--', '.',
153
+ ...inst.judge_tests.map((t) => `:(exclude)${t}`),
154
+ ])
155
+ if (res.stdout.trim().length === 0) {
156
+ throw new Error(`calibrate ${inst.id}: impl-only gold diff is empty — judge_tests exclude everything?`)
157
+ }
158
+ return res.stdout
159
+ }
160
+
161
+ /** Run both admission directions for one instance. Throws only on infra failure. */
162
+ export async function calibrateFactoryInstance(inst: LoadedFactoryInstance): Promise<FactoryCalibrationResult> {
163
+ const gold = await judgeFactoryPatch(inst, await goldImplPatch(inst))
164
+ const base = await judgeFactoryPatch(inst, '')
165
+ return {
166
+ iid: inst.id,
167
+ goldResolved: gold.result.resolved,
168
+ goldPassed: gold.result.passed,
169
+ baseResolved: base.result.resolved,
170
+ basePassed: base.result.passed,
171
+ total: inst.judgeTestTotal,
172
+ admitted: gold.result.resolved && !base.result.resolved,
173
+ }
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // CLI: tsx src/swe-arena/calibrate.ts --factory <instancesDirOrInstanceDir> [id ...]
178
+ // Rejection is LOUD: any instance failing either direction exits nonzero.
179
+ // ---------------------------------------------------------------------------
180
+
181
+ const isMain = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href
182
+
183
+ if (isMain) {
184
+ const [mode, root, ...ids] = process.argv.slice(2)
185
+ if (mode !== '--factory' || !root) {
186
+ console.error('usage: tsx src/swe-arena/calibrate.ts --factory <instancesDir|instanceDir> [id ...]')
187
+ process.exit(2)
188
+ }
189
+ let instances: LoadedFactoryInstance[]
190
+ try {
191
+ instances = loadFactoryInstances(root)
192
+ } catch {
193
+ instances = [loadFactoryInstance(root)]
194
+ }
195
+ if (ids.length > 0) {
196
+ const byId = new Map(instances.map((i) => [i.id, i]))
197
+ instances = ids.map((id) => {
198
+ const inst = byId.get(id)
199
+ if (!inst) throw new Error(`unknown instance id ${id} (have: ${[...byId.keys()].join(', ')})`)
200
+ return inst
201
+ })
202
+ }
203
+ let rejected = 0
204
+ for (const inst of instances) {
205
+ const r = await calibrateFactoryInstance(inst)
206
+ const verdict = r.admitted ? 'ADMITTED' : 'REJECTED'
207
+ console.log(
208
+ `CALIBRATE ${r.iid}: gold ${r.goldPassed}/${r.total} resolved=${r.goldResolved}; ` +
209
+ `base ${r.basePassed}/${r.total} resolved=${r.baseResolved} → ${verdict}`,
210
+ )
211
+ if (!r.admitted) rejected += 1
212
+ }
213
+ if (rejected > 0) {
214
+ console.error(`calibration gate: ${rejected} instance(s) REJECTED (gold must pass AND base must fail)`)
215
+ process.exit(1)
216
+ }
217
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Substrate passthrough guard for the improve-loop options this harness
3
+ * depends on: `selfImprove` forwarding `premeasuredBaseline` and
4
+ * `budget.maxImprovementShots` into the loop (merged to agent-eval main).
5
+ *
6
+ * The remaining risk is a STALE INSTALL: the bench consumes agent-eval via a
7
+ * pnpm `file:` dependency, which snapshots the checkout at install time — a
8
+ * rebuilt-but-never-reinstalled substrate silently reverts to a bundle whose
9
+ * `selfImprove` DROPS both options. Silent drop is the worst failure mode
10
+ * here (a "premeasured" baseline would quietly re-run and re-spend; the depth
11
+ * dial would quietly pin to the lib default), so the guard FAILS LOUD instead
12
+ * of falling back.
13
+ *
14
+ * The probe reads the RESOLVED `@tangle-network/agent-eval/contract` module
15
+ * text (the bundle that contains the compiled `selfImprove`) and requires
16
+ * both option names. Verified against the pre-merge build: that bundle
17
+ * contained ZERO occurrences of either literal (selfImprove never named them;
18
+ * `runOptimization`'s own seam compiles into a different chunk), so the probe
19
+ * cannot false-positive on a stale substrate.
20
+ */
21
+
22
+ import { readFileSync } from 'node:fs'
23
+ import { createRequire } from 'node:module'
24
+ import { fileURLToPath } from 'node:url'
25
+
26
+ export interface ImproveLoopPassthroughCaps {
27
+ /** `selfImprove` forwards `premeasuredBaseline` into the loop. */
28
+ premeasuredBaseline: boolean
29
+ /** `selfImprove` forwards `budget.maxImprovementShots` into the loop. */
30
+ maxImprovementShots: boolean
31
+ }
32
+
33
+ /** Pure probe over the contract module text — unit-testable. */
34
+ export function detectPassthroughCaps(contractModuleText: string): ImproveLoopPassthroughCaps {
35
+ return {
36
+ premeasuredBaseline: contractModuleText.includes('premeasuredBaseline'),
37
+ maxImprovementShots: contractModuleText.includes('maxImprovementShots'),
38
+ }
39
+ }
40
+
41
+ /** Resolve the installed agent-eval contract bundle's file path. */
42
+ export function resolveContractModulePath(): string {
43
+ const require = createRequire(import.meta.url)
44
+ const url = import.meta.resolve?.('@tangle-network/agent-eval/contract')
45
+ if (typeof url === 'string' && url.startsWith('file:')) return fileURLToPath(url)
46
+ return require.resolve('@tangle-network/agent-eval/contract')
47
+ }
48
+
49
+ /** Fail-loud stale-install guard: throws unless the resolved substrate names
50
+ * BOTH passthrough options in its contract bundle. An unreadable bundle also
51
+ * throws — nothing here ever downgrades to a silent fallback. */
52
+ export function assertSubstratePassthroughs(log: (msg: string) => void = () => {}): void {
53
+ let path: string
54
+ let caps: ImproveLoopPassthroughCaps
55
+ try {
56
+ path = resolveContractModulePath()
57
+ caps = detectPassthroughCaps(readFileSync(path, 'utf8'))
58
+ } catch (cause) {
59
+ throw new Error(
60
+ 'substrate passthrough probe failed — cannot prove the installed agent-eval forwards ' +
61
+ `premeasuredBaseline/maxImprovementShots: ${(cause as Error).message}`,
62
+ { cause },
63
+ )
64
+ }
65
+ log(
66
+ `substrate caps (${path}): premeasuredBaseline=${caps.premeasuredBaseline} maxImprovementShots=${caps.maxImprovementShots}`,
67
+ )
68
+ const missing = (Object.keys(caps) as Array<keyof ImproveLoopPassthroughCaps>).filter((k) => !caps[k])
69
+ if (missing.length > 0) {
70
+ throw new Error(
71
+ `stale substrate install: the resolved agent-eval contract bundle (${path}) never names ` +
72
+ `${missing.join(' + ')}, so selfImprove would silently drop the option(s). ` +
73
+ 'Rebuild the checkout (cd ~/code/agent-eval && git pull && pnpm build), then `pnpm install --force` in the bench.',
74
+ )
75
+ }
76
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Substrate passthrough guard: the pure probe over module text plus the live
3
+ * contract check against the installed agent-eval bundle. The guard must pass
4
+ * cleanly on a substrate that threads premeasuredBaseline +
5
+ * maxImprovementShots and THROW LOUD (never fall back) on one that drops
6
+ * them — so a run against a stale install dies at t≈0, before it can
7
+ * silently re-spend its baseline.
8
+ */
9
+
10
+ import { readFileSync } from 'node:fs'
11
+ import { describe, expect, it } from 'vitest'
12
+ import {
13
+ assertSubstratePassthroughs,
14
+ detectPassthroughCaps,
15
+ resolveContractModulePath,
16
+ } from './capabilities.mts'
17
+
18
+ describe('detectPassthroughCaps (pure)', () => {
19
+ it('an option name absent from the bundle reads as no capability', () => {
20
+ expect(detectPassthroughCaps('')).toEqual({ premeasuredBaseline: false, maxImprovementShots: false })
21
+ expect(detectPassthroughCaps('function selfImprove(opts) { return runSelfImprove(opts) }')).toEqual({
22
+ premeasuredBaseline: false,
23
+ maxImprovementShots: false,
24
+ })
25
+ })
26
+
27
+ it('the forwarding literals flip their capability independently', () => {
28
+ expect(detectPassthroughCaps('premeasuredBaseline: opts.premeasuredBaseline,')).toEqual({
29
+ premeasuredBaseline: true,
30
+ maxImprovementShots: false,
31
+ })
32
+ expect(detectPassthroughCaps('maxImprovementShots: budget.maxImprovementShots,')).toEqual({
33
+ premeasuredBaseline: false,
34
+ maxImprovementShots: true,
35
+ })
36
+ })
37
+ })
38
+
39
+ describe('live substrate guard', () => {
40
+ it('passes on a substrate that names both passthroughs, throws loud otherwise', () => {
41
+ const path = resolveContractModulePath()
42
+ expect(path).toMatch(/agent-eval/)
43
+ const caps = detectPassthroughCaps(readFileSync(path, 'utf8'))
44
+ // The guard's contract holds against whatever substrate is installed:
45
+ // both literals present ⇒ clean pass with the caps logged; anything less
46
+ // ⇒ a loud actionable error naming the stale bundle (never a silent
47
+ // fallback). A run against a stale install dies HERE, at t≈0.
48
+ if (caps.premeasuredBaseline && caps.maxImprovementShots) {
49
+ const logged: string[] = []
50
+ expect(() => assertSubstratePassthroughs((msg) => logged.push(msg))).not.toThrow()
51
+ expect(logged.join('\n')).toContain('premeasuredBaseline=true')
52
+ expect(logged.join('\n')).toContain('maxImprovementShots=true')
53
+ } else {
54
+ expect(() => assertSubstratePassthroughs()).toThrow(/stale substrate install/)
55
+ }
56
+ })
57
+ })
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Endpoint capacity gate — the typed port of `probe-capacity.sh`, generalized
3
+ * after a proven blind spot: the bash probe watched ONLY the z.ai coding
4
+ * endpoint while the supervisor BRAIN rides router.tangle.tools — three
5
+ * evolution rounds went infra-null because the gate said "capacity" while the
6
+ * router 503-stormed. Rule encoded here: gate every arm on the endpoint that
7
+ * arm actually calls; supervisor arms MUST include the router-path probe.
8
+ *
9
+ * Secrets discipline: probes spawn `dotenvx run … -- bash -c 'curl …'` from
10
+ * the secrets dir; the API key is referenced by NAME inside the child shell
11
+ * (single-quoted script, so it is never expanded — let alone logged — in this
12
+ * process).
13
+ */
14
+
15
+ import { run } from './proc'
16
+ import type { SecretsEnv } from './arms'
17
+
18
+ export type CapacityProbe = (signal?: AbortSignal) => Promise<boolean>
19
+
20
+ export interface EndpointCapacityGate {
21
+ /** Human label for status lines (e.g. 'z.ai-coding', 'router'). */
22
+ name: string
23
+ probe: CapacityProbe
24
+ /** Window passes when >= k of n probes succeed (bash: 3 of 4). */
25
+ kOfN: { k: number; n: number }
26
+ /** Consecutive passing windows required before opening. Default 1. */
27
+ steadyM?: number
28
+ /** Total wait budget; exceeded → gate reports no-capacity (orchestrate: 300 min). */
29
+ waitCeilingMs: number
30
+ /** Pause between probes inside a window (bash: 1s). */
31
+ probeIntervalMs?: number
32
+ /** Pause between windows while waiting (orchestrate: 30s). */
33
+ retryDelayMs?: number
34
+ onStatus?: (msg: string) => void
35
+ }
36
+
37
+ export function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void> {
38
+ signal?.throwIfAborted()
39
+ return new Promise((resolve, reject) => {
40
+ let timer: NodeJS.Timeout | undefined
41
+ const onAbort = () => {
42
+ if (timer) clearTimeout(timer)
43
+ signal?.removeEventListener('abort', onAbort)
44
+ reject(signal?.reason ?? new Error('capacity wait aborted'))
45
+ }
46
+ timer = setTimeout(() => {
47
+ signal?.removeEventListener('abort', onAbort)
48
+ resolve()
49
+ }, ms)
50
+ signal?.addEventListener('abort', onAbort, { once: true })
51
+ })
52
+ }
53
+
54
+ /** One k-of-n probe window. Exported for direct reuse (bash probe-capacity.sh body). */
55
+ export async function probeWindow(
56
+ gate: EndpointCapacityGate,
57
+ signal?: AbortSignal,
58
+ ): Promise<{ ok: number; passed: boolean }> {
59
+ const { k, n } = gate.kOfN
60
+ let ok = 0
61
+ for (let i = 0; i < n; i++) {
62
+ signal?.throwIfAborted()
63
+ try {
64
+ if (await gate.probe(signal)) ok += 1
65
+ } catch {
66
+ // Endpoint failures count as a failed probe; caller cancellation does not.
67
+ signal?.throwIfAborted()
68
+ }
69
+ signal?.throwIfAborted()
70
+ if (i < n - 1) await sleepWithSignal(gate.probeIntervalMs ?? 1000, signal)
71
+ }
72
+ return { ok, passed: ok >= k }
73
+ }
74
+
75
+ /**
76
+ * Block until the endpoint shows steady capacity (steadyM consecutive passing
77
+ * k-of-n windows) or the ceiling elapses. Returns whether capacity was found —
78
+ * callers decide whether a closed gate skips the instance or aborts the run.
79
+ */
80
+ export async function waitForCapacity(gate: EndpointCapacityGate, signal?: AbortSignal): Promise<boolean> {
81
+ signal?.throwIfAborted()
82
+ const steadyM = gate.steadyM ?? 1
83
+ const deadline = Date.now() + gate.waitCeilingMs
84
+ let consecutive = 0
85
+ for (;;) {
86
+ signal?.throwIfAborted()
87
+ const { ok, passed } = await probeWindow(gate, signal)
88
+ signal?.throwIfAborted()
89
+ gate.onStatus?.(`[${gate.name}] capacity: ${ok}/${gate.kOfN.n}${passed ? '' : ' (below k)'} steady=${passed ? consecutive + 1 : 0}/${steadyM}`)
90
+ if (passed) {
91
+ consecutive += 1
92
+ if (consecutive >= steadyM) return true
93
+ } else {
94
+ consecutive = 0
95
+ }
96
+ if (Date.now() >= deadline) return false
97
+ await sleepWithSignal(gate.retryDelayMs ?? 30_000, signal)
98
+ }
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Probe functions.
103
+ // ---------------------------------------------------------------------------
104
+
105
+ export interface HttpProbeSpec {
106
+ url: string
107
+ /** NAME of the env var holding the bearer key (resolved in the dotenvx child). */
108
+ apiKeyEnv: string
109
+ model: string
110
+ secrets: SecretsEnv
111
+ /** curl --max-time, seconds. Default 40 (probe-capacity.sh). */
112
+ maxTimeS?: number
113
+ /**
114
+ * max_tokens in the probe body. Default 8000 — glm-5.2 returns empty content
115
+ * below that (measured), and an empty-content 200 would be a lying probe.
116
+ */
117
+ maxTokens?: number
118
+ }
119
+
120
+ export const ZAI_CODING_ENDPOINT = 'https://api.z.ai/api/coding/paas/v4/chat/completions'
121
+ export const ROUTER_ENDPOINT = 'https://router.tangle.tools/v1/chat/completions'
122
+
123
+ /** Build the probe request body (probe-body.json semantics). */
124
+ export function probeBody(model: string, maxTokens: number): string {
125
+ return JSON.stringify({
126
+ model,
127
+ messages: [{ role: 'user', content: 'Reply with the single word OK.' }],
128
+ max_tokens: maxTokens,
129
+ temperature: 0,
130
+ })
131
+ }
132
+
133
+ /**
134
+ * Generic chat-completions probe: true iff the endpoint returns HTTP 200
135
+ * within the time budget. The key stays inside the child shell.
136
+ */
137
+ export function httpCapacityProbe(spec: HttpProbeSpec): CapacityProbe {
138
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(spec.apiKeyEnv)) {
139
+ throw new Error(`invalid apiKeyEnv name: ${spec.apiKeyEnv}`)
140
+ }
141
+ const body = probeBody(spec.model, spec.maxTokens ?? 8000)
142
+ const maxTime = spec.maxTimeS ?? 40
143
+ return async (signal?: AbortSignal) => {
144
+ signal?.throwIfAborted()
145
+ // Body via stdin (--data @-) so the payload never sits on a command line.
146
+ // The HTTP code is marker-anchored because dotenvx writes its injection
147
+ // banner to the same stdout stream.
148
+ const script =
149
+ `curl -sS -o /dev/null -w "HTTP_CODE=%{http_code}" --max-time ${maxTime} ` +
150
+ `-X POST "$PROBE_URL" ` +
151
+ `-H "Authorization: Bearer $${spec.apiKeyEnv}" -H "Content-Type: application/json" ` +
152
+ `--data @-`
153
+ const argv = ['run', ...spec.secrets.envFiles.flatMap((f) => ['-f', f]), '--', 'bash', '-c', script]
154
+ const res = await run('dotenvx', argv, {
155
+ cwd: spec.secrets.secretsDir,
156
+ timeoutMs: (maxTime + 20) * 1000,
157
+ stdin: body,
158
+ env: { ...process.env, PROBE_URL: spec.url },
159
+ signal,
160
+ })
161
+ signal?.throwIfAborted()
162
+ return /HTTP_CODE=200\s*$/.test(res.stdout)
163
+ }
164
+ }
165
+
166
+ /** probe-capacity.sh's z.ai coding-plan probe (the WORKER path). */
167
+ export function zaiCodingProbe(secrets: SecretsEnv, model = 'glm-5.2'): CapacityProbe {
168
+ return httpCapacityProbe({ url: ZAI_CODING_ENDPOINT, apiKeyEnv: 'ZAI_API_KEY', model, secrets })
169
+ }
170
+
171
+ /**
172
+ * Router-path probe (the BRAIN path — router.tangle.tools with TANGLE_API_KEY).
173
+ * Supervisor arms must gate on this; probing only z.ai is the proven blind spot.
174
+ */
175
+ export function routerProbe(secrets: SecretsEnv, model = 'glm-5.2'): CapacityProbe {
176
+ return httpCapacityProbe({ url: ROUTER_ENDPOINT, apiKeyEnv: 'TANGLE_API_KEY', model, secrets })
177
+ }
178
+
179
+ /** The gates an arm must pass, by kind: solo → worker path; supervisor → BOTH paths. */
180
+ export function gatesForArmKind(
181
+ kind: 'solo' | 'supervisor',
182
+ secrets: SecretsEnv,
183
+ opts: { waitCeilingMs?: number; model?: string; onStatus?: (msg: string) => void } = {},
184
+ ): EndpointCapacityGate[] {
185
+ const base = {
186
+ kOfN: { k: 3, n: 4 },
187
+ waitCeilingMs: opts.waitCeilingMs ?? 300 * 60_000,
188
+ ...(opts.onStatus ? { onStatus: opts.onStatus } : {}),
189
+ }
190
+ const worker: EndpointCapacityGate = { name: 'z.ai-coding', probe: zaiCodingProbe(secrets, opts.model), ...base }
191
+ if (kind === 'solo') return [worker]
192
+ const brain: EndpointCapacityGate = { name: 'router', probe: routerProbe(secrets, opts.model), ...base }
193
+ return [worker, brain]
194
+ }