@tangle-network/agent-bench 0.3.7 → 0.4.0

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 (211) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/HARNESS.md +43 -0
  3. package/README.md +7 -0
  4. package/dist/adapters.js +23 -23
  5. package/dist/benchmarks/_harness.d.ts +1 -1
  6. package/dist/benchmarks/_harness.js +1 -1
  7. package/dist/benchmarks/aec-bench.js +2 -2
  8. package/dist/benchmarks/agentbench.js +2 -2
  9. package/dist/benchmarks/appworld.js +2 -2
  10. package/dist/benchmarks/bfcl.js +2 -2
  11. package/dist/benchmarks/commit0.js +2 -2
  12. package/dist/benchmarks/crag.js +2 -2
  13. package/dist/benchmarks/dabstep.js +2 -2
  14. package/dist/benchmarks/enterpriseops-gym.js +2 -2
  15. package/dist/benchmarks/finresearchbench.js +2 -2
  16. package/dist/benchmarks/nomiracl.js +2 -2
  17. package/dist/benchmarks/open-rag-bench.js +2 -2
  18. package/dist/benchmarks/programbench.js +2 -2
  19. package/dist/benchmarks/ragbench.js +2 -2
  20. package/dist/benchmarks/swe-bench.js +2 -2
  21. package/dist/benchmarks/t2-ragbench.js +2 -2
  22. package/dist/benchmarks/tau-bench-shared.js +2 -2
  23. package/dist/benchmarks/tau2-bench.js +3 -3
  24. package/dist/benchmarks/tau3-banking.js +3 -3
  25. package/dist/benchmarks/terminal-bench.js +2 -2
  26. package/dist/benchmarks/toollm.js +2 -2
  27. package/dist/benchmarks/webarena-verified.js +2 -2
  28. package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
  29. package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
  30. package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
  31. package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
  32. package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
  33. package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
  34. package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
  35. package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
  36. package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
  37. package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
  38. package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
  39. package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
  40. package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
  41. package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
  42. package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
  43. package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
  44. package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
  45. package/dist/{chunk-PWQVGAJB.js → chunk-V7AEBY6U.js} +21 -21
  46. package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
  47. package/dist/chunk-WSKWVEQB.js.map +1 -0
  48. package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
  49. package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
  50. package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
  51. package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
  52. package/dist/index.js +23 -23
  53. package/package.json +5 -5
  54. package/scripts/trata-hedge/README.md +6 -5
  55. package/scripts/verify-packed-consumer.mjs +1 -1
  56. package/src/benchmarks/_harness.ts +20 -2
  57. package/src/benchmarks/humaneval.test.mts +2 -2
  58. package/src/gate.ts +1 -1
  59. package/src/hev-eval.mts +5 -2
  60. package/src/hev-improve.mts +118 -73
  61. package/src/official-optimizer-config.mts +89 -0
  62. package/src/official-optimizer-config.test.mts +88 -0
  63. package/src/profiles.ts +2 -2
  64. package/src/quant-arena/README.md +144 -0
  65. package/src/quant-arena/backtest.test.mts +135 -0
  66. package/src/quant-arena/backtest.ts +218 -0
  67. package/src/quant-arena/data.test.mts +44 -0
  68. package/src/quant-arena/data.ts +141 -0
  69. package/src/quant-arena/driver.test.mts +253 -0
  70. package/src/quant-arena/driver.ts +219 -0
  71. package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
  72. package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
  73. package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
  74. package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
  75. package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
  76. package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
  77. package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
  78. package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
  79. package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
  80. package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
  81. package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
  82. package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
  83. package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
  84. package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
  85. package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
  86. package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
  87. package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
  88. package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
  89. package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
  90. package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
  91. package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
  92. package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
  93. package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
  94. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
  95. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
  96. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
  97. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
  98. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
  99. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
  100. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
  101. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
  102. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
  103. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
  104. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
  105. package/src/quant-arena/holdout-certify.mts +206 -0
  106. package/src/quant-arena/holdout-certify.test.mts +82 -0
  107. package/src/quant-arena/leak-audit.test.mts +79 -0
  108. package/src/quant-arena/leak-audit.ts +95 -0
  109. package/src/quant-arena/make-fixtures.mts +161 -0
  110. package/src/quant-arena/multiplicity.test.mts +68 -0
  111. package/src/quant-arena/multiplicity.ts +87 -0
  112. package/src/quant-arena/nautilus-certify.ts +31 -0
  113. package/src/quant-arena/oms.ts +90 -0
  114. package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
  115. package/src/quant-arena/python/pyproject.toml +8 -0
  116. package/src/quant-arena/python/uv.lock +1297 -0
  117. package/src/quant-arena/python/vbt-worker.py +192 -0
  118. package/src/quant-arena/quant-loop.mts +813 -0
  119. package/src/quant-arena/quant-loop.test.mts +75 -0
  120. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
  121. package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
  122. package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
  123. package/src/quant-arena/types.ts +133 -0
  124. package/src/quant-arena/vbt-client.ts +321 -0
  125. package/src/quant-arena/vbt-parity.test.mts +183 -0
  126. package/src/quant-arena/windows.test.mts +45 -0
  127. package/src/quant-arena/windows.ts +54 -0
  128. package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
  129. package/src/rollout-ledger/backfill-swe-arena.test.mts +342 -0
  130. package/src/rollout-ledger/settle-capture.mts +442 -0
  131. package/src/rollout-ledger/settle-capture.test.mts +270 -0
  132. package/src/smoke-structural-rollout.mts +15 -9
  133. package/src/swe-arena/activation.mts +225 -0
  134. package/src/swe-arena/activation.test.mts +300 -0
  135. package/src/swe-arena/arms.ts +30 -14
  136. package/src/swe-arena/briefing.mts +217 -0
  137. package/src/swe-arena/briefing.test.mts +178 -0
  138. package/src/swe-arena/calibrate.ts +102 -1
  139. package/src/swe-arena/cell-evidence.mts +41 -9
  140. package/src/swe-arena/factory-command-container.ts +284 -0
  141. package/src/swe-arena/factory-judge-child.mts +228 -0
  142. package/src/swe-arena/factory.test.mts +643 -0
  143. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
  144. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
  145. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
  146. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
  147. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
  148. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
  149. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
  150. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
  151. package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
  152. package/src/swe-arena/fixtures/run-report/README.md +43 -0
  153. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
  154. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
  155. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
  156. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
  157. package/src/swe-arena/fixtures.ts +135 -2
  158. package/src/swe-arena/gepa-seat.mts +877 -0
  159. package/src/swe-arena/gepa-seat.test.mts +1059 -0
  160. package/src/swe-arena/implementation-ref.test.mts +64 -0
  161. package/src/swe-arena/implementation-ref.ts +62 -0
  162. package/src/swe-arena/ledger-orphans.mts +77 -0
  163. package/src/swe-arena/ledger-orphans.test.mts +147 -0
  164. package/src/swe-arena/outer-loop.mts +710 -33
  165. package/src/swe-arena/premeasured-from-cells.mts +281 -0
  166. package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
  167. package/src/swe-arena/proposer-fanout.mts +327 -31
  168. package/src/swe-arena/proposer-fanout.test.mts +246 -0
  169. package/src/swe-arena/proposer-provenance.mts +172 -0
  170. package/src/swe-arena/proposer-provenance.test.mts +106 -0
  171. package/src/swe-arena/run-experiment.mts +367 -1
  172. package/src/swe-arena/run-report.mts +75 -0
  173. package/src/swe-arena/run-supervisor.test.mts +5 -3
  174. package/src/swe-arena/score-split.mts +140 -0
  175. package/src/swe-arena/score-split.test.mts +123 -0
  176. package/src/swe-arena/scratch-worktree.test.mts +55 -0
  177. package/src/swe-arena/scratch-worktree.ts +34 -0
  178. package/src/swe-arena/types.ts +52 -0
  179. package/src/swe-code-improve.mts +24 -25
  180. package/src/swe-improve.mts +129 -96
  181. package/src/swe-local-proof.mts +6 -1
  182. package/src/swe-stream.mts +4 -2
  183. package/src/swe-structural.mts +245 -837
  184. package/src/tb-container-executor.test.mts +30 -6
  185. package/src/tb-supervisor-sidecar.mts +2 -1
  186. package/src/trata-gepa.mts +182 -245
  187. package/dist/chunk-LRRD7NAG.js.map +0 -1
  188. package/src/live-improve-campaign-mbpp.mts +0 -641
  189. package/src/live-improve-campaign.mts +0 -500
  190. /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
  191. /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
  192. /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
  193. /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
  194. /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
  195. /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
  196. /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
  197. /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
  198. /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
  199. /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
  200. /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
  201. /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
  202. /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
  203. /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
  204. /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
  205. /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
  206. /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
  207. /package/dist/{chunk-PWQVGAJB.js.map → chunk-V7AEBY6U.js.map} +0 -0
  208. /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
  209. /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
  210. /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
  211. /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Gen-5 MAP+TOOLBOX briefing: evidence-index generation (one line per
3
+ * evidence path, private instances excluded, leak guard fail-loud), the
4
+ * toolbox/permission prompt section, and the change-space briefing override.
5
+ */
6
+
7
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
8
+ import { existsSync } from 'node:fs'
9
+ import { tmpdir } from 'node:os'
10
+ import { join } from 'node:path'
11
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
12
+ import {
13
+ AUTHOR_BRIEFING_RELPATH,
14
+ AUTHOR_BRIEFING_VERSION,
15
+ briefingPromptSection,
16
+ collectEvidenceIndexRows,
17
+ defaultAuthorBriefing,
18
+ EVIDENCE_INDEX_FILENAME,
19
+ resolveAuthorBriefing,
20
+ writeEvidenceIndex,
21
+ } from './briefing.mts'
22
+ import { splitInstances } from './score-split.mts'
23
+ import { runOk } from './proc.ts'
24
+
25
+ const SIX = [
26
+ 'astropy__astropy-13033',
27
+ 'django__django-11532',
28
+ 'matplotlib__matplotlib-20826',
29
+ 'pydata__xarray-4687',
30
+ 'pytest-dev__pytest-6197',
31
+ 'sphinx-doc__sphinx-9658',
32
+ ]
33
+
34
+ describe('evidence index', () => {
35
+ let root: string
36
+ let outDir: string
37
+ let roundsDir: string
38
+ let priorDir: string
39
+ const split = splitInstances('r4-seed', SIX, 4)
40
+ const publicIid = split.publicInstances[0]!
41
+ const privateIid = split.privateInstances[0]!
42
+
43
+ beforeEach(async () => {
44
+ root = await mkdtemp(join(tmpdir(), 'briefing-'))
45
+ outDir = join(root, 'out')
46
+ roundsDir = join(root, 'rounds')
47
+ priorDir = join(root, 'gen4')
48
+ await mkdir(outDir, { recursive: true })
49
+ await mkdir(roundsDir, { recursive: true })
50
+ await writeFile(join(roundsDir, 'gen-0.jsonl'), '{}\n')
51
+ await writeFile(join(roundsDir, 'round4-summary-r4-x.json'), '{}\n')
52
+ // Seed artifact dirs: one public, one private instance.
53
+ for (const iid of [publicIid, privateIid]) {
54
+ await mkdir(join(root, 'runs', iid, 'SUP4'), { recursive: true })
55
+ await writeFile(join(root, 'patches', `${iid}.sup4.patch`), 'p\n').catch(async () => {
56
+ await mkdir(join(root, 'patches'), { recursive: true })
57
+ await writeFile(join(root, 'patches', `${iid}.sup4.patch`), 'p\n')
58
+ })
59
+ }
60
+ // Prior run outDir with arm-run judge evidence for a public + a private instance.
61
+ for (const iid of [publicIid, privateIid]) {
62
+ const runDir = join(priorDir, 'arm-runs', 'cand111111', 'rep-0', 'runs', iid)
63
+ await mkdir(runDir, { recursive: true })
64
+ await writeFile(join(runDir, 'judge.json'), '{}\n')
65
+ }
66
+ await mkdir(join(priorDir, 'candidates'), { recursive: true })
67
+ await mkdir(join(priorDir, 'proposer-shots'), { recursive: true })
68
+ })
69
+
70
+ afterEach(async () => {
71
+ await rm(root, { recursive: true, force: true })
72
+ })
73
+
74
+ const args = () => ({
75
+ outDir,
76
+ roundsDir,
77
+ seedArtifactRuns: [publicIid, privateIid].map((iid) => ({
78
+ iid,
79
+ arm: 'SUP4',
80
+ dir: join(root, 'runs', iid, 'SUP4'),
81
+ patchPath: join(root, 'patches', `${iid}.sup4.patch`),
82
+ })),
83
+ priorEvidenceDirs: [priorDir],
84
+ paretoParentPatches: [] as Array<{ label: string; path: string }>,
85
+ split,
86
+ })
87
+
88
+ it('lists one line per evidence path: staircase, summaries, seed runs, prior arm runs, shot receipts', async () => {
89
+ const { path, rows } = await writeEvidenceIndex(args())
90
+ expect(path).toBe(join(outDir, EVIDENCE_INDEX_FILENAME))
91
+ expect(existsSync(path)).toBe(true)
92
+ const text = await readFile(path, 'utf8')
93
+ expect(text).toContain(join(roundsDir, 'gen-0.jsonl'))
94
+ expect(text).toContain(join(roundsDir, 'round4-summary-r4-x.json'))
95
+ expect(text).toContain(join(root, 'runs', publicIid, 'SUP4'))
96
+ expect(text).toContain(join(root, 'patches', `${publicIid}.sup4.patch`))
97
+ expect(text).toContain(join(priorDir, 'candidates'))
98
+ expect(text).toContain(join(priorDir, 'proposer-shots'))
99
+ expect(text).toContain(join(priorDir, 'arm-runs', 'cand111111', 'rep-0', 'runs', publicIid))
100
+ // One markdown bullet per row.
101
+ expect(text.split('\n').filter((l) => l.startsWith('- ')).length).toBe(rows.length)
102
+ })
103
+
104
+ it('NEVER surfaces private instances: no private row, no private id anywhere in the rendered text', async () => {
105
+ const { path, rows } = await writeEvidenceIndex(args())
106
+ const text = await readFile(path, 'utf8')
107
+ for (const iid of split.privateInstances) expect(text).not.toContain(iid)
108
+ expect(rows.every((r) => r.iid === null || !split.privateInstances.includes(r.iid))).toBe(true)
109
+ // The withholding itself is disclosed (count only, no identities).
110
+ expect(text).toContain('2 improvement instance(s) are PRIVATE')
111
+ })
112
+
113
+ it('fails loud when a run-level path would leak a private id into the rendered index', async () => {
114
+ const leakPatch = join(outDir, `${privateIid}.parent.patch`)
115
+ await writeFile(leakPatch, 'x\n')
116
+ await expect(
117
+ writeEvidenceIndex({ ...args(), paretoParentPatches: [{ label: 'leaky', path: leakPatch }] }),
118
+ ).rejects.toThrow(/leaked/)
119
+ })
120
+
121
+ it('collectEvidenceIndexRows includes everything when no split is configured', async () => {
122
+ const rows = await collectEvidenceIndexRows({ ...args(), split: null })
123
+ const iids = rows.map((r) => r.iid).filter((i): i is string => i !== null)
124
+ expect(iids).toContain(privateIid)
125
+ })
126
+ })
127
+
128
+ describe('briefing text', () => {
129
+ it('default briefing names the toolbox (traces CLI, trace analysts, AxLLM) and grants subagent permission with a budget', () => {
130
+ const text = defaultAuthorBriefing()
131
+ expect(text).toContain('npx --yes @tangle-network/traces@latest')
132
+ expect(text).toContain('agent-eval trace analysts')
133
+ expect(text).toContain('AxLLM')
134
+ expect(text).toContain('spawn your own subagents')
135
+ expect(text).toContain('Research budget')
136
+ expect(text).toContain('ONE input')
137
+ expect(text).toContain(AUTHOR_BRIEFING_RELPATH)
138
+ expect(text).toContain(AUTHOR_BRIEFING_VERSION)
139
+ })
140
+
141
+ it('briefingPromptSection leads with the evidence map path', () => {
142
+ const section = briefingPromptSection({
143
+ indexPath: '/tmp/x/evidence-index.md',
144
+ briefingText: defaultAuthorBriefing(),
145
+ briefingSource: 'default',
146
+ })
147
+ expect(section).toContain('EVIDENCE MAP: /tmp/x/evidence-index.md')
148
+ expect(section.indexOf('EVIDENCE MAP')).toBeLessThan(section.indexOf('TOOLBOX'))
149
+ })
150
+
151
+ it('resolveAuthorBriefing prefers the change-space file at the ref (authors may rewrite their own briefing)', async () => {
152
+ const repo = await mkdtemp(join(tmpdir(), 'briefing-repo-'))
153
+ try {
154
+ await runOk('git', ['init', '-q', '-b', 'main', repo])
155
+ await runOk('git', ['-C', repo, 'config', 'user.email', 't@t.dev'])
156
+ await runOk('git', ['-C', repo, 'config', 'user.name', 'T'])
157
+ await writeFile(join(repo, 'base.txt'), 'x\n')
158
+ await runOk('git', ['-C', repo, 'add', '-A'])
159
+ await runOk('git', ['-C', repo, 'commit', '-q', '-m', 'init'])
160
+
161
+ // No override committed → the versioned default.
162
+ const fallback = await resolveAuthorBriefing(repo, 'main')
163
+ expect(fallback.source).toBe('default')
164
+ expect(fallback.text).toBe(defaultAuthorBriefing())
165
+
166
+ // Commit the change-space override → it wins.
167
+ await mkdir(join(repo, 'extensions', 'pi'), { recursive: true })
168
+ await writeFile(join(repo, AUTHOR_BRIEFING_RELPATH), 'MY OWN RESEARCH RULES\n')
169
+ await runOk('git', ['-C', repo, 'add', '-A'])
170
+ await runOk('git', ['-C', repo, 'commit', '-q', '-m', 'briefing override'])
171
+ const override = await resolveAuthorBriefing(repo, 'main')
172
+ expect(override.source).toBe('change-space')
173
+ expect(override.text).toBe('MY OWN RESEARCH RULES\n')
174
+ } finally {
175
+ await rm(repo, { recursive: true, force: true })
176
+ }
177
+ })
178
+ })
@@ -19,8 +19,11 @@
19
19
 
