@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,489 @@
1
+ /**
2
+ * Gen-3 proposer fan-out — N configured proposers author candidates
3
+ * CONCURRENTLY, each as an `AgentProfile`-pinned harness invocation in its own
4
+ * scratch loops worktree, followed by a CHEAP PRE-FILTER so only survivors
5
+ * reach the expensive full evaluation (reps × instances arm cells).
6
+ *
7
+ * Shape (gen3_design, supervisor-lab/.evolve/state.json):
8
+ * stage 1 — parallel authoring. The improvement driver calls
9
+ * `generate()` once per candidate index, serially. The FIRST call of a
10
+ * generation triggers the fan-out barrier: every proposer authors at once
11
+ * (Promise.all) in scratch worktrees created from the incumbent commit;
12
+ * later calls consume the finished outcomes and apply each proposer's
13
+ * patch onto the driver-owned candidate worktree. Authoring parallelism is
14
+ * free; eval bandwidth stays the binding resource.
15
+ * stage 2 — pre-filter per candidate BEFORE arm spend: change-space check +
16
+ * `tsc` run inside the authoring loop's verifier (existing), plus ONE
17
+ * smoke cell (cheapest instance, 1 rep) through the injected
18
+ * `SmokeRunner`. Killed candidates never become surfaces (the driver
19
+ * discards their worktree — zero arm cells); each kill is recorded and
20
+ * drained into a staircase dot with verdict `rejected-prefilter`.
21
+ * stage 3 — survivors proceed through the unchanged pinned-baseline
22
+ * reps-fail-closed gate.
23
+ *
24
+ * Proposer identity: each proposer is an `AgentProfile` invocation
25
+ * (`agenticGenerator`'s `profile` option → `harnessInvocation`, which threads
26
+ * `prompt.systemPrompt`/`instructions` into the composed prompt and
27
+ * `model.default` into the harness's `-m` flag — verified supported for the
28
+ * claude CLI; a bare profile is byte-identical to the legacy prompt-only
29
+ * invocation). Profile `resources` are only materialized on the reproducible
30
+ * Codex path, so a non-codex proposer with resources fails loud here instead
31
+ * of silently dropping them.
32
+ *
33
+ * NOTE on the import cycle with outer-loop.mts: outer-loop imports this
34
+ * module's generator factory; this module imports outer-loop's change-space +
35
+ * verifier primitives. Every cross-reference is inside a function body (never
36
+ * at module-evaluation time), which ESM resolves correctly.
37
+ */
38
+
39
+ import { createHash } from 'node:crypto'
40
+ import { readFileSync } from 'node:fs'
41
+ import { mkdir, rm, writeFile } from 'node:fs/promises'
42
+ import { join } from 'node:path'
43
+ import { fileURLToPath } from 'node:url'
44
+ import {
45
+ agenticGenerator,
46
+ type CandidateGenerator,
47
+ type AgenticGeneratorShotExecution,
48
+ type AgenticGeneratorShotReceipt,
49
+ } from '@tangle-network/agent-runtime'
50
+ import { runLocalHarness } from '@tangle-network/agent-runtime/mcp'
51
+ import type { AgentProfile } from '@tangle-network/agent-interface'
52
+ import type { AnalystFinding, CostLedgerHandle } from '@tangle-network/agent-eval'
53
+ import {
54
+ changeSpaceViolations,
55
+ loopsCandidateVerifier,
56
+ proposerShotEnv,
57
+ purgeIgnoredArtifacts,
58
+ round4BuildPrompt,
59
+ type OuterLoopConfig,
60
+ } from './outer-loop.mts'
61
+ import { run, runOk } from './proc.ts'
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Config types.
65
+ // ---------------------------------------------------------------------------
66
+
67
+ export interface ProposerSpec {
68
+ /** Unique short name — becomes the candidate's staircase `label`. */
69
+ name: string
70
+ /** Path to an `AgentProfile` JSON. Absolute, or relative to this module's
71
+ * `profiles/` directory. Omitted = bare profile (legacy invocation). */
72
+ profile?: string
73
+ harness: 'claude' | 'codex' | 'opencode'
74
+ /** Free-text authoring lens appended to the task prompt (e.g. a mechanics
75
+ * or reviewer stance). Never overrides the protocol change-space text. */
76
+ lens?: string
77
+ /** Which diagnosis findings this proposer sees. Protocol/steering and
78
+ * raw-trace-context findings always pass through. Default 'all'. */
79
+ diagnosisSlice?: 'all' | 'mechanics' | 'prompts'
80
+ }
81
+
82
+ export interface PrefilterConfig {
83
+ enabled: boolean
84
+ /** Instance for the single smoke cell: an explicit iid (e.g.
85
+ * 'pallets__flask-5014' once its verify fixture is committed) or
86
+ * 'cheapest-of-set' — the improvement-set instance with the smallest
87
+ * measured baseline wall time. */
88
+ smokeInstance: string
89
+ /** When true the smoke cell must RESOLVE to survive (right for a designated
90
+ * easy instance). Default false: the mechanism bar — the arm must complete,
91
+ * deliver a non-empty patch, and draw a conclusive judge verdict — so a
92
+ * hard cheapest-of-set instance cannot kill every candidate. */
93
+ requireResolved?: boolean
94
+ }
95
+
96
+ export interface SmokeVerdict {
97
+ iid: string
98
+ pass: boolean
99
+ reason: string
100
+ resolved: boolean | null
101
+ patchLines: number
102
+ wallS: number
103
+ }
104
+
105
+ export type SmokeRunner = (args: {
106
+ scratchPath: string
107
+ generation: number
108
+ proposer: ProposerSpec
109
+ costLedger?: CostLedgerHandle
110
+ }) => Promise<SmokeVerdict>
111
+
112
+ export interface PrefilterKill {
113
+ generation: number
114
+ candidateIndex: number
115
+ proposer: string
116
+ harness: ProposerSpec['harness']
117
+ stage: 'change-space' | 'smoke'
118
+ reason: string
119
+ diffSha256: string | null
120
+ /** Persisted kill-forensics patch under `<outDir>/proposer-patches/`. */
121
+ patchPath: string | null
122
+ smoke?: SmokeVerdict
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Author profiles.
127
+ // ---------------------------------------------------------------------------
128
+
129
+ export const PROFILES_DIR = fileURLToPath(new URL('./profiles', import.meta.url))
130
+
131
+ /** Load + validate a proposer's `AgentProfile`. Fail-closed on resources for
132
+ * non-codex harnesses: `agenticGenerator` only materializes profile resource
133
+ * files on the reproducible Codex path, so accepting them here would drop
134
+ * them silently. */
135
+ export function loadAuthorProfile(spec: ProposerSpec): AgentProfile | undefined {
136
+ if (!spec.profile) return undefined
137
+ const path = spec.profile.startsWith('/') ? spec.profile : join(PROFILES_DIR, spec.profile)
138
+ const profile = JSON.parse(readFileSync(path, 'utf8')) as AgentProfile
139
+ if (typeof profile.name !== 'string' || profile.name.length === 0) {
140
+ throw new Error(`proposer ${spec.name}: profile ${path} has no name`)
141
+ }
142
+ if (profile.resources && spec.harness !== 'codex') {
143
+ throw new Error(
144
+ `proposer ${spec.name}: profile ${path} declares resources, which only materialize on the ` +
145
+ `reproducible codex path — the ${spec.harness} harness would silently drop them`,
146
+ )
147
+ }
148
+ return profile
149
+ }
150
+
151
+ /** The gen-2 author, codified: one bare-profile claude proposer. */
152
+ export function defaultProposers(): ProposerSpec[] {
153
+ return [{ name: 'default-author', profile: 'default-author.profile.json', harness: 'claude' }]
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Diagnosis slicing + prompt lens.
158
+ // ---------------------------------------------------------------------------
159
+
160
+ const PROMPTY = /prompt|instruction|wording|message text|phrasing/i
161
+
162
+ /** Findings that must reach EVERY proposer regardless of slice: the protocol
163
+ * steering finding and the raw-trace-context findings (they are contract +
164
+ * grounding, not diagnosis). */
165
+ function isSteeringOrRawTrace(f: Record<string, unknown>): boolean {
166
+ return (
167
+ f.analyst_id === 'round4-protocol' ||
168
+ f.analyst_id === 'raw-trace-distiller' ||
169
+ f.area === 'raw-trace-context' ||
170
+ f.area === 'constraint'
171
+ )
172
+ }
173
+
174
+ /** Slice the diagnosis findings for one proposer. Pure. */
175
+ export function sliceFindings(findings: AnalystFinding[], slice: ProposerSpec['diagnosisSlice']): AnalystFinding[] {
176
+ if (slice === undefined || slice === 'all') return findings
177
+ return findings.filter((finding) => {
178
+ const f = finding as unknown as Record<string, unknown>
179
+ if (isSteeringOrRawTrace(f)) return true
180
+ const text = `${String(f.area ?? '')} ${String(f.subject ?? '')} ${String(f.claim ?? '')} ${String(f.recommended_action ?? '')}`
181
+ return slice === 'prompts' ? PROMPTY.test(text) : !PROMPTY.test(text)
182
+ })
183
+ }
184
+
185
+ /** The proposer's task prompt: the shared round prompt plus this proposer's
186
+ * authoring lens (appended so the protocol change-space text stays intact). */
187
+ export function proposerBuildPrompt(
188
+ args: { report: unknown; findings: Array<Record<string, unknown>> },
189
+ spec: ProposerSpec,
190
+ ): string {
191
+ const base = round4BuildPrompt(args)
192
+ if (!spec.lens) return base
193
+ return `${base}\n\nYOUR AUTHORING LENS (${spec.name}):\n${spec.lens}`
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Shared shot persistence + spend settlement (used by the fan-out authors AND
198
+ // the legacy single-proposer generator in outer-loop.mts — one implementation).
199
+ // ---------------------------------------------------------------------------
200
+
201
+ export function proposerShotHooks(opts: {
202
+ shotDir: string
203
+ harness: ProposerSpec['harness']
204
+ ledger: () => CostLedgerHandle | undefined
205
+ phase: () => string | undefined
206
+ }): (receipt: AgenticGeneratorShotReceipt, execution: AgenticGeneratorShotExecution | null) => Promise<void> {
207
+ return async (receipt, execution) => {
208
+ const tail = (s: string | undefined): string | null =>
209
+ s === undefined ? null : s.length > 20_000 ? s.slice(-20_000) : s
210
+ await mkdir(opts.shotDir, { recursive: true })
211
+ const name = `gen${receipt.generation ?? 'x'}-cand${receipt.candidateIndex ?? 'x'}-shot${receipt.shot}.json`
212
+ await writeFile(
213
+ join(opts.shotDir, name),
214
+ JSON.stringify({ receipt, stdoutTail: tail(execution?.stdout), stderrTail: tail(execution?.stderr) }, null, 2),
215
+ )
216
+ // Proposer-shot spend → the lib's run ledger. The generator only settles
217
+ // its own receipts on the codexReproducible path (costCallId non-null);
218
+ // the claude/opencode author path otherwise leaves every shot as $0 in
219
+ // the run's spend summary. Import the shot receipt's measured usage.
220
+ const ledger = opts.ledger()
221
+ if (ledger && receipt.costCallId === null && (receipt.usage || receipt.costUsdKnown)) {
222
+ const usage = receipt.usage
223
+ const paid = await ledger.runPaidCall({
224
+ channel: 'driver',
225
+ phase: opts.phase() ?? 'search.proposal',
226
+ actor: `proposer-shot:${opts.harness}`,
227
+ model: receipt.model ?? `${opts.harness}-cli`,
228
+ tags: {
229
+ generation: String(receipt.generation ?? -1),
230
+ candidateIndex: String(receipt.candidateIndex ?? -1),
231
+ shot: String(receipt.shot),
232
+ },
233
+ execute: async () => receipt,
234
+ receipt: () => ({
235
+ model: receipt.model ?? `${opts.harness}-cli`,
236
+ inputTokens: usage?.inputTokens ?? 0,
237
+ outputTokens: usage ? usage.outputTokens + usage.reasoningOutputTokens : 0,
238
+ ...(usage ? { cachedTokens: usage.cachedInputTokens } : { usageUnknown: true }),
239
+ ...(receipt.costUsdKnown && receipt.costUsd !== null ? { actualCostUsd: receipt.costUsd } : {}),
240
+ }),
241
+ })
242
+ if (!paid.succeeded) throw paid.error
243
+ }
244
+ }
245
+ }
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // Scratch authoring worktrees (no node_modules symlink — the verifier and the
249
+ // smoke runner link one in temporarily when they need it).
250
+ // ---------------------------------------------------------------------------
251
+
252
+ async function addAuthoringWorktree(loopsRepo: string, commit: string, dest: string): Promise<void> {
253
+ await run('git', ['-C', loopsRepo, 'worktree', 'remove', '--force', '--', dest])
254
+ await rm(dest, { recursive: true, force: true })
255
+ await run('git', ['-C', loopsRepo, 'worktree', 'prune'])
256
+ await runOk('git', ['-C', loopsRepo, 'worktree', 'add', '--detach', dest, commit])
257
+ }
258
+
259
+ async function removeAuthoringWorktree(loopsRepo: string, dest: string): Promise<void> {
260
+ const res = await run('git', ['-C', loopsRepo, 'worktree', 'remove', '--force', '--', dest])
261
+ if (res.code !== 0) {
262
+ await rm(dest, { recursive: true, force: true })
263
+ await run('git', ['-C', loopsRepo, 'worktree', 'prune'])
264
+ }
265
+ }
266
+
267
+ const sanitize = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_')
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // The fan-out generator.
271
+ // ---------------------------------------------------------------------------
272
+
273
+ /** One proposer's authoring + pre-filter outcome for a generation. */
274
+ interface AuthorOutcome {
275
+ proposer: ProposerSpec
276
+ applied: boolean
277
+ summary: string
278
+ /** Full staged diff (tracked edits + untracked deliverables) vs the
279
+ * incumbent commit. Null when not applied or killed. */
280
+ patch: string | null
281
+ kill: PrefilterKill | null
282
+ }
283
+
284
+ /** Authoring seam — test-injectable. The default runs the proposer's
285
+ * `agenticGenerator` (profile-pinned harness invocation) in the scratch
286
+ * worktree. */
287
+ export type AuthorFn = (
288
+ proposer: ProposerSpec,
289
+ args: Parameters<CandidateGenerator['generate']>[0],
290
+ ) => Promise<{ applied: boolean; summary: string }>
291
+
292
+ export interface FanOutDeps {
293
+ author?: AuthorFn
294
+ smokeRunner?: SmokeRunner
295
+ log?: (msg: string) => void
296
+ }
297
+
298
+ export interface FanOutGenerator extends CandidateGenerator {
299
+ /** Prefilter kills recorded so far; draining clears the buffer. The caller
300
+ * turns each into a `rejected-prefilter` staircase dot. */
301
+ drainPrefilterKills(): PrefilterKill[]
302
+ }
303
+
304
+ function defaultAuthor(config: OuterLoopConfig, deps: FanOutDeps): AuthorFn {
305
+ // One agenticGenerator per proposer, created lazily and cached: each carries
306
+ // its own profile, lens prompt, and shot-receipt home.
307
+ const inners = new Map<string, CandidateGenerator>()
308
+ const ledgers = new Map<string, { ledger?: CostLedgerHandle; phase?: string }>()
309
+ return (proposer, args) => {
310
+ let inner = inners.get(proposer.name)
311
+ const slot = ledgers.get(proposer.name) ?? {}
312
+ ledgers.set(proposer.name, slot)
313
+ if (!inner) {
314
+ const profile = loadAuthorProfile(proposer)
315
+ inner = agenticGenerator({
316
+ harness: proposer.harness,
317
+ ...(profile ? { profile } : {}),
318
+ timeoutMs: config.proposerTimeoutMs,
319
+ buildPrompt: (a) =>
320
+ proposerBuildPrompt(a as unknown as { report: unknown; findings: Array<Record<string, unknown>> }, proposer),
321
+ verify: loopsCandidateVerifier(config.loopsRepo),
322
+ runHarness: (options) => runLocalHarness({ ...options, env: proposerShotEnv(proposer.harness) }),
323
+ onShotCompleted: proposerShotHooks({
324
+ shotDir: join(config.outDir, 'proposer-shots', sanitize(proposer.name)),
325
+ harness: proposer.harness,
326
+ ledger: () => slot.ledger,
327
+ phase: () => slot.phase,
328
+ }),
329
+ })
330
+ inners.set(proposer.name, inner)
331
+ }
332
+ slot.ledger = args.costLedger
333
+ slot.phase = args.costPhase
334
+ return inner.generate(args)
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Build the fan-out `CandidateGenerator` for `config.proposers`. The driver
340
+ * still owns every candidate worktree; this generator authors in its OWN
341
+ * scratch worktrees (in parallel, one per proposer) and applies each
342
+ * surviving proposer's patch onto the driver worktree for that candidate
343
+ * index. `populationSize` must equal `proposers.length` — enforced by the
344
+ * caller (runRound) and re-checked per call here.
345
+ */
346
+ export function fanOutLoopsGenerator(config: OuterLoopConfig, deps: FanOutDeps = {}): FanOutGenerator {
347
+ const proposers = config.proposers ?? []
348
+ if (proposers.length === 0) throw new Error('fanOutLoopsGenerator: config.proposers is empty')
349
+ const names = new Set(proposers.map((p) => p.name))
350
+ if (names.size !== proposers.length) throw new Error('fanOutLoopsGenerator: duplicate proposer names')
351
+ const author = deps.author ?? defaultAuthor(config, deps)
352
+ const log = deps.log ?? (() => {})
353
+ const kills: PrefilterKill[] = []
354
+ const byGeneration = new Map<number, Promise<AuthorOutcome[]>>()
355
+
356
+ const runFanOut = async (
357
+ generation: number,
358
+ args: Parameters<CandidateGenerator['generate']>[0],
359
+ ): Promise<AuthorOutcome[]> => {
360
+ const head = await runOk('git', ['-C', args.worktreePath, 'rev-parse', 'HEAD'])
361
+ const baseCommit = head.stdout.trim()
362
+ const patchesDir = join(config.outDir, 'proposer-patches')
363
+ await mkdir(patchesDir, { recursive: true })
364
+ log(`fan-out gen ${generation}: ${proposers.length} proposer(s) authoring in parallel from ${baseCommit.slice(0, 10)}`)
365
+ return Promise.all(
366
+ proposers.map(async (proposer, index): Promise<AuthorOutcome> => {
367
+ const scratch = join(config.outDir, 'proposer-wt', `gen${generation}-cand${index}-${sanitize(proposer.name)}`)
368
+ await addAuthoringWorktree(config.loopsRepo, baseCommit, scratch)
369
+ try {
370
+ const authored = await author(proposer, {
371
+ ...args,
372
+ worktreePath: scratch,
373
+ findings: sliceFindings(args.findings, proposer.diagnosisSlice),
374
+ candidateIndex: index,
375
+ })
376
+ if (!authored.applied) {
377
+ log(`fan-out gen ${generation} ${proposer.name}: no verified candidate (applied=false)`)
378
+ return { proposer, applied: false, summary: authored.summary, patch: null, kill: null }
379
+ }
380
+ // Anti-tamper purge BEFORE the diff: ignored artifacts (dep installs)
381
+ // must never reach the candidate worktree or the change-space check.
382
+ await purgeIgnoredArtifacts(scratch)
383
+ await runOk('git', ['-C', scratch, 'add', '-A'])
384
+ const diff = (await runOk('git', ['-C', scratch, 'diff', '--cached'])).stdout
385
+ if (diff.trim().length === 0) {
386
+ return { proposer, applied: false, summary: authored.summary, patch: null, kill: null }
387
+ }
388
+ const namesOut = await runOk('git', ['-C', scratch, 'diff', '--cached', '--name-only'])
389
+ const changed = namesOut.stdout.split('\n').map((s) => s.trim()).filter(Boolean)
390
+ const diffSha256 = `sha256:${createHash('sha256').update(diff).digest('hex')}`
391
+ const patchPath = join(patchesDir, `gen${generation}-cand${index}-${sanitize(proposer.name)}.patch`)
392
+ await writeFile(patchPath, diff)
393
+
394
+ // Pre-filter stage A — change-space (defense-in-depth: the authoring
395
+ // verifier already enforced it shot-by-shot; the final diff rules).
396
+ const violations = changeSpaceViolations(changed)
397
+ if (violations.length > 0) {
398
+ const kill: PrefilterKill = {
399
+ generation,
400
+ candidateIndex: index,
401
+ proposer: proposer.name,
402
+ harness: proposer.harness,
403
+ stage: 'change-space',
404
+ reason: `out-of-space paths: ${violations.join(', ')}`,
405
+ diffSha256,
406
+ patchPath,
407
+ }
408
+ kills.push(kill)
409
+ log(`prefilter KILL gen ${generation} ${proposer.name}: ${kill.reason}`)
410
+ return { proposer, applied: false, summary: authored.summary, patch: null, kill }
411
+ }
412
+
413
+ // Pre-filter stage B — one smoke cell before any full-arm spend.
414
+ if (config.prefilter?.enabled && deps.smokeRunner) {
415
+ const smoke = await deps.smokeRunner({
416
+ scratchPath: scratch,
417
+ generation,
418
+ proposer,
419
+ ...(args.costLedger ? { costLedger: args.costLedger } : {}),
420
+ })
421
+ if (!smoke.pass) {
422
+ const kill: PrefilterKill = {
423
+ generation,
424
+ candidateIndex: index,
425
+ proposer: proposer.name,
426
+ harness: proposer.harness,
427
+ stage: 'smoke',
428
+ reason: smoke.reason,
429
+ diffSha256,
430
+ patchPath,
431
+ smoke,
432
+ }
433
+ kills.push(kill)
434
+ log(`prefilter KILL gen ${generation} ${proposer.name}: ${kill.reason}`)
435
+ return { proposer, applied: false, summary: authored.summary, patch: null, kill }
436
+ }
437
+ log(`prefilter pass gen ${generation} ${proposer.name}: ${smoke.reason}`)
438
+ }
439
+ return { proposer, applied: true, summary: authored.summary, patch: diff, kill: null }
440
+ } finally {
441
+ await removeAuthoringWorktree(config.loopsRepo, scratch)
442
+ }
443
+ }),
444
+ )
445
+ }
446
+
447
+ return {
448
+ kind: `gen3-fanout:${proposers.map((p) => `${p.name}@${p.harness}`).join('+')}`,
449
+ proposesWithoutFindings: true,
450
+ drainPrefilterKills() {
451
+ return kills.splice(0, kills.length)
452
+ },
453
+ async generate(args) {
454
+ const generation = args.generation ?? 0
455
+ const index = args.candidateIndex ?? 0
456
+ if (index >= proposers.length) {
457
+ throw new Error(
458
+ `fan-out: candidate index ${index} has no proposer (proposers=${proposers.length}) — ` +
459
+ 'config.populationSize must equal config.proposers.length',
460
+ )
461
+ }
462
+ let pending = byGeneration.get(generation)
463
+ if (!pending) {
464
+ pending = runFanOut(generation, args)
465
+ byGeneration.set(generation, pending)
466
+ }
467
+ const outcomes = await pending
468
+ const outcome = outcomes[index]!
469
+ if (!outcome.applied || outcome.patch === null) return { applied: false, summary: outcome.summary }
470
+
471
+ // Apply the survivor's patch onto the driver-owned candidate worktree.
472
+ const patchFile = join(config.outDir, 'proposer-patches', `.apply-gen${generation}-cand${index}.patch`)
473
+ await writeFile(patchFile, outcome.patch)
474
+ await runOk('git', ['-C', args.worktreePath, 'apply', '--whitespace=nowarn', patchFile])
475
+ await rm(patchFile, { force: true })
476
+ await purgeIgnoredArtifacts(args.worktreePath)
477
+ return {
478
+ applied: true,
479
+ summary: outcome.summary,
480
+ label: outcome.proposer.name,
481
+ rationale:
482
+ `proposer ${outcome.proposer.name} (${outcome.proposer.harness}` +
483
+ `${outcome.proposer.profile ? `, profile ${outcome.proposer.profile}` : ''}` +
484
+ `${outcome.proposer.diagnosisSlice && outcome.proposer.diagnosisSlice !== 'all' ? `, slice ${outcome.proposer.diagnosisSlice}` : ''}` +
485
+ `): ${outcome.summary || 'authored change'}`,
486
+ }
487
+ },
488
+ }
489
+ }