@tangle-network/agent-bench 0.3.5 → 0.3.7

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 (262) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +13 -1
  3. package/dist/adapters.d.ts +15 -0
  4. package/dist/adapters.js +43 -0
  5. package/dist/adapters.js.map +1 -0
  6. package/dist/benchmarks/_harness.d.ts +125 -0
  7. package/dist/benchmarks/_harness.js +33 -0
  8. package/dist/benchmarks/_harness.js.map +1 -0
  9. package/dist/benchmarks/aec-bench.d.ts +27 -0
  10. package/dist/benchmarks/aec-bench.js +8 -0
  11. package/dist/benchmarks/aec-bench.js.map +1 -0
  12. package/dist/benchmarks/agentbench.d.ts +16 -0
  13. package/dist/benchmarks/agentbench.js +10 -0
  14. package/dist/benchmarks/agentbench.js.map +1 -0
  15. package/dist/benchmarks/appworld.d.ts +37 -0
  16. package/dist/benchmarks/appworld.js +14 -0
  17. package/dist/benchmarks/appworld.js.map +1 -0
  18. package/dist/benchmarks/bfcl.d.ts +18 -0
  19. package/dist/benchmarks/bfcl.js +10 -0
  20. package/dist/benchmarks/bfcl.js.map +1 -0
  21. package/dist/benchmarks/cad-design.d.ts +45 -0
  22. package/dist/benchmarks/cad-design.js +7 -0
  23. package/dist/benchmarks/cad-design.js.map +1 -0
  24. package/dist/benchmarks/cadbench.d.ts +19 -0
  25. package/dist/benchmarks/cadbench.js +8 -0
  26. package/dist/benchmarks/cadbench.js.map +1 -0
  27. package/dist/benchmarks/cadgenbench.d.ts +22 -0
  28. package/dist/benchmarks/cadgenbench.js +8 -0
  29. package/dist/benchmarks/cadgenbench.js.map +1 -0
  30. package/dist/benchmarks/commit0.d.ts +31 -0
  31. package/dist/benchmarks/commit0.js +10 -0
  32. package/dist/benchmarks/commit0.js.map +1 -0
  33. package/dist/benchmarks/crag.d.ts +14 -0
  34. package/dist/benchmarks/crag.js +9 -0
  35. package/dist/benchmarks/crag.js.map +1 -0
  36. package/dist/benchmarks/dabstep.d.ts +18 -0
  37. package/dist/benchmarks/dabstep.js +10 -0
  38. package/dist/benchmarks/dabstep.js.map +1 -0
  39. package/dist/benchmarks/enterpriseops-gym.d.ts +38 -0
  40. package/dist/benchmarks/enterpriseops-gym.js +10 -0
  41. package/dist/benchmarks/enterpriseops-gym.js.map +1 -0
  42. package/dist/benchmarks/finresearchbench.d.ts +15 -0
  43. package/dist/benchmarks/finresearchbench.js +8 -0
  44. package/dist/benchmarks/finresearchbench.js.map +1 -0
  45. package/dist/benchmarks/finsearchcomp.d.ts +49 -0
  46. package/dist/benchmarks/finsearchcomp.js +7 -0
  47. package/dist/benchmarks/finsearchcomp.js.map +1 -0
  48. package/dist/benchmarks/frames.d.ts +59 -0
  49. package/dist/benchmarks/frames.js +13 -0
  50. package/dist/benchmarks/frames.js.map +1 -0
  51. package/dist/benchmarks/hotpotqa.d.ts +48 -0
  52. package/dist/benchmarks/hotpotqa.js +15 -0
  53. package/dist/benchmarks/hotpotqa.js.map +1 -0
  54. package/dist/benchmarks/humaneval.d.ts +62 -0
  55. package/dist/benchmarks/humaneval.js +17 -0
  56. package/dist/benchmarks/humaneval.js.map +1 -0
  57. package/dist/benchmarks/mind2web.d.ts +41 -0
  58. package/dist/benchmarks/mind2web.js +9 -0
  59. package/dist/benchmarks/mind2web.js.map +1 -0
  60. package/dist/benchmarks/nomiracl.d.ts +15 -0
  61. package/dist/benchmarks/nomiracl.js +9 -0
  62. package/dist/benchmarks/nomiracl.js.map +1 -0
  63. package/dist/benchmarks/open-rag-bench.d.ts +14 -0
  64. package/dist/benchmarks/open-rag-bench.js +9 -0
  65. package/dist/benchmarks/open-rag-bench.js.map +1 -0
  66. package/dist/benchmarks/programbench.d.ts +38 -0
  67. package/dist/benchmarks/programbench.js +10 -0
  68. package/dist/benchmarks/programbench.js.map +1 -0
  69. package/dist/benchmarks/rag-shared.d.ts +42 -0
  70. package/dist/benchmarks/rag-shared.js +39 -0
  71. package/dist/benchmarks/rag-shared.js.map +1 -0
  72. package/dist/benchmarks/ragbench.d.ts +16 -0
  73. package/dist/benchmarks/ragbench.js +9 -0
  74. package/dist/benchmarks/ragbench.js.map +1 -0
  75. package/dist/benchmarks/simpleqa.d.ts +64 -0
  76. package/dist/benchmarks/simpleqa.js +11 -0
  77. package/dist/benchmarks/simpleqa.js.map +1 -0
  78. package/dist/benchmarks/swe-bench.d.ts +56 -0
  79. package/dist/benchmarks/swe-bench.js +14 -0
  80. package/dist/benchmarks/swe-bench.js.map +1 -0
  81. package/dist/benchmarks/t2-ragbench.d.ts +14 -0
  82. package/dist/benchmarks/t2-ragbench.js +9 -0
  83. package/dist/benchmarks/t2-ragbench.js.map +1 -0
  84. package/dist/benchmarks/tau-bench-shared.d.ts +26 -0
  85. package/dist/benchmarks/tau-bench-shared.js +10 -0
  86. package/dist/benchmarks/tau-bench-shared.js.map +1 -0
  87. package/dist/benchmarks/tau2-bench.d.ts +7 -0
  88. package/dist/benchmarks/tau2-bench.js +11 -0
  89. package/dist/benchmarks/tau2-bench.js.map +1 -0
  90. package/dist/benchmarks/tau3-banking.d.ts +15 -0
  91. package/dist/benchmarks/tau3-banking.js +9 -0
  92. package/dist/benchmarks/tau3-banking.js.map +1 -0
  93. package/dist/benchmarks/terminal-bench.d.ts +24 -0
  94. package/dist/benchmarks/terminal-bench.js +8 -0
  95. package/dist/benchmarks/terminal-bench.js.map +1 -0
  96. package/dist/benchmarks/toollm.d.ts +16 -0
  97. package/dist/benchmarks/toollm.js +10 -0
  98. package/dist/benchmarks/toollm.js.map +1 -0
  99. package/dist/benchmarks/trata-hedge.d.ts +32 -0
  100. package/dist/benchmarks/trata-hedge.js +7 -0
  101. package/dist/benchmarks/trata-hedge.js.map +1 -0
  102. package/dist/benchmarks/types.d.ts +107 -0
  103. package/dist/benchmarks/types.js +1 -0
  104. package/dist/benchmarks/types.js.map +1 -0
  105. package/dist/benchmarks/webarena-verified.d.ts +16 -0
  106. package/dist/benchmarks/webarena-verified.js +10 -0
  107. package/dist/benchmarks/webarena-verified.js.map +1 -0
  108. package/dist/chunk-2PVVP7GN.js +197 -0
  109. package/dist/chunk-2PVVP7GN.js.map +1 -0
  110. package/dist/chunk-2XU6OGEN.js +170 -0
  111. package/dist/chunk-2XU6OGEN.js.map +1 -0
  112. package/dist/chunk-53UPUNBZ.js +325 -0
  113. package/dist/chunk-53UPUNBZ.js.map +1 -0
  114. package/dist/chunk-5H5XV76F.js +240 -0
  115. package/dist/chunk-5H5XV76F.js.map +1 -0
  116. package/dist/chunk-7WSD27QQ.js +118 -0
  117. package/dist/chunk-7WSD27QQ.js.map +1 -0
  118. package/dist/chunk-C7T7WEK2.js +103 -0
  119. package/dist/chunk-C7T7WEK2.js.map +1 -0
  120. package/dist/chunk-CKUVRZ2T.js +251 -0
  121. package/dist/chunk-CKUVRZ2T.js.map +1 -0
  122. package/dist/chunk-HBSWHQNJ.js +30 -0
  123. package/dist/chunk-HBSWHQNJ.js.map +1 -0
  124. package/dist/chunk-HHXFIHXC.js +116 -0
  125. package/dist/chunk-HHXFIHXC.js.map +1 -0
  126. package/dist/chunk-IFAV6KEM.js +276 -0
  127. package/dist/chunk-IFAV6KEM.js.map +1 -0
  128. package/dist/chunk-INNOYXCP.js +387 -0
  129. package/dist/chunk-INNOYXCP.js.map +1 -0
  130. package/dist/chunk-J3KDJNX2.js +182 -0
  131. package/dist/chunk-J3KDJNX2.js.map +1 -0
  132. package/dist/chunk-JRWWGMK7.js +148 -0
  133. package/dist/chunk-JRWWGMK7.js.map +1 -0
  134. package/dist/chunk-JTHWEDEW.js +32 -0
  135. package/dist/chunk-JTHWEDEW.js.map +1 -0
  136. package/dist/chunk-KDIKRJGB.js +120 -0
  137. package/dist/chunk-KDIKRJGB.js.map +1 -0
  138. package/dist/chunk-LRRD7NAG.js +301 -0
  139. package/dist/chunk-LRRD7NAG.js.map +1 -0
  140. package/dist/chunk-ODT47UAY.js +221 -0
  141. package/dist/chunk-ODT47UAY.js.map +1 -0
  142. package/dist/chunk-PA2ZKHJC.js +230 -0
  143. package/dist/chunk-PA2ZKHJC.js.map +1 -0
  144. package/dist/chunk-PUIRNYI7.js +189 -0
  145. package/dist/chunk-PUIRNYI7.js.map +1 -0
  146. package/dist/chunk-PWQVGAJB.js +144 -0
  147. package/dist/chunk-PWQVGAJB.js.map +1 -0
  148. package/dist/chunk-R36V2VP7.js +169 -0
  149. package/dist/chunk-R36V2VP7.js.map +1 -0
  150. package/dist/chunk-R67DFVLO.js +142 -0
  151. package/dist/chunk-R67DFVLO.js.map +1 -0
  152. package/dist/chunk-SEVJPLZC.js +260 -0
  153. package/dist/chunk-SEVJPLZC.js.map +1 -0
  154. package/dist/chunk-SYDW647C.js +318 -0
  155. package/dist/chunk-SYDW647C.js.map +1 -0
  156. package/dist/chunk-TBKU5XQI.js +228 -0
  157. package/dist/chunk-TBKU5XQI.js.map +1 -0
  158. package/dist/chunk-TSWPNOYM.js +147 -0
  159. package/dist/chunk-TSWPNOYM.js.map +1 -0
  160. package/dist/chunk-UAIOHCUK.js +27 -0
  161. package/dist/chunk-UAIOHCUK.js.map +1 -0
  162. package/dist/chunk-UPAMRDX4.js +233 -0
  163. package/dist/chunk-UPAMRDX4.js.map +1 -0
  164. package/dist/chunk-VQRS7VUC.js +342 -0
  165. package/dist/chunk-VQRS7VUC.js.map +1 -0
  166. package/dist/chunk-X3BTXCJ4.js +262 -0
  167. package/dist/chunk-X3BTXCJ4.js.map +1 -0
  168. package/dist/chunk-X5YKXC6V.js +211 -0
  169. package/dist/chunk-X5YKXC6V.js.map +1 -0
  170. package/dist/chunk-Y6O2OCUO.js +130 -0
  171. package/dist/chunk-Y6O2OCUO.js.map +1 -0
  172. package/dist/chunk-YCGY7UIZ.js +208 -0
  173. package/dist/chunk-YCGY7UIZ.js.map +1 -0
  174. package/dist/chunk-Z7ML6L77.js +162 -0
  175. package/dist/chunk-Z7ML6L77.js.map +1 -0
  176. package/dist/chunk-ZEWMTR5M.js +136 -0
  177. package/dist/chunk-ZEWMTR5M.js.map +1 -0
  178. package/dist/index.d.ts +355 -0
  179. package/dist/index.js +1908 -0
  180. package/dist/index.js.map +1 -0
  181. package/package.json +26 -9
  182. package/scripts/run-package-tests.mjs +30 -8
  183. package/scripts/verify-packed-consumer.mjs +12 -1
  184. package/scripts/verify-pier-agent.mts +1 -0
  185. package/src/benchmarks/humaneval.test.mts +122 -0
  186. package/src/benchmarks/humaneval.ts +100 -27
  187. package/src/david-attribution.mts +78 -0
  188. package/src/david-goliath.mts +149 -0
  189. package/src/hev-improve.mts +25 -6
  190. package/src/humaneval-object-ablation.mts +201 -0
  191. package/src/live-improve-campaign-mbpp.mts +641 -0
  192. package/src/live-improve-campaign.mts +500 -0
  193. package/src/mbpp-structural.mts +12 -7
  194. package/src/stream-observe.py +45 -0
  195. package/src/stream-observe.tpl.html +247 -0
  196. package/src/supervisor-arena.mts +816 -0
  197. package/src/swe-arena/analyze.ts +211 -0
  198. package/src/swe-arena/arms.ts +788 -0
  199. package/src/swe-arena/bootstrap-meta.mts +188 -0
  200. package/src/swe-arena/bootstrap-meta.test.mts +51 -0
  201. package/src/swe-arena/calibrate.ts +116 -0
  202. package/src/swe-arena/capabilities.mts +76 -0
  203. package/src/swe-arena/capabilities.test.mts +57 -0
  204. package/src/swe-arena/capacity.ts +194 -0
  205. package/src/swe-arena/cell-evidence.mts +405 -0
  206. package/src/swe-arena/cell-evidence.test.mts +248 -0
  207. package/src/swe-arena/diagnosis-ensemble.test.mts +210 -0
  208. package/src/swe-arena/diagnosis-ensemble.ts +520 -0
  209. package/src/swe-arena/execution.test.mts +1170 -0
  210. package/src/swe-arena/fixtures/analyze.py +80 -0
  211. package/src/swe-arena/fixtures/excludes.txt +8 -0
  212. package/src/swe-arena/fixtures/gen1-salvage/README.md +45 -0
  213. package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +116 -0
  214. package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +293 -0
  215. package/src/swe-arena/fixtures/holdout-preregister.log +12 -0
  216. package/src/swe-arena/fixtures/holdout.json +44 -0
  217. package/src/swe-arena/fixtures/instances.json +146 -0
  218. package/src/swe-arena/fixtures/ledger.jsonl +12 -0
  219. package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +36 -0
  220. package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +33 -0
  221. package/src/swe-arena/fixtures/rejudge.jsonl +15 -0
  222. package/src/swe-arena/fixtures/rematch.jsonl +3 -0
  223. package/src/swe-arena/fixtures/rematch2.jsonl +3 -0
  224. package/src/swe-arena/fixtures/rematch3.jsonl +3 -0
  225. package/src/swe-arena/fixtures/sup-journal-true.json +19 -0
  226. package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +48 -0
  227. package/src/swe-arena/fixtures/verify/django__django-11532.sh +50 -0
  228. package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +76 -0
  229. package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +44 -0
  230. package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +32 -0
  231. package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +51 -0
  232. package/src/swe-arena/fixtures/worker-tokens.json +42 -0
  233. package/src/swe-arena/fixtures.ts +104 -0
  234. package/src/swe-arena/holdout-certify.mts +408 -0
  235. package/src/swe-arena/holdout-certify.test.mts +160 -0
  236. package/src/swe-arena/judge-child.mts +37 -0
  237. package/src/swe-arena/manifest.mts +293 -0
  238. package/src/swe-arena/manifest.test.mts +169 -0
  239. package/src/swe-arena/materialize.ts +142 -0
  240. package/src/swe-arena/outer-loop.mts +2145 -0
  241. package/src/swe-arena/outer-loop.test.mts +696 -0
  242. package/src/swe-arena/parity.test.mts +87 -0
  243. package/src/swe-arena/proc.test.mts +174 -0
  244. package/src/swe-arena/proc.ts +260 -0
  245. package/src/swe-arena/profiles/default-author.profile.json +4 -0
  246. package/src/swe-arena/proposer-fanout.mts +489 -0
  247. package/src/swe-arena/proposer-fanout.test.mts +372 -0
  248. package/src/swe-arena/reconcile.ts +0 -0
  249. package/src/swe-arena/replay.mts +183 -0
  250. package/src/swe-arena/replay.test.mts +300 -0
  251. package/src/swe-arena/run-experiment.mts +361 -0
  252. package/src/swe-arena/run-supervisor.mjs +297 -0
  253. package/src/swe-arena/run-supervisor.test.mts +498 -0
  254. package/src/swe-arena/serialized-judge.ts +414 -0
  255. package/src/swe-arena/types.ts +166 -0
  256. package/src/swe-code-improve.mts +328 -0
  257. package/src/swe-emit-patch.mts +104 -0
  258. package/src/swe-improve.mts +232 -0
  259. package/src/swe-jail.ts +2 -2
  260. package/src/swe-local-proof.mts +169 -0
  261. package/src/swe-repro-calibrate.mts +446 -0
  262. package/src/swe-stream.mts +1497 -0
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Metadata bootstrap — regenerates every per-instance input the experiment
3
+ * used to hand-generate into its scratchpad (`task-meta.json` via
4
+ * `load_meta.py`) straight from the SWE-bench Verified dataset through the
5
+ * bench adapter. The HF dataset cache under ~/.cache survives host reboots;
6
+ * the scratchpad does not — this module removes that scratch dependency
7
+ * permanently (proven necessary: a reboot wiped /tmp and took task-meta.json,
8
+ * the verify scripts, and the round configs with it).
9
+ *
10
+ * tsx src/swe-arena/bootstrap-meta.mts --task-meta <out.json> [iid...]
11
+ * tsx src/swe-arena/bootstrap-meta.mts --problems <outDir> [iid...]
12
+ * tsx src/swe-arena/bootstrap-meta.mts --calibrate <verifyDir> <workDir> [iid...]
13
+ *
14
+ * `iid...` defaults to the round-4 improvement set (defaultRound4Config).
15
+ *
16
+ * HONESTY FIREWALL: `--problems` writes ONLY problem statements — the mode a
17
+ * verify-script author is allowed to read. Gold patches surface ONLY on the
18
+ * `--calibrate` path, where calibrate.ts applies them MECHANICALLY (git apply
19
+ * / patch --fuzz=3) and the official judge grades them; no author-facing
20
+ * output ever contains patch/test_patch/FAIL_TO_PASS content.
21
+ */
22
+
23
+ import { mkdir, writeFile } from 'node:fs/promises'
24
+ import { join, resolve } from 'node:path'
25
+ import { pathToFileURL } from 'node:url'
26
+ import { createSweBenchAdapter } from '../benchmarks/swe-bench.ts'
27
+ import { calibrateInstance, type CalibrationResult } from './calibrate.ts'
28
+ import { defaultRound4Config } from './outer-loop.mts'
29
+ import { loadInstanceImages } from './run-experiment.mts'
30
+ import { createSerializedJudge } from './serialized-judge.ts'
31
+ import type { SweInstance } from './types.ts'
32
+
33
+ /** Validate one adapter task-metadata record into the SweInstance shape the
34
+ * harness consumes. Throws on any missing/empty load-bearing field — a blank
35
+ * problem statement or gold patch must never flow silently into a run. */
36
+ export function assertSweInstance(id: string, metadata: Record<string, unknown> | undefined): SweInstance {
37
+ if (!metadata) throw new Error(`bootstrap-meta: ${id} has no metadata on its adapter task`)
38
+ const str = (key: keyof SweInstance): string => {
39
+ const v = metadata[key]
40
+ if (typeof v !== 'string' || v.length === 0) {
41
+ throw new Error(`bootstrap-meta: ${id} has a missing/empty ${String(key)}`)
42
+ }
43
+ return v
44
+ }
45
+ const optStr = (key: keyof SweInstance): string | null => {
46
+ const v = metadata[key]
47
+ return typeof v === 'string' && v.length > 0 ? v : null
48
+ }
49
+ const instance: SweInstance = {
50
+ instance_id: str('instance_id'),
51
+ repo: str('repo'),
52
+ base_commit: str('base_commit'),
53
+ problem_statement: str('problem_statement'),
54
+ patch: str('patch'),
55
+ test_patch: str('test_patch'),
56
+ FAIL_TO_PASS: str('FAIL_TO_PASS'),
57
+ PASS_TO_PASS: str('PASS_TO_PASS'),
58
+ version: optStr('version'),
59
+ environment_setup_commit: optStr('environment_setup_commit'),
60
+ }
61
+ if (instance.instance_id !== id) {
62
+ throw new Error(`bootstrap-meta: metadata instance_id ${instance.instance_id} != requested ${id}`)
63
+ }
64
+ return instance
65
+ }
66
+
67
+ /** One dataset load for all ids, via the adapter (single source of truth). */
68
+ export async function loadSweInstances(ids: string[]): Promise<Map<string, SweInstance>> {
69
+ if (ids.length === 0) throw new Error('bootstrap-meta: empty instance list')
70
+ const adapter = createSweBenchAdapter()
71
+ const tasks = await adapter.loadTasks({ ids, split: 'test' })
72
+ const byId = new Map(tasks.map((t) => [t.id, t]))
73
+ const out = new Map<string, SweInstance>()
74
+ for (const id of ids) {
75
+ const task = byId.get(id)
76
+ if (!task) throw new Error(`bootstrap-meta: ${id} not found in SWE-bench_Verified`)
77
+ out.set(id, assertSweInstance(id, task.metadata as Record<string, unknown> | undefined))
78
+ }
79
+ return out
80
+ }
81
+
82
+ /** task-meta.json — the experiment's exact shape: `{ [iid]: SweInstance }`. */
83
+ export async function writeTaskMeta(ids: string[], outPath: string): Promise<void> {
84
+ const instances = await loadSweInstances(ids)
85
+ await writeFile(outPath, JSON.stringify(Object.fromEntries(instances), null, 1) + '\n')
86
+ }
87
+
88
+ /** <outDir>/<iid>.problem.md — problem statements ONLY (author-safe). */
89
+ export async function writeProblemStatements(ids: string[], outDir: string): Promise<string[]> {
90
+ const instances = await loadSweInstances(ids)
91
+ await mkdir(outDir, { recursive: true })
92
+ const written: string[] = []
93
+ for (const [id, inst] of instances) {
94
+ const path = join(outDir, `${id}.problem.md`)
95
+ await writeFile(path, inst.problem_statement)
96
+ written.push(path)
97
+ }
98
+ return written
99
+ }
100
+
101
+ /** Dual-calibrate each instance through the M2 path: repro base-fail/gold-pass
102
+ * plus the official-judge gold gate. Gold patches come from the adapter and
103
+ * are only ever applied mechanically. */
104
+ export async function calibrateIds(
105
+ ids: string[],
106
+ opts: { verifyDir: string; workDir: string; instanceImagesPath?: string; keepWorkspaces?: boolean },
107
+ ): Promise<CalibrationResult[]> {
108
+ const instances = await loadSweInstances(ids)
109
+ const images = await loadInstanceImages(opts.instanceImagesPath)
110
+ const judge = createSerializedJudge()
111
+ await mkdir(opts.workDir, { recursive: true })
112
+ const results: CalibrationResult[] = []
113
+ for (const id of ids) {
114
+ const inst = instances.get(id)!
115
+ const entry = images[id]
116
+ if (!entry) throw new Error(`bootstrap-meta: ${id} has no image mapping (fixtures/instances.json)`)
117
+ // verifyCmd runs with cwd = the materialized WORKSPACE (and, downstream,
118
+ // worker clones) — the script path must be absolute or bash exits 127.
119
+ const result = await calibrateInstance({
120
+ instanceId: id,
121
+ image: entry.image,
122
+ baseCommit: entry.base_commit,
123
+ goldPatch: inst.patch,
124
+ verifyCmd: `bash ${resolve(opts.verifyDir, `${id}.sh`)}`,
125
+ workDir: opts.workDir,
126
+ judge,
127
+ ...(opts.keepWorkspaces !== undefined ? { keepWorkspaces: opts.keepWorkspaces } : {}),
128
+ })
129
+ results.push(result)
130
+ console.log(
131
+ `CALIBRATION ${id}: baseRc=${result.baseRc} goldApplyRc=${result.goldApplyRc} goldRc=${result.goldRc} ` +
132
+ `verifyCalibrated=${result.verifyCalibrated} goldOfficialResolved=${result.goldOfficialResolved} ` +
133
+ `experimentValid=${result.experimentValid}`,
134
+ )
135
+ }
136
+ return results
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // CLI.
141
+ // ---------------------------------------------------------------------------
142
+
143
+ const isMain = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href
144
+
145
+ if (isMain) {
146
+ const [mode, ...rest] = process.argv.slice(2)
147
+ const defaultIds = defaultRound4Config().instances
148
+ if (mode === '--task-meta') {
149
+ const [outPath, ...ids] = rest
150
+ if (!outPath) {
151
+ console.error('usage: bootstrap-meta.mts --task-meta <out.json> [iid...]')
152
+ process.exit(2)
153
+ }
154
+ await writeTaskMeta(ids.length > 0 ? ids : defaultIds, outPath)
155
+ console.log(`task-meta → ${outPath}`)
156
+ } else if (mode === '--problems') {
157
+ const [outDir, ...ids] = rest
158
+ if (!outDir) {
159
+ console.error('usage: bootstrap-meta.mts --problems <outDir> [iid...]')
160
+ process.exit(2)
161
+ }
162
+ for (const p of await writeProblemStatements(ids.length > 0 ? ids : defaultIds, outDir)) {
163
+ console.log(`problem statement → ${p}`)
164
+ }
165
+ } else if (mode === '--calibrate') {
166
+ const [verifyDir, workDir, ...ids] = rest
167
+ if (!verifyDir || !workDir) {
168
+ console.error('usage: bootstrap-meta.mts --calibrate <verifyDir> <workDir> [iid...]')
169
+ process.exit(2)
170
+ }
171
+ const results = await calibrateIds(ids.length > 0 ? ids : defaultIds, { verifyDir, workDir })
172
+ const summaryPath = join(workDir, 'calibration.json')
173
+ await writeFile(summaryPath, JSON.stringify(results, null, 2) + '\n')
174
+ console.log(`calibration summary → ${summaryPath}`)
175
+ const invalid = results.filter((r) => !r.experimentValid)
176
+ if (invalid.length > 0) {
177
+ console.error(`NOT experiment-valid: ${invalid.map((r) => r.iid).join(', ')}`)
178
+ process.exit(1)
179
+ }
180
+ } else {
181
+ console.error(
182
+ 'usage: tsx src/swe-arena/bootstrap-meta.mts --task-meta <out.json> [iid...]\n' +
183
+ ' tsx src/swe-arena/bootstrap-meta.mts --problems <outDir> [iid...]\n' +
184
+ ' tsx src/swe-arena/bootstrap-meta.mts --calibrate <verifyDir> <workDir> [iid...]',
185
+ )
186
+ process.exit(2)
187
+ }
188
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Unit tests for the metadata bootstrap's validation layer — pure, no dataset
3
+ * load. The adapter-backed loaders are exercised by the calibration CLI.
4
+ */
5
+
6
+ import { describe, expect, it } from 'vitest'
7
+ import { assertSweInstance } from './bootstrap-meta.mts'
8
+
9
+ const full = {
10
+ instance_id: 'django__django-11532',
11
+ repo: 'django/django',
12
+ base_commit: 'a5308514fb4bc5086c9a16a8a24a945eeebb073c',
13
+ problem_statement: 'Email messages crash on non-ASCII domain…',
14
+ patch: 'diff --git a/django/core/mail/utils.py …',
15
+ test_patch: 'diff --git a/tests/mail/tests.py …',
16
+ FAIL_TO_PASS: '["test_unicode_dns (mail.tests.MailTests)"]',
17
+ PASS_TO_PASS: '["test_ascii (mail.tests.MailTests)"]',
18
+ version: '3.0',
19
+ environment_setup_commit: '419a78300f7cd27611196e1e464d50fd0385ff27',
20
+ }
21
+
22
+ describe('assertSweInstance', () => {
23
+ it('accepts a complete metadata record and returns the typed instance', () => {
24
+ const inst = assertSweInstance('django__django-11532', full)
25
+ expect(inst.instance_id).toBe('django__django-11532')
26
+ expect(inst.patch).toContain('diff --git')
27
+ expect(inst.version).toBe('3.0')
28
+ })
29
+
30
+ it('normalizes absent optional fields to null', () => {
31
+ const inst = assertSweInstance('django__django-11532', {
32
+ ...full,
33
+ version: undefined,
34
+ environment_setup_commit: '',
35
+ })
36
+ expect(inst.version).toBeNull()
37
+ expect(inst.environment_setup_commit).toBeNull()
38
+ })
39
+
40
+ it('throws on a missing/empty load-bearing field (never a silent blank)', () => {
41
+ expect(() => assertSweInstance('django__django-11532', { ...full, problem_statement: '' })).toThrow(
42
+ /problem_statement/,
43
+ )
44
+ expect(() => assertSweInstance('django__django-11532', { ...full, patch: undefined })).toThrow(/patch/)
45
+ expect(() => assertSweInstance('django__django-11532', undefined)).toThrow(/no metadata/)
46
+ })
47
+
48
+ it('throws on an instance_id mismatch', () => {
49
+ expect(() => assertSweInstance('astropy__astropy-13033', full)).toThrow(/instance_id/)
50
+ })
51
+ })
@@ -0,0 +1,116 @@
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 { materializeWorkspace } from './materialize'
23
+ import { run, shq } from './proc'
24
+ import type { SerializedJudge } from './serialized-judge'
25
+
26
+ export interface CalibrateOptions {
27
+ instanceId: string
28
+ image: string
29
+ baseCommit: string
30
+ /** The official gold patch text (task-meta `patch`). */
31
+ goldPatch: string
32
+ /**
33
+ * Self-repro verify command, run via `bash -c` with cwd = the workspace
34
+ * (calibrate.sh ran `bash verify/<iid>.sh` from inside the tree).
35
+ */
36
+ verifyCmd: string
37
+ /** Scratch root; two throwaway workspaces are created and removed under it. */
38
+ workDir: string
39
+ /** Judge for the official gold gate. */
40
+ judge: SerializedJudge
41
+ /** Ceiling for one verify run (repro scripts self-limit at 180s; this is a backstop). */
42
+ verifyTimeoutMs?: number
43
+ /** Keep the calibration workspaces for post-mortem. Default: removed. */
44
+ keepWorkspaces?: boolean
45
+ }
46
+
47
+ export interface CalibrationResult {
48
+ iid: string
49
+ /** rc of verify on pristine base (must be nonzero). */
50
+ baseRc: number
51
+ /** rc of applying the gold patch (0 via git apply or the fuzz fallback). */
52
+ goldApplyRc: number
53
+ /** rc of verify with gold applied (must be zero). */
54
+ goldRc: number
55
+ /** base FAILS and gold PASSES. */
56
+ verifyCalibrated: boolean
57
+ /** Official judge resolves the gold patch. */
58
+ goldOfficialResolved: boolean
59
+ /** verifyCalibrated && goldOfficialResolved — the experiment admission bar. */
60
+ experimentValid: boolean
61
+ }
62
+
63
+ async function runVerify(verifyCmd: string, ws: string, timeoutMs: number): Promise<number> {
64
+ const res = await run('bash', ['-c', verifyCmd], { cwd: ws, timeoutMs })
65
+ return res.code
66
+ }
67
+
68
+ /**
69
+ * Apply a patch file with calibrate.sh's exact fallback chain:
70
+ * `git apply --whitespace=nowarn`, then `patch -p1 --fuzz=3` on failure.
71
+ * Returns the rc of the LAST attempt (0 = applied).
72
+ */
73
+ export async function applyPatchWithFallback(ws: string, patchFile: string): Promise<number> {
74
+ const gitApply = await run('git', ['apply', '--whitespace=nowarn', patchFile], { cwd: ws })
75
+ if (gitApply.code === 0) return 0
76
+ const fuzz = await run('bash', ['-c', `patch -p1 --fuzz=3 < ${shq(patchFile)}`], { cwd: ws })
77
+ return fuzz.code
78
+ }
79
+
80
+ export async function calibrateInstance(opts: CalibrateOptions): Promise<CalibrationResult> {
81
+ const { instanceId, image, baseCommit, verifyCmd, judge } = opts
82
+ const verifyTimeoutMs = opts.verifyTimeoutMs ?? 600_000
83
+ const baseWs = join(opts.workDir, `cal-base-${instanceId}`)
84
+ const goldWs = join(opts.workDir, `cal-gold-${instanceId}`)
85
+ const goldPatchFile = join(opts.workDir, `${instanceId}.gold.patch`)
86
+
87
+ try {
88
+ await materializeWorkspace({ instanceId, image, baseCommit, dest: baseWs })
89
+ const baseRc = await runVerify(verifyCmd, baseWs, verifyTimeoutMs)
90
+
91
+ await materializeWorkspace({ instanceId, image, baseCommit, dest: goldWs })
92
+ await writeFile(goldPatchFile, opts.goldPatch)
93
+ const goldApplyRc = await applyPatchWithFallback(goldWs, goldPatchFile)
94
+ const goldRc = await runVerify(verifyCmd, goldWs, verifyTimeoutMs)
95
+
96
+ const verifyCalibrated = baseRc !== 0 && goldRc === 0
97
+
98
+ const goldVerdict = await judge.judge(instanceId, goldPatchFile, 'gold')
99
+ const goldOfficialResolved = goldVerdict.resolved === true
100
+
101
+ return {
102
+ iid: instanceId,
103
+ baseRc,
104
+ goldApplyRc,
105
+ goldRc,
106
+ verifyCalibrated,
107
+ goldOfficialResolved,
108
+ experimentValid: verifyCalibrated && goldOfficialResolved,
109
+ }
110
+ } finally {
111
+ if (!opts.keepWorkspaces) {
112
+ await rm(baseWs, { recursive: true, force: true })
113
+ await rm(goldWs, { recursive: true, force: true })
114
+ }
115
+ }
116
+ }
@@ -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
+ }