20
20
  import { rm, writeFile } from 'node:fs/promises'
21
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'
22
25
  import { materializeWorkspace } from './materialize'
23
- import { run, shq } from './proc'
26
+ import { run, runOk, shq } from './proc'
24
27
  import type { SerializedJudge } from './serialized-judge'
25
28
 
26
29
  export interface CalibrateOptions {
@@ -114,3 +117,101 @@ export async function calibrateInstance(opts: CalibrateOptions): Promise<Calibra
114
117
  }
115
118
  }
116
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
+ }
@@ -248,9 +248,17 @@ export type StaircaseVerdict =
248
248
  * evaluation — the candidate never became a measured surface. Emitted by
249
249
  * the outer loop's kill-row writer, never by `decideVerdict`. */
250
250
  | 'rejected-prefilter'
251
+ /** GEN-5 activation gate: the candidate's own machine-checkable predicate
252
+ * (activation.mts) proved its mechanism NEVER fired in its campaign
253
+ * traces — recorded, never promoted, EVEN when the score improved. */
254
+ | 'quarantined-inactive'
251
255
 
252
256
  /** protocol_v2 keep-if-better: improvement-set resolved-count must RISE and
253
- * cost must stay within the guard. Fail-closed on unprovable cost. */
257
+ * cost must stay within the guard. Fail-closed on unprovable cost.
258
+ * GEN-5: `activationFired === false` quarantines the candidate ahead of the
259
+ * score comparison — an improved score with an inactive mechanism is
260
+ * indistinguishable from luck (undefined/null = gate not applicable:
261
+ * disabled, baseline, or predicate unevaluated). */
254
262
  export function decideVerdict(input: {
255
263
  violations: string[]
256
264
  coverageComplete: boolean
@@ -258,8 +266,10 @@ export function decideVerdict(input: {
258
266
  parentResolvedCount: number
259
267
  costRatio: number | null
260
268
  costGuardRatio: number
269
+ activationFired?: boolean | null
261
270
  }): StaircaseVerdict {
262
271
  if (input.violations.length > 0) return 'rejected-out-of-space'
272
+ if (input.activationFired === false) return 'quarantined-inactive'
263
273
  if (!input.coverageComplete) return 'rejected-incomplete'
264
274
  if (input.resolvedCount <= input.parentResolvedCount) return 'rejected-no-gain'
265
275
  if (input.costRatio === null || input.costRatio > input.costGuardRatio) return 'rejected-cost'
@@ -273,27 +283,46 @@ export function decideVerdict(input: {
273
283
  // coverage-incomplete downstream, fail-closed).
274
284
  // ---------------------------------------------------------------------------
275
285
 
276
- /** Parse every `<cellDir>/cached-result.json` under one campaign dir. Missing dir = []. */
277
- export async function loadCampaignCells(campaignDir: string): Promise<EvidenceCell[]> {
286
+ /** Every `<cellDir>/cached-result.json` under one campaign dir, verbatim and
287
+ * identity-checked. Missing dir = []. A cache that exists but carries no
288
+ * scenarioId/rep cannot be attributed to a cell, so it throws rather than
289
+ * disappearing from the campaign's coverage. */
290
+ export async function loadCampaignCellRecords(campaignDir: string): Promise<Record<string, unknown>[]> {
278
291
  const entries = await readdir(campaignDir, { withFileTypes: true }).catch(() => [])
279
- const cells: EvidenceCell[] = []
292
+ const records: Record<string, unknown>[] = []
280
293
  for (const entry of entries) {
281
294
  if (!entry.isDirectory()) continue
282
295
  const path = join(campaignDir, entry.name, 'cached-result.json')
283
296
  const raw = await readFile(path, 'utf8').catch(() => null)
284
297
  if (raw === null) continue
285
- let parsed: Record<string, unknown>
298
+ let parsed: unknown
286
299
  try {
287
- parsed = JSON.parse(raw) as Record<string, unknown>
300
+ parsed = JSON.parse(raw)
288
301
  } catch {
289
302
  throw new Error(`loadCampaignCells: corrupt cell cache ${path}`)
290
303
  }
291
- if (typeof parsed.scenarioId !== 'string' || typeof parsed.rep !== 'number') {
304
+ // null and arrays parse fine and would make the identity check below throw a
305
+ // TypeError instead of naming what is wrong with the file.
306
+ const record =
307
+ parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
308
+ ? (parsed as Record<string, unknown>)
309
+ : null
310
+ if (record === null || typeof record.scenarioId !== 'string' || typeof record.rep !== 'number') {
292
311
  throw new Error(`loadCampaignCells: ${path} is not a campaign cell (scenarioId/rep missing)`)
293
312
  }
313
+ records.push(record)
314
+ }
315
+ return records
316
+ }
317
+
318
+ /** Parse every `<cellDir>/cached-result.json` under one campaign dir. Missing dir = []. */
319
+ export async function loadCampaignCells(campaignDir: string): Promise<EvidenceCell[]> {
320
+ const cells: EvidenceCell[] = []
321
+ for (const parsed of await loadCampaignCellRecords(campaignDir)) {
294
322
  cells.push({
295
- scenarioId: parsed.scenarioId,
296
- rep: parsed.rep,
323
+ // loadCampaignCellRecords has already proven both are present and typed.
324
+ scenarioId: parsed.scenarioId as string,
325
+ rep: parsed.rep as number,
297
326
  artifact: (parsed.artifact ?? null) as R4Artifact | null,
298
327
  ...(typeof parsed.error === 'string' ? { error: parsed.error } : {}),
299
328
  ...(typeof parsed.costUsd === 'number' ? { costUsd: parsed.costUsd } : {}),
@@ -377,6 +406,8 @@ export function gateEvidenceFromCells(input: {
377
406
  iids: string[]
378
407
  reps: number
379
408
  costGuardRatio: number
409
+ /** GEN-5 activation-gate outcome for the winner (see decideVerdict). */
410
+ activationFired?: boolean | null
380
411
  }): GateEvidence {
381
412
  const winnerRuns = replicateRunsFromCells(input.winnerCells)
382
413
  const baselineRuns = replicateRunsFromCells(input.baselineCells)
@@ -400,6 +431,7 @@ export function gateEvidenceFromCells(input: {
400
431
  parentResolvedCount: baseResolved,
401
432
  costRatio,
402
433
  costGuardRatio: input.costGuardRatio,
434
+ ...(input.activationFired !== undefined ? { activationFired: input.activationFired } : {}),
403
435
  }),
404
436
  }
405
437
  }
@@ -0,0 +1,284 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { mkdir, mkdtemp, realpath, rm, stat } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { delimiter, isAbsolute, join } from 'node:path'
5
+
6
+ import { run, type RunResult } from './proc'
7
+
8
+ const WORKSPACE_PATH = '/workspace'
9
+ const STATE_PATH = '/factory-state'
10
+ const IMAGE_DIGEST_RE = /^.+@sha256:[0-9a-f]{64}$/
11
+ const CONTAINER_PATH =
12
+ `${STATE_PATH}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`
13
+ const CONTROL_TIMEOUT_MS = 30_000
14
+
15
+ export type FactoryCommandNetwork = 'enabled' | 'none'
16
+
17
+ export interface RunFactoryCommandOptions {
18
+ image: string
19
+ network: FactoryCommandNetwork
20
+ timeoutMs: number
21
+ signal?: AbortSignal
22
+ maxBuffer?: number
23
+ }
24
+
25
+ function hostPath(baseEnv: NodeJS.ProcessEnv): string {
26
+ const path = (baseEnv.PATH ?? '')
27
+ .split(delimiter)
28
+ .filter((entry) => entry.length > 0 && isAbsolute(entry))
29
+ .join(delimiter)
30
+ if (!path) throw new Error('factory container control process requires an absolute PATH')
31
+ return path
32
+ }
33
+
34
+ async function createControlEnvironment(): Promise<{
35
+ env: NodeJS.ProcessEnv
36
+ dispose: () => Promise<void>
37
+ }> {
38
+ const root = await mkdtemp(join(tmpdir(), 'factory-container-control-'))
39
+ const home = join(root, 'home')
40
+ const dockerConfig = join(root, 'docker')
41
+ const xdgConfig = join(root, 'xdg')
42
+ const temp = join(root, 'tmp')
43
+ try {
44
+ await Promise.all(
45
+ [home, dockerConfig, xdgConfig, temp].map((dir) =>
46
+ mkdir(dir, { recursive: true, mode: 0o700 }),
47
+ ),
48
+ )
49
+ let disposal: Promise<void> | undefined
50
+ return {
51
+ env: {
52
+ PATH: hostPath(process.env),
53
+ HOME: home,
54
+ DOCKER_CONFIG: dockerConfig,
55
+ XDG_CONFIG_HOME: xdgConfig,
56
+ TMPDIR: temp,
57
+ TMP: temp,
58
+ TEMP: temp,
59
+ LANG: 'C',
60
+ LC_ALL: 'C',
61
+ TZ: 'UTC',
62
+ },
63
+ dispose: () => (disposal ??= rm(root, { recursive: true, force: true })),
64
+ }
65
+ } catch (error) {
66
+ await rm(root, { recursive: true, force: true })
67
+ throw error
68
+ }
69
+ }
70
+
71
+ function currentUser(): { uid: number; gid: number } {
72
+ const uid = process.getuid?.()
73
+ const gid = process.getgid?.()
74
+ if (
75
+ typeof uid !== 'number' ||
76
+ typeof gid !== 'number' ||
77
+ !Number.isSafeInteger(uid) ||
78
+ !Number.isSafeInteger(gid)
79
+ ) {
80
+ throw new Error('factory command containers require a POSIX uid and gid')
81
+ }
82
+ if (uid === 0) {
83
+ throw new Error('factory command containers refuse to run candidate commands as root')
84
+ }
85
+ return { uid, gid }
86
+ }
87
+
88
+ function containerEnvironment(): string[] {
89
+ return [
90
+ `PATH=${CONTAINER_PATH}`,
91
+ `HOME=${STATE_PATH}/home`,
92
+ `XDG_CONFIG_HOME=${STATE_PATH}/xdg/config`,
93
+ `XDG_DATA_HOME=${STATE_PATH}/xdg/data`,
94
+ `XDG_CACHE_HOME=${STATE_PATH}/xdg/cache`,
95
+ `XDG_STATE_HOME=${STATE_PATH}/xdg/state`,
96
+ `XDG_RUNTIME_DIR=${STATE_PATH}/xdg/runtime`,
97
+ 'TMPDIR=/tmp',
98
+ 'TMP=/tmp',
99
+ 'TEMP=/tmp',
100
+ 'LANG=C',
101
+ 'LC_ALL=C',
102
+ 'TZ=UTC',
103
+ 'CI=false',
104
+ `COREPACK_HOME=${STATE_PATH}/corepack`,
105
+ 'COREPACK_DEFAULT_TO_LATEST=0',
106
+ 'COREPACK_ENABLE_DOWNLOAD_PROMPT=0',
107
+ `PNPM_HOME=${STATE_PATH}/pnpm-home`,
108
+ `npm_config_store_dir=${STATE_PATH}/pnpm-store`,
109
+ `NPM_CONFIG_CACHE=${STATE_PATH}/npm-cache`,
110
+ `NPM_CONFIG_USERCONFIG=${STATE_PATH}/npm/user.npmrc`,
111
+ `NPM_CONFIG_GLOBALCONFIG=${STATE_PATH}/npm/global.npmrc`,
112
+ 'GIT_CONFIG_NOSYSTEM=1',
113
+ `GIT_CONFIG_GLOBAL=${STATE_PATH}/git/global.config`,
114
+ `GIT_CONFIG_SYSTEM=${STATE_PATH}/git/system.config`,
115
+ 'GIT_TERMINAL_PROMPT=0',
116
+ 'GCM_INTERACTIVE=Never',
117
+ ]
118
+ }
119
+
120
+ const COMMAND_WRAPPER = `
121
+ set -eu
122
+ mkdir -p \
123
+ "$HOME" \
124
+ "$XDG_CONFIG_HOME" \
125
+ "$XDG_DATA_HOME" \
126
+ "$XDG_CACHE_HOME" \
127
+ "$XDG_STATE_HOME" \
128
+ "$XDG_RUNTIME_DIR" \
129
+ "$COREPACK_HOME" \
130
+ "$PNPM_HOME" \
131
+ "$npm_config_store_dir" \
132
+ "$NPM_CONFIG_CACHE" \
133
+ "${STATE_PATH}/npm" \
134
+ "${STATE_PATH}/git" \
135
+ "${STATE_PATH}/bin"
136
+ touch \
137
+ "$NPM_CONFIG_USERCONFIG" \
138
+ "$NPM_CONFIG_GLOBALCONFIG" \
139
+ "$GIT_CONFIG_GLOBAL" \
140
+ "$GIT_CONFIG_SYSTEM"
141
+ corepack enable --install-directory "${STATE_PATH}/bin" >/dev/null
142
+ exec /bin/bash --noprofile --norc -c "$1"
143
+ `
144
+
145
+ function dockerArgs(input: {
146
+ name: string
147
+ workspace: string
148
+ command: string
149
+ image: string
150
+ network: FactoryCommandNetwork
151
+ uid: number
152
+ gid: number
153
+ }): string[] {
154
+ const user = `${input.uid}:${input.gid}`
155
+ return [
156
+ 'run',
157
+ '--rm',
158
+ '--name',
159
+ input.name,
160
+ '--hostname',
161
+ 'factory-command',
162
+ '--pull=missing',
163
+ '--read-only',
164
+ '--init',
165
+ '--cap-drop=ALL',
166
+ '--security-opt=no-new-privileges',
167
+ '--pids-limit=512',
168
+ '--memory=4g',
169
+ '--memory-swap=4g',
170
+ '--cpus=2',
171
+ '--ulimit',
172
+ 'nofile=4096:4096',
173
+ '--ipc=private',
174
+ '--network',
175
+ input.network === 'none' ? 'none' : 'bridge',
176
+ '--user',
177
+ user,
178
+ '--workdir',
179
+ WORKSPACE_PATH,
180
+ '--mount',
181
+ `type=bind,src=${input.workspace},dst=${WORKSPACE_PATH},bind-propagation=rprivate`,
182
+ '--tmpfs',
183
+ `/tmp:rw,nosuid,nodev,mode=1777,size=512m,uid=${input.uid},gid=${input.gid}`,
184
+ '--tmpfs',
185
+ `${STATE_PATH}:rw,nosuid,nodev,mode=700,size=2048m,uid=${input.uid},gid=${input.gid}`,
186
+ '--entrypoint=/usr/bin/env',
187
+ input.image,
188
+ '-i',
189
+ ...containerEnvironment(),
190
+ '/bin/bash',
191
+ '--noprofile',
192
+ '--norc',
193
+ '-c',
194
+ COMMAND_WRAPPER,
195
+ 'factory-command',
196
+ input.command,
197
+ ]
198
+ }
199
+
200
+ function isAlreadyRemoved(result: RunResult): boolean {
201
+ return /no such container/iu.test(result.stderr || result.stdout)
202
+ }
203
+
204
+ /**
205
+ * Run one repository-controlled command in a fresh container.
206
+ *
207
+ * The candidate sees one writable bind mount: its disposable workspace.
208
+ * Process ids, root files, HOME, XDG state, Corepack, npm, and pnpm caches are
209
+ * private to this invocation and disappear when the container exits.
210
+ */
211
+ export async function runFactoryCommand(
212
+ workspace: string,
213
+ command: string,
214
+ options: RunFactoryCommandOptions,
215
+ ): Promise<RunResult> {
216
+ if (!command.trim()) throw new Error('factory command must not be empty')
217
+ if (!IMAGE_DIGEST_RE.test(options.image)) {
218
+ throw new Error(`factory command image must be pinned by sha256 digest: ${options.image}`)
219
+ }
220
+ if (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0) {
221
+ throw new Error(`factory command timeout must be a positive integer: ${options.timeoutMs}`)
222
+ }
223
+ const workspacePath = await realpath(workspace)
224
+ if (!(await stat(workspacePath)).isDirectory()) {
225
+ throw new Error(`factory command workspace is not a directory: ${workspacePath}`)
226
+ }
227
+ if (workspacePath.includes(',')) {
228
+ throw new Error(`factory command workspace path contains an unsupported comma: ${workspacePath}`)
229
+ }
230
+ const { uid, gid } = currentUser()
231
+ const name = `factory-command-${process.pid}-${randomUUID()}`
232
+ const control = await createControlEnvironment()
233
+ let commandResult: RunResult | undefined
234
+ let commandError: unknown
235
+ try {
236
+ commandResult = await run(
237
+ 'docker',
238
+ dockerArgs({
239
+ name,
240
+ workspace: workspacePath,
241
+ command,
242
+ image: options.image,
243
+ network: options.network,
244
+ uid,
245
+ gid,
246
+ }),
247
+ {
248
+ env: control.env,
249
+ timeoutMs: options.timeoutMs,
250
+ killGraceMs: 2_000,
251
+ ...(options.signal ? { signal: options.signal } : {}),
252
+ ...(options.maxBuffer === undefined ? {} : { maxBuffer: options.maxBuffer }),
253
+ },
254
+ )
255
+ } catch (error) {
256
+ commandError = error
257
+ }
258
+
259
+ let cleanupError: unknown
260
+ try {
261
+ const cleanup = await run('docker', ['rm', '-f', name], {
262
+ env: control.env,
263
+ timeoutMs: CONTROL_TIMEOUT_MS,
264
+ })
265
+ if (cleanup.code !== 0 && !isAlreadyRemoved(cleanup)) {
266
+ cleanupError = new Error(
267
+ `factory command container cleanup failed (${cleanup.code}): ${(cleanup.stderr || cleanup.stdout).slice(-1000)}`,
268
+ )
269
+ }
270
+ } catch (error) {
271
+ cleanupError = error
272
+ }
273
+ await control.dispose()
274
+
275
+ if (commandError !== undefined && cleanupError !== undefined) {
276
+ throw new AggregateError(
277
+ [commandError, cleanupError],
278
+ 'factory command and container cleanup both failed',
279
+ )
280
+ }
281
+ if (commandError !== undefined) throw commandError
282
+ if (cleanupError !== undefined) throw cleanupError
283
+ return commandResult!
284
+ }