@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,641 @@
1
+ /**
2
+ * live-improve-campaign-mbpp — the kill-flow follow-up to live-improve-campaign.mts
3
+ * (HumanEval × Qwen2.5-7B: DEV baseline 90%, saturated, gate correctly HELD). Identical
4
+ * loop, moved to a config WITH headroom: sanitized MBPP, where the same worker measures
5
+ * 76.7% at the baseline recipe k5/r2/t6 against an 85.9% pass@5 bound (~9pts of room).
6
+ * The OPEN question this run answers: does dial-tuning ship a win when there is room
7
+ * to win? `improve()` (surface 'rollout-policy') tunes { k, repairRounds, testgen }
8
+ * and the library's own held-out gate makes the ship/hold call — a HOLD is a valid
9
+ * result; the gate is never loosened.
10
+ *
11
+ * Wiring is live-improve-campaign.mts verbatim except the dataset seams:
12
+ * - Tasks come from mbpp-structural.mts's loadMbpp (sanitized MBPP, MBPP_JSON env);
13
+ * prompt = its basePrompt (description + the shown official assert).
14
+ * - VISIBLE checks: the shown assert (test_list[0]) rides task.meta.visibleChecks so
15
+ * the strategy's default officialChecksFromMeta() source ranks it as the OFFICIAL
16
+ * check, lexicographically above the model-authored guesses (mbpp-structural's
17
+ * measured lesson: unweighted guesses flip selection negative).
18
+ * - HIDDEN grading: test_list[1:] (+ test_imports), script-side nonce-sentinel judge
19
+ * in docker --network=none, AFTER the strategy locks its artifact.
20
+ * - test_imports are prepended to every candidate before visible-check execution
21
+ * (a CheckRunner prelude wrapper) and inside the hidden program, mirroring the rig.
22
+ *
23
+ * Honesty split (identical to the prior run):
24
+ * - DEV = sanitized-MBPP usable-task index [0, DEV_N) and HELD-OUT = [DEV_N,
25
+ * DEV_N+HOLD_N) — fixed, disjoint slices, passed as explicit
26
+ * `budget.holdoutScenarios` so the library's own split machinery enforces
27
+ * disjointness (it throws on overlap).
28
+ * - Deterministic proposer; `analyzeGeneration: null` keeps the findings channel
29
+ * empty — the improver's context is ONLY the DEV composites the loop accumulates.
30
+ * - The gate decision is `result.gateDecision` from the library — never recomputed
31
+ * here. The recompute-from-raw block at the end cross-checks the held-out pass
32
+ * counts against the durable per-cell files; a mismatch voids any ship claim.
33
+ *
34
+ * Guards (all from the prior run): SMOKE=1 first (6 dev + 6 held-out tasks, 1
35
+ * generation, population 2 — proves the full path completes before the burn);
36
+ * fail-loud model preflight; MAX_CALLS hard cap (default 8000 — the run aborts loud,
37
+ * durable provenance survives in RUN_DIR); dollars ceiling.
38
+ *
39
+ * Run (key via dotenvx; never in the shell history):
40
+ * cd ~/company/devops/secrets && dotenvx run -f agent-state.env -- bash -c ' \
41
+ * cd /home/drew/code/agent-runtime-swe && \
42
+ * MBPP_JSON=/abs/sanitized-mbpp.json npx tsx bench/src/live-improve-campaign-mbpp.mts'
43
+ */
44
+
45
+ import { execFile, execFileSync } from 'node:child_process'
46
+ import { randomBytes } from 'node:crypto'
47
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs'
48
+ import { tmpdir } from 'node:os'
49
+ import { join } from 'node:path'
50
+ import type {
51
+ DispatchContext,
52
+ JudgeConfig,
53
+ MutableSurface,
54
+ Scenario,
55
+ } from '@tangle-network/agent-eval/contract'
56
+ import type { AgentProfile } from '@tangle-network/agent-interface'
57
+ import { improve } from '../../src/improvement/improve'
58
+ import {
59
+ parseRolloutPolicy,
60
+ ROLLOUT_POLICY_EXTENSION,
61
+ serializeRolloutPolicy,
62
+ structuralRolloutPolicyFromProfile,
63
+ } from '../../src/improvement/rollout-policy'
64
+ import {
65
+ type AgenticRunResult,
66
+ type CheckExecChannel,
67
+ type CheckOutcome,
68
+ type CheckRunner,
69
+ createVerifierEnvironment,
70
+ runAgentic,
71
+ sandboxCheckRunner,
72
+ structuralRollout,
73
+ type StructuralRolloutResult,
74
+ } from '../../src/runtime/index'
75
+ import { basePrompt, loadMbpp, type MbppTask } from './mbpp-structural.mts'
76
+
77
+ function must(name: string): string {
78
+ const v = process.env[name]
79
+ if (!v) throw new Error(`env ${name} is required`)
80
+ return v
81
+ }
82
+
83
+ const SMOKE = process.env.SMOKE === '1'
84
+ const DEV_N = Number(process.env.DEV_N ?? (SMOKE ? 6 : 150))
85
+ const HOLD_N = Number(process.env.HOLD_N ?? (SMOKE ? 6 : 150))
86
+ const GENERATIONS = Number(process.env.GENERATIONS ?? (SMOKE ? 1 : 2))
87
+ const POPULATION = Number(process.env.POPULATION ?? (SMOKE ? 2 : 4))
88
+ const CONCURRENCY = Number(process.env.CONCURRENCY ?? 8)
89
+ const DOCKER_CONCURRENCY = Number(process.env.DOCKER_CONCURRENCY ?? 6)
90
+ // The worker of the measured MBPP basis: 76.7% repaired@1 at k5/r2/t6, 85.9% pass@5
91
+ // bound — the headroom config the saturated HumanEval run lacked.
92
+ const MODEL = process.env.MODEL ?? 'Qwen/Qwen2.5-7B-Instruct-Turbo'
93
+ const BASE = process.env.ROUTER_BASE ?? 'https://api.together.xyz/v1'
94
+ const TEMP = Number(process.env.TEMPERATURE ?? 0.8)
95
+ const MAX_TOKENS = Number(process.env.MAX_TOKENS ?? 2500)
96
+ const DOLLARS = Number(process.env.DOLLARS ?? 15)
97
+ /** Hard LLM-call cap across every phase — the runaway guard. Hitting it aborts the
98
+ * process loud (durable provenance survives in RUN_DIR); it never silently degrades. */
99
+ const MAX_CALLS = Number(process.env.MAX_CALLS ?? 8000)
100
+ const RUN_DIR = process.env.RUN_DIR ?? join(tmpdir(), `live-improve-campaign-mbpp-${Date.now()}`)
101
+
102
+ const systemPrompt = 'You are an expert Python programmer.'
103
+ const dockerImage = 'python:3.12-slim'
104
+ const dockerTimeoutMs = Number(process.env.DOCKER_TIMEOUT_MS ?? 20000)
105
+
106
+ // ── Docker: ONE semaphored --network=none exec channel for BOTH judges ───────────────
107
+
108
+ let dockerInFlight = 0
109
+ const dockerWaiters: Array<() => void> = []
110
+ async function withDockerSlot<T>(fn: () => Promise<T>): Promise<T> {
111
+ if (dockerInFlight >= DOCKER_CONCURRENCY) await new Promise<void>((r) => dockerWaiters.push(r))
112
+ dockerInFlight += 1
113
+ try {
114
+ return await fn()
115
+ } finally {
116
+ dockerInFlight -= 1
117
+ dockerWaiters.shift()?.()
118
+ }
119
+ }
120
+
121
+ const containerPrefix = `licm-${process.pid}`
122
+ let containerSeq = 0
123
+
124
+ function reapContainers(): void {
125
+ try {
126
+ const ids = execFileSync('docker', ['ps', '-aq', '--filter', `name=${containerPrefix}`], {
127
+ timeout: 10000,
128
+ })
129
+ .toString()
130
+ .trim()
131
+ if (ids) execFileSync('docker', ['rm', '-f', ...ids.split('\n')], { timeout: 15000 })
132
+ } catch {
133
+ /* reaper is best-effort by design */
134
+ }
135
+ }
136
+ process.on('SIGINT', () => {
137
+ reapContainers()
138
+ process.exit(130)
139
+ })
140
+ process.on('SIGTERM', () => {
141
+ reapContainers()
142
+ process.exit(143)
143
+ })
144
+
145
+ const dockerBox: CheckExecChannel = {
146
+ exec(command, options) {
147
+ const timeoutMs = options?.timeoutMs ?? dockerTimeoutMs
148
+ return withDockerSlot(
149
+ () =>
150
+ new Promise((resolve, reject) => {
151
+ const name = `${containerPrefix}-${containerSeq++}`
152
+ let settled = false
153
+ const reap = () => execFile('docker', ['rm', '-f', name], () => {})
154
+ const finish = (r: { exitCode: number; stdout: string; stderr: string }) => {
155
+ if (settled) return
156
+ settled = true
157
+ clearTimeout(backstop)
158
+ reap()
159
+ resolve(r)
160
+ }
161
+ const fail = (e: Error) => {
162
+ if (settled) return
163
+ settled = true
164
+ clearTimeout(backstop)
165
+ reap()
166
+ reject(e)
167
+ }
168
+ // execFile's timeout kills the docker CLIENT; a hung container could leave
169
+ // the callback unfired. The backstop guarantees resolution and the named
170
+ // reap kills the stray container.
171
+ const backstop = setTimeout(
172
+ () => finish({ exitCode: 124, stdout: '', stderr: 'timed out (backstop)' }),
173
+ timeoutMs + 3000,
174
+ )
175
+ execFile(
176
+ 'docker',
177
+ ['run', '--rm', '--name', name, '--network=none', '--cpus=1', '--memory=512m', dockerImage, 'sh', '-c', command],
178
+ { timeout: timeoutMs, killSignal: 'SIGKILL', maxBuffer: 4 * 1024 * 1024 },
179
+ (err, stdout, stderr) => {
180
+ if (err) {
181
+ const e = err as NodeJS.ErrnoException & { code?: number | string }
182
+ if (e.code === 'ENOENT') {
183
+ fail(new Error('docker binary not found on PATH'))
184
+ return
185
+ }
186
+ if (/cannot connect to the docker daemon|is the docker daemon running|permission denied while trying to connect/i.test(stderr ?? '')) {
187
+ fail(new Error(`docker daemon unreachable: ${(stderr ?? '').slice(0, 200)}`))
188
+ return
189
+ }
190
+ finish({ exitCode: typeof e.code === 'number' ? e.code : 1, stdout: stdout ?? '', stderr: stderr ?? '' })
191
+ return
192
+ }
193
+ finish({ exitCode: 0, stdout: stdout ?? '', stderr: stderr ?? '' })
194
+ },
195
+ )
196
+ }),
197
+ )
198
+ },
199
+ }
200
+
201
+ // ── The hidden nonce judge (script-side, AFTER the strategy locks its artifact) ──────
202
+ // MBPP hidden suite = test_list[1:] with test_imports; pass requires exit 0 AND the
203
+ // per-call nonce sentinel in stdout — a candidate printing a forged verdict cannot pass.
204
+
205
+ function buildHiddenProgram(task: MbppTask, candidate: string, nonce: string): string {
206
+ return `${task.testImports.join('\n')}\n${candidate}\n\n${task.hiddenAsserts.join('\n')}\nprint("HIDDEN-${nonce} PASS")\n`
207
+ }
208
+
209
+ async function runHiddenJudge(
210
+ task: MbppTask,
211
+ candidate: string,
212
+ ): Promise<{ pass: number; detail?: string }> {
213
+ const nonce = randomBytes(8).toString('hex')
214
+ const b64 = Buffer.from(buildHiddenProgram(task, candidate, nonce), 'utf8').toString('base64')
215
+ const r = await dockerBox.exec(`printf '%s' '${b64}' | base64 -d | python3 -`, {
216
+ timeoutMs: dockerTimeoutMs,
217
+ })
218
+ if (r.exitCode === 0 && r.stdout.includes(`HIDDEN-${nonce} PASS`)) return { pass: 1 }
219
+ return { pass: 0, detail: (r.stderr || r.stdout).slice(-300) || 'timed out (no output)' }
220
+ }
221
+
222
+ // ── Scenarios: fixed disjoint slices of sanitized MBPP ────────────────────────────────
223
+
224
+ interface MbppScenario extends Scenario {
225
+ kind: 'mbpp'
226
+ }
227
+
228
+ const taskById = new Map<string, MbppTask>()
229
+ const scenarioId = (t: MbppTask) => `mbpp/${t.taskId}`
230
+
231
+ // ── The real evaluator: one cell = one structuralRollout run + hidden grade ──────────
232
+
233
+ interface CellArtifact {
234
+ taskId: string
235
+ policy: string
236
+ /** Hidden nonce-judge grade of the FINAL selected candidate: {0,1}. */
237
+ pass: number
238
+ detail?: string
239
+ repairStop: string
240
+ shots: number
241
+ completions: number
242
+ authoredChecks: number
243
+ officialChecks: number
244
+ tokens: { input: number; output: number }
245
+ usd: number
246
+ ms: number
247
+ }
248
+
249
+ interface ScoredCandidate {
250
+ candidate: string
251
+ outcome: CheckOutcome
252
+ }
253
+ function recordingRunner(inner: CheckRunner, log: ScoredCandidate[]): CheckRunner {
254
+ return {
255
+ async run(candidate, checks, ctx) {
256
+ const outcome = await inner.run(candidate, checks, ctx)
257
+ log.push({ candidate, outcome })
258
+ return outcome
259
+ },
260
+ }
261
+ }
262
+
263
+ /** MBPP's test_imports must be in scope when the visible checks run (the rig prepends
264
+ * them to every judged program). The recorded candidate stays RAW — the hidden judge
265
+ * prepends the same imports itself. */
266
+ function testImportsPrelude(inner: CheckRunner): CheckRunner {
267
+ return {
268
+ run(candidate, checks, ctx) {
269
+ const raw = ctx.task.meta?.testImports
270
+ const imports = Array.isArray(raw) ? raw.filter((i): i is string => typeof i === 'string') : []
271
+ return inner.run(imports.length > 0 ? `${imports.join('\n')}\n${candidate}` : candidate, checks, ctx)
272
+ },
273
+ }
274
+ }
275
+
276
+ // Global spend meter (every cell of every phase — baseline, generations, holdout).
277
+ const spend = { cells: 0, llmCalls: 0, tokensIn: 0, tokensOut: 0, usd: 0, hiddenPass: 0 }
278
+
279
+ async function evaluateCell(
280
+ surface: MutableSurface,
281
+ scenario: MbppScenario,
282
+ ctx: DispatchContext,
283
+ ): Promise<CellArtifact> {
284
+ if (spend.llmCalls >= MAX_CALLS) {
285
+ console.error(
286
+ `\nBUDGET CAP HIT: ${spend.llmCalls} llm calls >= MAX_CALLS=${MAX_CALLS} — aborting loud; durable provenance in ${RUN_DIR}`,
287
+ )
288
+ reapContainers()
289
+ process.exit(1)
290
+ }
291
+ const policy = parseRolloutPolicy(surface)
292
+ if (!policy) {
293
+ throw new Error(`agent: surface carries no valid rollout policy: ${String(surface).slice(0, 120)}`)
294
+ }
295
+ const task = taskById.get(scenario.id)
296
+ if (!task) throw new Error(`agent: unknown scenario id ${scenario.id}`)
297
+
298
+ const scored: ScoredCandidate[] = []
299
+ const strategy = structuralRollout({
300
+ policy: { ...policy, temperature: TEMP },
301
+ checkRunner: recordingRunner(testImportsPrelude(sandboxCheckRunner({ box: dockerBox })), scored),
302
+ })
303
+ // INERT check: the strategy's harness-verified score channel carries no hidden
304
+ // signal — hidden grading happens below, after the rollout locks its artifact.
305
+ const inertSurface = createVerifierEnvironment({
306
+ name: 'mbpp-inert',
307
+ check: () => ({ passes: 0, total: 1, errored: 0 }),
308
+ })
309
+ const result = (await runAgentic({
310
+ surface: inertSurface,
311
+ task: {
312
+ id: scenario.id,
313
+ systemPrompt,
314
+ userPrompt: basePrompt(task),
315
+ meta: {
316
+ entryPoint: task.entryPoint,
317
+ // The OFFICIAL check: the shown assert (test_list[0], printed in the prompt)
318
+ // feeds the strategy's default officialChecksFromMeta() source, so it ranks
319
+ // above the model-authored guesses in selection and repair.
320
+ visibleChecks: [task.shownAssert],
321
+ testImports: task.testImports,
322
+ },
323
+ },
324
+ routerBaseUrl: BASE,
325
+ routerKey: must('TOGETHER_API_KEY'),
326
+ model: MODEL,
327
+ temperature: TEMP,
328
+ maxTokens: MAX_TOKENS,
329
+ innerTurns: 2,
330
+ strategy,
331
+ // The strategy's documented sizing: k samples + repair rounds + the check-author consult.
332
+ budget: policy.k + policy.repairRounds + 1,
333
+ })) as AgenticRunResult & StructuralRolloutResult
334
+
335
+ // Backend integrity: report REAL usage on every cell (expectUsage 'assert' upstream).
336
+ ctx.cost.observe(result.usd, 'together')
337
+ ctx.cost.observeTokens(result.tokens)
338
+
339
+ const winner = result.selection.find((r) => r.selected)
340
+ if (!winner) {
341
+ // Zero candidates means EVERY shot for this cell returned null — the signature of a
342
+ // dead worker (credits exhausted / rate-limited / outage), not a hard task. The
343
+ // runtime swallows exhausted-retry shots as null, so without this the run degrades
344
+ // silently for hundreds of cells and then dies at the holdout with a cryptic empty-
345
+ // gate error. Probe the API once and abort LOUD with the real HTTP status so the
346
+ // operator sees the actual cause (e.g. HTTP 402 credit exceeded) immediately.
347
+ if (result.repairStop === 'no-candidates') {
348
+ await abortOnDeadWorker(scenario.id)
349
+ }
350
+ throw new Error(`${scenario.id}: no receipt marked selected (repairStop=${result.repairStop})`)
351
+ }
352
+ const rec = scored[winner.candidateIndex]
353
+ if (!rec) {
354
+ throw new Error(
355
+ `${scenario.id}: selected receipt #${winner.candidateIndex} has no recorded candidate (${scored.length} scored)`,
356
+ )
357
+ }
358
+
359
+ const hidden = await runHiddenJudge(task, rec.candidate)
360
+
361
+ spend.cells += 1
362
+ spend.llmCalls += result.completions
363
+ spend.tokensIn += result.tokens.input
364
+ spend.tokensOut += result.tokens.output
365
+ spend.usd += result.usd
366
+ spend.hiddenPass += hidden.pass
367
+
368
+ const artifact: CellArtifact = {
369
+ taskId: scenario.id,
370
+ policy: serializeRolloutPolicy(policy),
371
+ pass: hidden.pass,
372
+ ...(hidden.detail ? { detail: hidden.detail } : {}),
373
+ repairStop: result.repairStop,
374
+ shots: result.shots,
375
+ completions: result.completions,
376
+ authoredChecks: result.authoredChecks,
377
+ officialChecks: result.officialChecks,
378
+ tokens: result.tokens,
379
+ usd: result.usd,
380
+ ms: result.ms,
381
+ }
382
+ appendFileSync(
383
+ join(RUN_DIR, 'cells.jsonl'),
384
+ `${JSON.stringify({ cellId: ctx.cellId, generation: ctx.generation ?? null, ...artifact, detail: undefined })}\n`,
385
+ )
386
+ console.log(
387
+ ` [cell ${String(spend.cells).padStart(4)}] ${scenario.id.padEnd(10)} ${artifact.policy.padEnd(38)} hidden=${hidden.pass ? 'PASS' : 'fail'} ${result.repairStop} calls=${result.completions}`,
388
+ )
389
+ return artifact
390
+ }
391
+
392
+ // The in-loop judge is a deterministic transcriber of the script-side hidden grade —
393
+ // the grading itself never runs inside the strategy or the proposer's view.
394
+ const hiddenJudge: JudgeConfig<CellArtifact, MbppScenario> = {
395
+ name: 'hidden-nonce-judge',
396
+ dimensions: [
397
+ { key: 'hidden', description: 'MBPP hidden suite test_list[1:] (docker --network=none, nonce sentinel)' },
398
+ ],
399
+ score: ({ artifact }) => ({
400
+ dimensions: { hidden: artifact.pass },
401
+ composite: artifact.pass,
402
+ notes: artifact.pass ? 'hidden PASS' : `hidden fail: ${(artifact.detail ?? '').slice(0, 160)}`,
403
+ }),
404
+ }
405
+
406
+ // ── Fail-loud model preflight (cost gate: prove the worker is live before any burn) ──
407
+
408
+ /** One direct 8-token probe of the worker. Returns HTTP status + a short body slice —
409
+ * the shared health check for the startup preflight and the mid-run dead-worker guard. */
410
+ async function probeWorker(): Promise<{ ok: boolean; status: number; body: string; content: string }> {
411
+ const res = await fetch(`${BASE}/chat/completions`, {
412
+ method: 'POST',
413
+ headers: { Authorization: `Bearer ${must('TOGETHER_API_KEY')}`, 'Content-Type': 'application/json' },
414
+ body: JSON.stringify({
415
+ model: MODEL,
416
+ max_tokens: 16,
417
+ temperature: 0,
418
+ messages: [{ role: 'user', content: 'Reply with the single word: ready' }],
419
+ }),
420
+ })
421
+ if (!res.ok) return { ok: false, status: res.status, body: (await res.text()).slice(0, 300), content: '' }
422
+ const d = (await res.json()) as { choices?: Array<{ message?: { content?: string } }> }
423
+ return { ok: true, status: res.status, body: '', content: (d.choices?.[0]?.message?.content ?? '').trim() }
424
+ }
425
+
426
+ async function preflightModel(): Promise<void> {
427
+ const p = await probeWorker()
428
+ if (!p.ok) throw new Error(`model preflight FAILED: ${MODEL} @ ${BASE} → HTTP ${p.status}: ${p.body}`)
429
+ if (p.content === '') throw new Error(`model preflight FAILED: ${MODEL} returned empty content`)
430
+ console.log(` preflight: ${MODEL} is live (replied ${JSON.stringify(p.content.slice(0, 40))})`)
431
+ }
432
+
433
+ /** Called when a cell produced zero candidates (every shot null). Probes the worker; if
434
+ * it is unhealthy (e.g. HTTP 402 credit exceeded, 429 rate limit, outage) the whole run
435
+ * is doomed — abort LOUD now rather than degrade through hundreds more null cells into
436
+ * a cryptic empty-holdout gate error. If the probe is HEALTHY the null was a one-off, so
437
+ * return and let the per-cell throw handle just this cell. */
438
+ async function abortOnDeadWorker(scenarioId: string): Promise<void> {
439
+ const p = await probeWorker()
440
+ if (p.ok && p.content !== '') return
441
+ console.error(
442
+ `\nDEAD WORKER: cell ${scenarioId} produced zero candidates and a direct probe returned ` +
443
+ `${p.ok ? `empty content` : `HTTP ${p.status}: ${p.body}`}. Every shot is failing — aborting ` +
444
+ `loud (durable provenance in ${RUN_DIR}). If this is HTTP 402, add Together credits and re-run.`,
445
+ )
446
+ reapContainers()
447
+ process.exit(1)
448
+ }
449
+
450
+ // ── Reporting helpers (read the library's own result objects; never re-decide) ───────
451
+
452
+ interface CampaignLike {
453
+ cells: Array<{ error?: string | null; judgeScores: Record<string, { composite: number }> }>
454
+ }
455
+ function passStats(campaign: CampaignLike): { passed: number; scored: number; errored: number; rate: number } {
456
+ let passed = 0
457
+ let scored = 0
458
+ let errored = 0
459
+ for (const cell of campaign.cells) {
460
+ if (cell.error) {
461
+ errored += 1
462
+ continue
463
+ }
464
+ scored += 1
465
+ const scores = Object.values(cell.judgeScores)
466
+ const composite = scores.length === 0 ? 0 : scores.reduce((s, j) => s + j.composite, 0) / scores.length
467
+ if (composite >= 0.999) passed += 1
468
+ }
469
+ return { passed, scored, errored, rate: scored > 0 ? passed / scored : 0 }
470
+ }
471
+ const pct = (x: number) => `${(100 * x).toFixed(1)}%`
472
+
473
+ /** Recompute pass counts from the DURABLE per-cell files the loop's fs storage wrote
474
+ * (`<runDir>/<phase>/<scenario>_<rep>/cached-result.json`, artifact.pass = the hidden
475
+ * nonce-judge grade). Independent of the in-memory campaign objects — the ship-claim
476
+ * cross-check. */
477
+ function recomputeFromRaw(dir: string): { passed: number; scored: number; errored: number } | null {
478
+ if (!existsSync(dir)) return null
479
+ let passed = 0
480
+ let scored = 0
481
+ let errored = 0
482
+ for (const entry of readdirSync(dir)) {
483
+ const file = join(dir, entry, 'cached-result.json')
484
+ if (!existsSync(file)) continue
485
+ const cell = JSON.parse(readFileSync(file, 'utf8')) as {
486
+ error?: string | null
487
+ artifact?: { pass?: number }
488
+ }
489
+ if (cell.error) {
490
+ errored += 1
491
+ continue
492
+ }
493
+ scored += 1
494
+ if ((cell.artifact?.pass ?? 0) >= 1) passed += 1
495
+ }
496
+ return { passed, scored, errored }
497
+ }
498
+
499
+ async function main(): Promise<void> {
500
+ must('TOGETHER_API_KEY')
501
+ mkdirSync(RUN_DIR, { recursive: true })
502
+ const started = Date.now()
503
+
504
+ const { tasks: all, droppedShort, droppedEntry } = loadMbpp(DEV_N + HOLD_N, 0)
505
+ if (all.length !== DEV_N + HOLD_N) {
506
+ throw new Error(`expected ${DEV_N + HOLD_N} usable MBPP tasks, loaded ${all.length}`)
507
+ }
508
+ const devTasks = all.slice(0, DEV_N)
509
+ const holdTasks = all.slice(DEV_N)
510
+ for (const t of all) taskById.set(scenarioId(t), t)
511
+
512
+ const toScenario = (t: MbppTask): MbppScenario => ({ id: scenarioId(t), kind: 'mbpp' })
513
+ const scenarios = all.map(toScenario)
514
+ const holdoutScenarios = holdTasks.map(toScenario)
515
+
516
+ const baselinePolicy = { k: 5, repairRounds: 2, testgen: 6 }
517
+ const profile: AgentProfile = {
518
+ name: 'mbpp-structural-worker',
519
+ extensions: { [ROLLOUT_POLICY_EXTENSION]: baselinePolicy },
520
+ }
521
+
522
+ console.log('=== LIVE self-improvement campaign · MBPP (headroom config) · improve() surface rollout-policy ===')
523
+ console.log(` worker: ${MODEL} @ ${BASE} (temp=${TEMP}, maxTokens=${MAX_TOKENS}, innerTurns=2)`)
524
+ console.log(
525
+ ` dataset: sanitized MBPP (${process.env.MBPP_JSON}); dropped at load: ${droppedShort.length} (<2 asserts), ${droppedEntry.length} (entry unresolved)`,
526
+ )
527
+ console.log(
528
+ ` DEV slice : usable-task index [0, ${DEV_N}) — mbpp/${devTasks[0]?.taskId} .. mbpp/${devTasks[devTasks.length - 1]?.taskId} (n=${devTasks.length})`,
529
+ )
530
+ console.log(
531
+ ` HELD-OUT slice : usable-task index [${DEV_N}, ${DEV_N + HOLD_N}) — mbpp/${holdTasks[0]?.taskId} .. mbpp/${holdTasks[holdTasks.length - 1]?.taskId} (n=${holdTasks.length})`,
532
+ )
533
+ console.log(` baseline policy: ${JSON.stringify(baselinePolicy)} (measured basis: 76.7% repaired@1, 85.9% pass@5 bound)`)
534
+ console.log(
535
+ ` budget: generations=${GENERATIONS} population<=${POPULATION} reps=1 concurrency=${CONCURRENCY} docker<=${DOCKER_CONCURRENCY} ceiling=$${DOLLARS} maxCalls=${MAX_CALLS}`,
536
+ )
537
+ console.log(` gate: library defaultProductionGate (paired bootstrap on held-out, ship iff CI.low > 0.05) — UNCHANGED`)
538
+ console.log(` runDir: ${RUN_DIR}`)
539
+ await preflightModel()
540
+ console.log(`\n profile BEFORE: ${JSON.stringify(profile)}\n`)
541
+
542
+ const result = await improve<MbppScenario, CellArtifact>(profile, [], {
543
+ surface: 'rollout-policy',
544
+ scenarios,
545
+ judge: hiddenJudge,
546
+ agent: evaluateCell,
547
+ budget: {
548
+ generations: GENERATIONS,
549
+ populationSize: POPULATION,
550
+ maxConcurrency: CONCURRENCY,
551
+ holdoutScenarios,
552
+ reps: 1,
553
+ dollars: DOLLARS,
554
+ },
555
+ runDir: RUN_DIR,
556
+ // Deterministic proposer, empty findings channel: the improver's context is ONLY
557
+ // the DEV composites the loop accumulates — no distilled failure text, no trace
558
+ // paths, and (by the loop's own structure) never a held-out cell.
559
+ analyzeGeneration: null,
560
+ })
561
+
562
+ const loop = result.raw.raw
563
+ const wallMin = (Date.now() - started) / 60000
564
+
565
+ console.log('\n── DEV (train) results — what the improver saw ──')
566
+ const baseDev = passStats(loop.baselineCampaign)
567
+ console.log(
568
+ ` gen -1 baseline ${serializeRolloutPolicy(structuralRolloutPolicyFromProfile(profile)!).padEnd(38)} DEV ${baseDev.passed}/${baseDev.scored} = ${pct(baseDev.rate)} (errored ${baseDev.errored})`,
569
+ )
570
+ for (const gen of loop.generations) {
571
+ const surfaceByHash = new Map(gen.surfaces.map((s) => [s.surfaceHash, s]))
572
+ const promotedHashes = new Set(gen.record.promoted)
573
+ for (const cand of gen.record.candidates) {
574
+ const s = surfaceByHash.get(cand.surfaceHash)
575
+ const stats = s ? passStats(s.campaign) : undefined
576
+ console.log(
577
+ ` gen ${String(gen.record.generationIndex).padStart(2)} ${String(cand.label ?? '').padEnd(16)} ${String(s?.surface ?? '?').padEnd(38)} DEV ${stats ? `${stats.passed}/${stats.scored} = ${pct(stats.rate)} (errored ${stats.errored})` : '?'}${promotedHashes.has(cand.surfaceHash) ? ' [promoted]' : ''}`,
578
+ )
579
+ }
580
+ }
581
+ console.log(` training winner: ${String(loop.winnerSurface)}${loop.winnerLabel ? ` (${loop.winnerLabel})` : ''}`)
582
+
583
+ console.log('\n── HELD-OUT gate — the library decides ──')
584
+ const baseHold = passStats(loop.baselineOnHoldout)
585
+ const winHold = passStats(loop.winnerOnHoldout)
586
+ console.log(
587
+ ` baseline on held-out : ${baseHold.passed}/${baseHold.scored} = ${pct(baseHold.rate)} (errored ${baseHold.errored})`,
588
+ )
589
+ console.log(
590
+ ` winner on held-out : ${winHold.passed}/${winHold.scored} = ${pct(winHold.rate)} (errored ${winHold.errored})`,
591
+ )
592
+ console.log(` gate decision: ${result.gateDecision.toUpperCase()} (lift ${result.lift >= 0 ? '+' : ''}${result.lift.toFixed(3)})`)
593
+ for (const reason of loop.gateResult.reasons) console.log(` reason: ${reason}`)
594
+ for (const g of loop.gateResult.contributingGates) {
595
+ console.log(` gate[${g.name}] passed=${g.passed} detail=${JSON.stringify(g.detail).slice(0, 300)}`)
596
+ }
597
+
598
+ console.log('\n── recompute-from-raw check (durable cell files, artifact.pass) ──')
599
+ const rawBase = recomputeFromRaw(join(RUN_DIR, 'holdout-baseline'))
600
+ const rawWin = recomputeFromRaw(join(RUN_DIR, 'holdout-winner'))
601
+ if (rawBase && rawWin && rawBase.scored > 0 && rawWin.scored > 0) {
602
+ const rawLift = rawWin.passed / rawWin.scored - rawBase.passed / rawBase.scored
603
+ console.log(` holdout-baseline raw: ${rawBase.passed}/${rawBase.scored} = ${pct(rawBase.passed / rawBase.scored)} (errored ${rawBase.errored})`)
604
+ console.log(` holdout-winner raw: ${rawWin.passed}/${rawWin.scored} = ${pct(rawWin.passed / rawWin.scored)} (errored ${rawWin.errored})`)
605
+ console.log(` raw pass-rate lift: ${rawLift >= 0 ? '+' : ''}${rawLift.toFixed(3)} (loop-reported composite lift ${result.lift >= 0 ? '+' : ''}${result.lift.toFixed(3)})`)
606
+ const agree = rawBase.passed === baseHold.passed && rawWin.passed === winHold.passed
607
+ console.log(` agreement with library objects: ${agree ? 'EXACT' : 'MISMATCH — investigate before any ship claim'}`)
608
+ } else if (rawBase && rawBase.scored > 0 && !rawWin) {
609
+ // The library skips the winner holdout campaign when winner == baseline (empty
610
+ // diff) — nothing shipped, so there is nothing separate to recompute.
611
+ console.log(
612
+ ` holdout-baseline raw: ${rawBase.passed}/${rawBase.scored} = ${pct(rawBase.passed / rawBase.scored)} (errored ${rawBase.errored})`,
613
+ )
614
+ console.log(' holdout-winner absent: winner == baseline, no separate winner campaign ran (ship impossible this run)')
615
+ } else {
616
+ console.log(' raw holdout cell files missing — cannot recompute (storage did not persist cells?)')
617
+ }
618
+
619
+ console.log('\n── ship/hold outcome ──')
620
+ console.log(` shipped: ${result.shipped}`)
621
+ console.log(` profile AFTER : ${JSON.stringify(result.profile)}`)
622
+ if (result.shipped) {
623
+ console.log(` policy change : ${serializeRolloutPolicy(structuralRolloutPolicyFromProfile(profile)!)} → ${serializeRolloutPolicy(structuralRolloutPolicyFromProfile(result.profile)!)}`)
624
+ } else {
625
+ console.log(' policy change : none (gate held — baseline policy stays)')
626
+ }
627
+
628
+ console.log('\n── spend / provenance ──')
629
+ console.log(
630
+ ` cells ${spend.cells} · llm calls ${spend.llmCalls} · tokens ${spend.tokensIn} in / ${spend.tokensOut} out · router-priced $${spend.usd.toFixed(4)} · loop-reported $${result.raw.totalCostUsd.toFixed(4)}`,
631
+ )
632
+ console.log(` wall ${wallMin.toFixed(1)} min · runDir ${RUN_DIR} (cells.jsonl + campaign cells + loop provenance)`)
633
+ reapContainers()
634
+ process.exit(0)
635
+ }
636
+
637
+ main().catch((e) => {
638
+ console.error(e)
639
+ reapContainers()
640
+ process.exit(1)
641
+ })