@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,520 @@
1
+ /**
2
+ * Diagnosis ensemble — N BLIND failure analysts over the SAME supervisor-run
3
+ * artifacts (brain.jsonl, driver.log, workers/*.ndjson + *.patch, verify.log,
4
+ * result.json, the delivered patch, and the official-judge outcome), each a
5
+ * different model routed through router.tangle.tools, fused by union +
6
+ * cross-analyst agreement ranking. Minority findings SURVIVE fusion marked as
7
+ * competing hypotheses — the round-4 protocol treats them as candidate
8
+ * proposal seeds, not noise.
9
+ *
10
+ * Design constraints (from supervisor-lab .evolve/state.json round4_design):
11
+ * - the model list is CONFIG — glm-5.2 is the only model proven routed today;
12
+ * gpt-5.5 / opus-4.8 slot in by editing the analyst spec list, never by a
13
+ * hardcoded requirement on an unrouted model.
14
+ * - analysts are blind: same bundle, no cross-talk, independent calls.
15
+ * - secrets discipline matches capacity.ts: the API key is referenced by NAME
16
+ * inside a dotenvx child shell; the response body lands in a file (dotenvx
17
+ * writes its banner to stdout, so stdout is only trusted for the marker).
18
+ */
19
+
20
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
21
+ import { join } from 'node:path'
22
+ import { type AnalystFinding, makeFinding } from '@tangle-network/agent-eval'
23
+ import { findSupervisorRunDir, type SecretsEnv } from './arms'
24
+ import { ROUTER_ENDPOINT, sleepWithSignal } from './capacity'
25
+ import { run } from './proc'
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Analyst specs.
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export interface AnalystSpec {
32
+ /** Stable label, e.g. 'glm-5.2#1'. Used in fusion attribution + filenames. */
33
+ id: string
34
+ model: string
35
+ /** Chat-completions endpoint. Default: the Tangle router. */
36
+ url?: string
37
+ /** NAME of the env var holding the bearer key (resolved in the dotenvx child). */
38
+ apiKeyEnv?: string
39
+ /** glm-5.2 returns empty content when starved below ~8000, and at sampled
40
+ * temperatures its REASONING alone can consume a full 8000 ceiling (both
41
+ * measured — the calibration smoke saw out=8000 with empty content).
42
+ * Default 16_000 so reasoning + the JSON answer always fit. */
43
+ maxTokens?: number
44
+ temperature?: number
45
+ }
46
+
47
+ /** N same-model analysts (the smoke default). Real rounds replace entries with
48
+ * other routed models — diversity comes from the config, never a hardcode.
49
+ * Same-model analysts at temperature 0 are byte-identical duplicates (measured
50
+ * in the calibration smoke: #2 and #3 returned the same 2823 output tokens),
51
+ * which silently inflates agreement — so only the FIRST same-model analyst
52
+ * runs at 0; the rest sample at 0.7 to buy real diversity. */
53
+ export function defaultAnalysts(n = 3, model = 'glm-5.2'): AnalystSpec[] {
54
+ return Array.from({ length: n }, (_, i) => ({
55
+ id: `${model}#${i + 1}`,
56
+ model,
57
+ temperature: i === 0 ? 0 : 0.7,
58
+ }))
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Artifact bundle — the ONE shared context every blind analyst reads.
63
+ // ---------------------------------------------------------------------------
64
+
65
+ /** One supervisor arm run to diagnose. `dir` is the run dir layout the arms
66
+ * write: brain.jsonl / driver.log / result.json / verify.log / ws/. */
67
+ export interface SupRunArtifacts {
68
+ iid: string
69
+ arm: string
70
+ dir: string
71
+ /** The delivered (extracted) arm patch, when it exists. */
72
+ patchPath?: string
73
+ /** Official-judge outcome for this run, when known. `resolved: null` =
74
+ * inconclusive judge — shown to analysts as such, never coerced. */
75
+ judge?: { resolved: boolean | null; score?: number; note?: string }
76
+ }
77
+
78
+ export interface BundleOptions {
79
+ /** Total bundle ceiling (chars). Default 60_000 (~15-20k tokens). */
80
+ maxChars?: number
81
+ /** Max workers whose ndjson/patch are excerpted per run. Default 6. */
82
+ maxWorkers?: number
83
+ }
84
+
85
+ const head = (s: string, n: number): string => (s.length <= n ? s : `${s.slice(0, n)}\n…[truncated head]`)
86
+ const tail = (s: string, n: number): string => (s.length <= n ? s : `…[truncated tail]\n${s.slice(-n)}`)
87
+
88
+ async function safeRead(path: string): Promise<string> {
89
+ return readFile(path, 'utf8').catch(() => '')
90
+ }
91
+
92
+ function section(title: string, body: string): string {
93
+ const trimmed = body.trim()
94
+ if (trimmed.length === 0) return ''
95
+ return `--- ${title} ---\n${trimmed}\n`
96
+ }
97
+
98
+ /** Build the bounded shared bundle. Every section is capped so one megabyte
99
+ * brain log cannot crowd out the patch the analysts must actually read. */
100
+ export async function buildArtifactBundle(
101
+ runs: SupRunArtifacts[],
102
+ opts: BundleOptions = {},
103
+ ): Promise<string> {
104
+ const maxChars = opts.maxChars ?? 60_000
105
+ const maxWorkers = opts.maxWorkers ?? 6
106
+ const parts: string[] = []
107
+ for (const r of runs) {
108
+ const chunks: string[] = [`### RUN ${r.iid} arm=${r.arm}\nrun dir: ${r.dir}`]
109
+ if (r.judge) {
110
+ chunks.push(
111
+ `official judge: resolved=${r.judge.resolved === null ? 'INCONCLUSIVE' : r.judge.resolved}` +
112
+ (r.judge.score !== undefined ? ` score=${r.judge.score}` : '') +
113
+ (r.judge.note ? ` (${r.judge.note})` : ''),
114
+ )
115
+ }
116
+ chunks.push(section('result.json', head(await safeRead(join(r.dir, 'result.json')), 1_500)))
117
+ chunks.push(section('verify.log (tail)', tail(await safeRead(join(r.dir, 'verify.log')), 2_000)))
118
+ const driver = await safeRead(join(r.dir, 'driver.log'))
119
+ chunks.push(section('driver.log (head)', head(driver, 800)))
120
+ chunks.push(section('driver.log (tail)', tail(driver, 4_000)))
121
+ chunks.push(section('brain.jsonl (tail)', tail(await safeRead(join(r.dir, 'brain.jsonl')), 4_000)))
122
+
123
+ const supRunDir = await findSupervisorRunDir(join(r.dir, 'ws'))
124
+ if (supRunDir) {
125
+ chunks.push(section('supervisor state.json', head(await safeRead(join(supRunDir, 'state.json')), 3_500)))
126
+ chunks.push(section('supervisor journal.jsonl (tail)', tail(await safeRead(join(supRunDir, 'journal.jsonl')), 3_000)))
127
+ const workerFiles = (await readdir(join(supRunDir, 'workers')).catch(() => [] as string[])).sort()
128
+ const ndjson = workerFiles.filter((f) => f.endsWith('.ndjson')).slice(0, maxWorkers)
129
+ const patches = workerFiles.filter((f) => f.endsWith('.patch')).slice(0, maxWorkers)
130
+ for (const f of ndjson) {
131
+ chunks.push(section(`workers/${f} (tail)`, tail(await safeRead(join(supRunDir, 'workers', f)), 2_500)))
132
+ }
133
+ for (const f of patches) {
134
+ chunks.push(section(`workers/${f} (head)`, head(await safeRead(join(supRunDir, 'workers', f)), 3_000)))
135
+ }
136
+ }
137
+ if (r.patchPath) {
138
+ chunks.push(section('DELIVERED PATCH (head)', head(await safeRead(r.patchPath), 6_000)))
139
+ }
140
+ parts.push(chunks.filter(Boolean).join('\n'))
141
+ }
142
+ const bundle = parts.join('\n\n')
143
+ if (bundle.length <= maxChars) return bundle
144
+ // Keep the head (earliest runs) and the tail (latest run's patch) — the
145
+ // middle is the least diagnostic. Marked loudly so analysts know.
146
+ const keep = Math.floor(maxChars / 2)
147
+ return `${bundle.slice(0, keep)}\n\n…[BUNDLE TRUNCATED: ${bundle.length - maxChars} chars removed]…\n\n${bundle.slice(-keep)}`
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // One blind analyst call.
152
+ // ---------------------------------------------------------------------------
153
+
154
+ export interface AnalystRawFinding {
155
+ failure_class: string
156
+ evidence_quote: string
157
+ proposed_direction: string
158
+ /** 0..1, clamped. Defaults to 0.5 when the model omits it. */
159
+ confidence: number
160
+ }
161
+
162
+ export interface AnalystReport {
163
+ analystId: string
164
+ model: string
165
+ ok: boolean
166
+ findings: AnalystRawFinding[]
167
+ error?: string
168
+ /** Raw model text, kept for the audit trail. */
169
+ rawText?: string
170
+ tokens?: { input: number; output: number }
171
+ }
172
+
173
+ /** The blind-analyst instruction. Exported so the calibration smoke and tests
174
+ * pin the exact contract the parser expects. */
175
+ export function analystPrompt(bundle: string): string {
176
+ return [
177
+ 'You are one independent, BLIND failure analyst (other analysts see the same artifacts; you cannot see them).',
178
+ 'The artifacts below come from runs of an automated software-engineering SUPERVISOR agent on SWE-bench Verified instances:',
179
+ '- a supervisor "brain" plans, spawns sandboxed workers (each in a clone of the instance repo), and settles a delivered patch;',
180
+ '- each run has a self-authored verify script (worker-visible reproduction check);',
181
+ '- after the run locks, the OFFICIAL hidden maintainer test suite grades the delivered patch (worker-blind);',
182
+ '- "verify_pass=true but official resolved=false" means the self-check passed while the maintainers\' tests did not.',
183
+ '',
184
+ 'Diagnose the DOMINANT reasons these runs failed to resolve. Ground every finding in a verbatim quote from the artifacts.',
185
+ 'Respond with STRICT JSON only (no markdown fences, no commentary):',
186
+ '{"findings":[{"failure_class":"2-6 word category","evidence_quote":"verbatim from artifacts","proposed_direction":"concrete change direction","confidence":0.0}]}',
187
+ 'Return 1 to 5 findings, most important first.',
188
+ '',
189
+ '===== ARTIFACTS =====',
190
+ bundle,
191
+ ].join('\n')
192
+ }
193
+
194
+ /** Extract + validate the strict-JSON findings contract from model text.
195
+ * Tolerates code fences and leading/trailing prose; throws on anything that
196
+ * does not contain one parseable findings object (the caller records the
197
+ * analyst as failed — never a silently-empty diagnosis). */
198
+ export function parseAnalystFindings(text: string): AnalystRawFinding[] {
199
+ const stripped = text.replace(/```(?:json)?/gi, '').trim()
200
+ const start = stripped.indexOf('{')
201
+ if (start === -1) throw new Error('analyst output contains no JSON object')
202
+ // Balanced-brace scan from the first '{' — models love trailing prose.
203
+ let depth = 0
204
+ let end = -1
205
+ let inString = false
206
+ let escaped = false
207
+ for (let i = start; i < stripped.length; i++) {
208
+ const ch = stripped[i]
209
+ if (inString) {
210
+ if (escaped) escaped = false
211
+ else if (ch === '\\') escaped = true
212
+ else if (ch === '"') inString = false
213
+ continue
214
+ }
215
+ if (ch === '"') inString = true
216
+ else if (ch === '{') depth += 1
217
+ else if (ch === '}') {
218
+ depth -= 1
219
+ if (depth === 0) {
220
+ end = i
221
+ break
222
+ }
223
+ }
224
+ }
225
+ if (end === -1) throw new Error('analyst output JSON object never closes')
226
+ let parsed: unknown
227
+ try {
228
+ parsed = JSON.parse(stripped.slice(start, end + 1))
229
+ } catch (cause) {
230
+ throw new Error(`analyst output is not valid JSON: ${(cause as Error).message}`)
231
+ }
232
+ const findings = (parsed as { findings?: unknown }).findings
233
+ if (!Array.isArray(findings) || findings.length === 0) {
234
+ throw new Error('analyst output has no findings array')
235
+ }
236
+ return findings.map((f, i) => {
237
+ const o = f as Record<string, unknown>
238
+ const failureClass = typeof o.failure_class === 'string' ? o.failure_class.trim() : ''
239
+ if (!failureClass) throw new Error(`finding ${i} missing failure_class`)
240
+ const conf = typeof o.confidence === 'number' && Number.isFinite(o.confidence) ? o.confidence : 0.5
241
+ return {
242
+ failure_class: failureClass,
243
+ evidence_quote: typeof o.evidence_quote === 'string' ? o.evidence_quote : '',
244
+ proposed_direction: typeof o.proposed_direction === 'string' ? o.proposed_direction : '',
245
+ confidence: Math.min(1, Math.max(0, conf)),
246
+ }
247
+ })
248
+ }
249
+
250
+ /** POST one blind analyst call through the router via dotenvx (key stays in
251
+ * the child; response body lands in `outFile`, never on shared stdout).
252
+ * ONE retry on transport failure (5xx/524/timeout) — an edge flake was
253
+ * observed live in the calibration smoke; a parse failure is NOT retried
254
+ * (same prompt, same model ⇒ same bad shape). */
255
+ export async function runAnalyst(
256
+ spec: AnalystSpec,
257
+ bundle: string,
258
+ secrets: SecretsEnv,
259
+ scratchDir: string,
260
+ opts: { timeoutMs?: number; retries?: number; retryDelayMs?: number; signal?: AbortSignal } = {},
261
+ ): Promise<AnalystReport> {
262
+ opts.signal?.throwIfAborted()
263
+ const apiKeyEnv = spec.apiKeyEnv ?? 'TANGLE_API_KEY'
264
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(apiKeyEnv)) {
265
+ return { analystId: spec.id, model: spec.model, ok: false, findings: [], error: `invalid apiKeyEnv name: ${apiKeyEnv}` }
266
+ }
267
+ const url = spec.url ?? ROUTER_ENDPOINT
268
+ const body = JSON.stringify({
269
+ model: spec.model,
270
+ messages: [{ role: 'user', content: analystPrompt(bundle) }],
271
+ temperature: spec.temperature ?? 0,
272
+ max_tokens: spec.maxTokens ?? 16_000,
273
+ })
274
+ opts.signal?.throwIfAborted()
275
+ await mkdir(scratchDir, { recursive: true })
276
+ const outFile = join(scratchDir, `analyst-${spec.id.replace(/[^a-zA-Z0-9._-]/g, '_')}.response.json`)
277
+ const timeoutMs = opts.timeoutMs ?? 600_000
278
+ // Body via stdin (--data @-) so the payload never sits on a command line;
279
+ // the HTTP code is marker-anchored on stdout (dotenvx banners share stdout).
280
+ const script =
281
+ `curl -sS -o "$DIAG_OUT" -w "HTTP_CODE=%{http_code}" --max-time ${Math.ceil(timeoutMs / 1000) - 20} ` +
282
+ `-X POST "$DIAG_URL" -H "Authorization: Bearer $${apiKeyEnv}" -H "Content-Type: application/json" --data @-`
283
+ const argv = ['run', ...secrets.envFiles.flatMap((f) => ['-f', f]), '--', 'bash', '-c', script]
284
+ const attempts = 1 + (opts.retries ?? 1)
285
+ let transportError = ''
286
+ let transported = false
287
+ for (let attempt = 0; attempt < attempts && !transported; attempt++) {
288
+ opts.signal?.throwIfAborted()
289
+ if (attempt > 0) await sleepWithSignal(opts.retryDelayMs ?? 5_000, opts.signal)
290
+ const res = await run('dotenvx', argv, {
291
+ cwd: secrets.secretsDir,
292
+ timeoutMs,
293
+ stdin: body,
294
+ env: { ...process.env, DIAG_URL: url, DIAG_OUT: outFile },
295
+ signal: opts.signal,
296
+ })
297
+ opts.signal?.throwIfAborted()
298
+ const codeMatch = res.stdout.match(/HTTP_CODE=(\d{3})\s*$/)
299
+ if (codeMatch && codeMatch[1] === '200') {
300
+ transported = true
301
+ } else {
302
+ transportError = `router call failed (http=${codeMatch?.[1] ?? 'none'}, rc=${res.code}${res.timedOut ? ', timeout' : ''}, attempt=${attempt + 1}/${attempts})`
303
+ }
304
+ }
305
+ if (!transported) {
306
+ return { analystId: spec.id, model: spec.model, ok: false, findings: [], error: transportError }
307
+ }
308
+ let content = ''
309
+ let tokens: AnalystReport['tokens']
310
+ try {
311
+ const parsed = JSON.parse(await readFile(outFile, 'utf8')) as {
312
+ choices?: Array<{ message?: { content?: string } }>
313
+ usage?: { prompt_tokens?: number; completion_tokens?: number }
314
+ }
315
+ content = parsed.choices?.[0]?.message?.content ?? ''
316
+ if (parsed.usage) {
317
+ tokens = { input: parsed.usage.prompt_tokens ?? 0, output: parsed.usage.completion_tokens ?? 0 }
318
+ }
319
+ } catch (cause) {
320
+ return { analystId: spec.id, model: spec.model, ok: false, findings: [], error: `unparseable response body: ${(cause as Error).message}` }
321
+ }
322
+ if (content.trim().length === 0) {
323
+ return { analystId: spec.id, model: spec.model, ok: false, findings: [], error: 'empty content (max_tokens starvation?)', tokens }
324
+ }
325
+ try {
326
+ const findings = parseAnalystFindings(content)
327
+ return { analystId: spec.id, model: spec.model, ok: true, findings, rawText: content, tokens }
328
+ } catch (cause) {
329
+ return { analystId: spec.id, model: spec.model, ok: false, findings: [], error: (cause as Error).message, rawText: content, tokens }
330
+ }
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Fusion — union + agreement rank; minority findings survive flagged.
335
+ // ---------------------------------------------------------------------------
336
+
337
+ export interface FusedFinding {
338
+ /** Representative label (the first-seen member's failure_class). */
339
+ failure_class: string
340
+ /** Distinct analysts whose findings landed in this cluster. */
341
+ analysts: string[]
342
+ agreement: number
343
+ /** True when only ONE analyst surfaced it while others reported ok — a
344
+ * surviving minority view, kept as an explicit competing hypothesis. */
345
+ competingHypothesis: boolean
346
+ meanConfidence: number
347
+ evidence: Array<{ analyst: string; quote: string }>
348
+ directions: Array<{ analyst: string; direction: string }>
349
+ }
350
+
351
+ const CLASS_STOPWORDS = new Set([
352
+ 'the', 'a', 'an', 'of', 'in', 'on', 'to', 'and', 'or', 'is', 'for', 'with', 'by', 'at', 'not', 'no',
353
+ ])
354
+
355
+ /** Normalized token set of a failure-class label. */
356
+ export function classTokens(label: string): string[] {
357
+ return [
358
+ ...new Set(
359
+ label
360
+ .toLowerCase()
361
+ .split(/[^a-z0-9]+/)
362
+ .filter((t) => t.length > 1 && !CLASS_STOPWORDS.has(t)),
363
+ ),
364
+ ].sort()
365
+ }
366
+
367
+ /** Jaccard similarity of two class labels' token sets. */
368
+ export function classSimilarity(a: string, b: string): number {
369
+ const ta = classTokens(a)
370
+ const tb = new Set(classTokens(b))
371
+ if (ta.length === 0 || tb.size === 0) return 0
372
+ let inter = 0
373
+ for (const t of ta) if (tb.has(t)) inter += 1
374
+ return inter / (ta.length + tb.size - inter)
375
+ }
376
+
377
+ /**
378
+ * Fuse per-analyst findings: greedy clustering on failure-class similarity
379
+ * (>= threshold joins the first matching cluster), then rank by agreement
380
+ * (desc) and mean confidence (desc). Every input finding survives — union, not
381
+ * intersection; a single-analyst cluster is marked `competingHypothesis` when
382
+ * at least one OTHER analyst produced a successful report.
383
+ */
384
+ export function fuseFindings(
385
+ reports: AnalystReport[],
386
+ opts: { similarityThreshold?: number } = {},
387
+ ): FusedFinding[] {
388
+ const threshold = opts.similarityThreshold ?? 0.5
389
+ const okReports = reports.filter((r) => r.ok)
390
+ interface Cluster {
391
+ representative: string
392
+ members: Array<{ analyst: string; finding: AnalystRawFinding }>
393
+ }
394
+ const clusters: Cluster[] = []
395
+ for (const report of okReports) {
396
+ for (const finding of report.findings) {
397
+ const match = clusters.find((c) => classSimilarity(c.representative, finding.failure_class) >= threshold)
398
+ if (match) {
399
+ match.members.push({ analyst: report.analystId, finding })
400
+ } else {
401
+ clusters.push({ representative: finding.failure_class, members: [{ analyst: report.analystId, finding }] })
402
+ }
403
+ }
404
+ }
405
+ const fused = clusters.map((c): FusedFinding => {
406
+ const analysts = [...new Set(c.members.map((m) => m.analyst))]
407
+ return {
408
+ failure_class: c.representative,
409
+ analysts,
410
+ agreement: analysts.length,
411
+ competingHypothesis: analysts.length === 1 && okReports.length > 1,
412
+ meanConfidence:
413
+ c.members.reduce((s, m) => s + m.finding.confidence, 0) / c.members.length,
414
+ evidence: c.members
415
+ .filter((m) => m.finding.evidence_quote.length > 0)
416
+ .map((m) => ({ analyst: m.analyst, quote: m.finding.evidence_quote })),
417
+ directions: c.members
418
+ .filter((m) => m.finding.proposed_direction.length > 0)
419
+ .map((m) => ({ analyst: m.analyst, direction: m.finding.proposed_direction })),
420
+ }
421
+ })
422
+ return fused.sort((a, b) => b.agreement - a.agreement || b.meanConfidence - a.meanConfidence)
423
+ }
424
+
425
+ // ---------------------------------------------------------------------------
426
+ // The ensemble.
427
+ // ---------------------------------------------------------------------------
428
+
429
+ export interface DiagnosisEnsembleResult {
430
+ bundleChars: number
431
+ reports: AnalystReport[]
432
+ fused: FusedFinding[]
433
+ }
434
+
435
+ /** Run every analyst (sequentially — gentle on the shared router key) over the
436
+ * SAME bundle and fuse. A failed analyst is recorded, never fabricated. */
437
+ export async function runDiagnosisEnsemble(input: {
438
+ analysts: AnalystSpec[]
439
+ runs: SupRunArtifacts[]
440
+ secrets: SecretsEnv
441
+ scratchDir: string
442
+ maxBundleChars?: number
443
+ timeoutMsPerAnalyst?: number
444
+ /** Transport retries per analyst (router 524 storms are a measured, known
445
+ * infra class — the loops brain itself runs with LOOPS_BRAIN_RETRIES=30). */
446
+ retriesPerAnalyst?: number
447
+ retryDelayMs?: number
448
+ onStatus?: (msg: string) => void
449
+ signal?: AbortSignal
450
+ }): Promise<DiagnosisEnsembleResult> {
451
+ input.signal?.throwIfAborted()
452
+ if (input.analysts.length === 0) throw new Error('diagnosis ensemble: no analysts configured')
453
+ if (input.runs.length === 0) throw new Error('diagnosis ensemble: no run artifacts to diagnose')
454
+ const bundle = await buildArtifactBundle(
455
+ input.runs,
456
+ input.maxBundleChars !== undefined ? { maxChars: input.maxBundleChars } : {},
457
+ )
458
+ input.signal?.throwIfAborted()
459
+ await mkdir(input.scratchDir, { recursive: true })
460
+ await writeFile(join(input.scratchDir, 'bundle.txt'), bundle)
461
+ input.signal?.throwIfAborted()
462
+ const reports: AnalystReport[] = []
463
+ for (const spec of input.analysts) {
464
+ input.signal?.throwIfAborted()
465
+ input.onStatus?.(`analyst ${spec.id} (${spec.model}) reading ${bundle.length} chars…`)
466
+ const report = await runAnalyst(spec, bundle, input.secrets, input.scratchDir, {
467
+ ...(input.timeoutMsPerAnalyst !== undefined ? { timeoutMs: input.timeoutMsPerAnalyst } : {}),
468
+ ...(input.retriesPerAnalyst !== undefined ? { retries: input.retriesPerAnalyst } : {}),
469
+ ...(input.retryDelayMs !== undefined ? { retryDelayMs: input.retryDelayMs } : {}),
470
+ ...(input.signal ? { signal: input.signal } : {}),
471
+ })
472
+ input.signal?.throwIfAborted()
473
+ input.onStatus?.(
474
+ `analyst ${spec.id}: ${report.ok ? `${report.findings.length} finding(s)` : `FAILED (${report.error})`}` +
475
+ (report.tokens ? ` [tokens in=${report.tokens.input} out=${report.tokens.output}]` : ''),
476
+ )
477
+ reports.push(report)
478
+ }
479
+ return { bundleChars: bundle.length, reports, fused: fuseFindings(reports) }
480
+ }
481
+
482
+ /** Calibration grading: does a finding surface FIX PLACEMENT (the known truth
483
+ * of the round-2 django run — a locally-authored helper where the gold fix
484
+ * extends django/utils/encoding.py)? Assistive keyword net; the smoke always
485
+ * prints the raw findings so a miss here is auditable, never load-bearing. */
486
+ export function surfacesPlacementRegex(): RegExp {
487
+ return /placement|misplac|wrong\s+(file|location|module|place)|different\s+(file|location|module)|expected\s+(location|file|module)|local\s+(helper|copy|implementation)|duplicat|encoding\.py|django\.utils\.encoding|utils\/encoding|belongs\s+in|should\s+(live|be\s+placed|be\s+located|go)\s+in|reimplement|instead\s+of\s+(the\s+)?(existing|shared|upstream)/i
488
+ }
489
+
490
+ /** Map fused findings onto the substrate's `AnalystFinding` envelope so they
491
+ * drop into the same `analyzeGeneration` slot the raw-trace distiller uses. */
492
+ export function fusedToAnalystFindings(
493
+ fused: FusedFinding[],
494
+ evidence: { dirs: string[]; totalAnalysts: number },
495
+ ): AnalystFinding[] {
496
+ return fused.map((f) => {
497
+ const directions = f.directions.map((d) => `[${d.analyst}] ${d.direction}`).join(' | ')
498
+ const quotes = f.evidence
499
+ .slice(0, 3)
500
+ .map((e) => `[${e.analyst}] "${e.quote.slice(0, 240)}"`)
501
+ .join('; ')
502
+ return makeFinding({
503
+ analyst_id: 'diagnosis-ensemble',
504
+ severity: f.agreement >= 2 ? 'high' : 'medium',
505
+ area: 'failure-diagnosis',
506
+ confidence: Math.min(1, Math.max(0, f.meanConfidence)),
507
+ claim:
508
+ `${f.competingHypothesis ? '[competing hypothesis] ' : ''}` +
509
+ `(${f.agreement}/${evidence.totalAnalysts} analysts) ${f.failure_class}` +
510
+ (quotes ? ` — evidence: ${quotes}` : ''),
511
+ ...(directions ? { recommended_action: directions } : {}),
512
+ evidence_refs: evidence.dirs.map((d) => ({ kind: 'artifact' as const, uri: d })),
513
+ metadata: {
514
+ agreement: f.agreement,
515
+ competingHypothesis: f.competingHypothesis,
516
+ analysts: f.analysts,
517
+ },
518
+ })
519
+ })
520
+ }