@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,300 @@
1
+ /**
2
+ * Gen-5 activation gate: predicate validation, execution over run artifacts
3
+ * (grep + script, ws/ skip with ws/.loops searched), the prefilter kill for a
4
+ * missing/invalid predicate, and the quarantine verdict path (an improved
5
+ * score with a never-fired mechanism is quarantined, not promoted).
6
+ */
7
+
8
+ import { existsSync } from 'node:fs'
9
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
10
+ import { tmpdir } from 'node:os'
11
+ import { join } from 'node:path'
12
+ import type { AnalystFinding } from '@tangle-network/agent-eval'
13
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
14
+ import {
15
+ ACTIVATION_PREDICATE_RELPATH,
16
+ activationPredicateInstruction,
17
+ parseActivationPredicate,
18
+ runActivationPredicate,
19
+ } from './activation.mts'
20
+ import { decideVerdict } from './cell-evidence.mts'
21
+ import { defaultRound4Config, parseStaircaseRow, STAIRCASE_SCHEMA, type OuterLoopConfig, type StaircaseRow } from './outer-loop.mts'
22
+ import { fanOutLoopsGenerator, type ProposerSpec } from './proposer-fanout.mts'
23
+ import { runOk } from './proc.ts'
24
+
25
+ describe('parseActivationPredicate', () => {
26
+ it('accepts a valid grep predicate (with optional files filters)', () => {
27
+ const parsed = parseActivationPredicate(
28
+ JSON.stringify({
29
+ description: 'patchRiskWarnings emitted in at least one settle',
30
+ kind: 'grep',
31
+ pattern: 'patchRiskWarnings|patch-risk',
32
+ files: ['journal.jsonl', 'workers/'],
33
+ }),
34
+ )
35
+ expect(parsed.ok).toBe(true)
36
+ })
37
+
38
+ it('accepts a valid script predicate', () => {
39
+ const parsed = parseActivationPredicate(
40
+ JSON.stringify({ description: 'x', kind: 'script', script: 'grep -rq X .' }),
41
+ )
42
+ expect(parsed.ok).toBe(true)
43
+ })
44
+
45
+ it.each([
46
+ ['not json', 'not valid JSON'],
47
+ ['[]', 'JSON object'],
48
+ [JSON.stringify({ description: '', kind: 'grep', pattern: 'a' }), 'description'],
49
+ [JSON.stringify({ description: 'x', kind: 'grep' }), 'pattern'],
50
+ [JSON.stringify({ description: 'x', kind: 'grep', pattern: '(' }), 'RegExp'],
51
+ [JSON.stringify({ description: 'x', kind: 'grep', pattern: 'a', files: [1] }), 'files'],
52
+ [JSON.stringify({ description: 'x', kind: 'script' }), 'script'],
53
+ [JSON.stringify({ description: 'x', kind: 'sql' }), 'kind'],
54
+ ])('rejects %s', (raw, want) => {
55
+ const parsed = parseActivationPredicate(raw)
56
+ expect(parsed.ok).toBe(false)
57
+ if (!parsed.ok) expect(parsed.error).toContain(want)
58
+ })
59
+
60
+ it('the prompt instruction carries the template and the quarantine warning', () => {
61
+ const text = activationPredicateInstruction()
62
+ expect(text).toContain(ACTIVATION_PREDICATE_RELPATH)
63
+ expect(text).toContain('QUARANTINED')
64
+ expect(text).toContain('"kind": "grep"')
65
+ })
66
+ })
67
+
68
+ describe('runActivationPredicate', () => {
69
+ let runA: string
70
+ let runB: string
71
+
72
+ beforeEach(async () => {
73
+ runA = await mkdtemp(join(tmpdir(), 'act-a-'))
74
+ runB = await mkdtemp(join(tmpdir(), 'act-b-'))
75
+ await writeFile(join(runA, 'driver.log'), 'boot\nno mechanism here\n')
76
+ await writeFile(join(runB, 'driver.log'), 'boot\npatchRiskWarnings: 2 warnings emitted\n')
77
+ })
78
+
79
+ afterEach(async () => {
80
+ await rm(runA, { recursive: true, force: true })
81
+ await rm(runB, { recursive: true, force: true })
82
+ })
83
+
84
+ const grep = (pattern: string, files?: string[]) => ({
85
+ description: 'd',
86
+ kind: 'grep' as const,
87
+ pattern,
88
+ ...(files ? { files } : {}),
89
+ })
90
+
91
+ it('fires with path:line evidence when the pattern appears in ANY run dir', async () => {
92
+ const res = await runActivationPredicate(grep('patchRiskWarnings'), [runA, runB])
93
+ expect(res.fired).toBe(true)
94
+ expect(res.checkedRunDirs).toBe(2)
95
+ expect(res.evidence[0]).toContain(join(runB, 'driver.log'))
96
+ expect(res.evidence[0]).toContain('patchRiskWarnings')
97
+ })
98
+
99
+ it('does not fire when the pattern never appears (fail-closed)', async () => {
100
+ const res = await runActivationPredicate(grep('neverEverPresent'), [runA, runB])
101
+ expect(res.fired).toBe(false)
102
+ expect(res.evidence).toEqual([])
103
+ })
104
+
105
+ it('honors files filters (relative-path substring)', async () => {
106
+ await writeFile(join(runB, 'other.txt'), 'patchRiskWarnings\n')
107
+ const onlyOther = await runActivationPredicate(grep('patchRiskWarnings', ['other.txt']), [runB])
108
+ expect(onlyOther.fired).toBe(true)
109
+ const onlyMissing = await runActivationPredicate(grep('patchRiskWarnings', ['nope.bin']), [runB])
110
+ expect(onlyMissing.fired).toBe(false)
111
+ })
112
+
113
+ it('skips the ws/ workspace subtree EXCEPT ws/.loops (supervisor artifacts)', async () => {
114
+ // Marker only inside ws/ (a checked-out repo) → must NOT count as fired.
115
+ await mkdir(join(runA, 'ws', 'src'), { recursive: true })
116
+ await writeFile(join(runA, 'ws', 'src', 'code.py'), 'patchRiskWarnings in the repo source\n')
117
+ const inWs = await runActivationPredicate(grep('patchRiskWarnings'), [runA])
118
+ expect(inWs.fired).toBe(false)
119
+ // Marker in ws/.loops → the supervisor's own artifacts, searched.
120
+ await mkdir(join(runA, 'ws', '.loops', 'supervisor', 's1'), { recursive: true })
121
+ await writeFile(join(runA, 'ws', '.loops', 'supervisor', 's1', 'journal.jsonl'), '{"k":"patchRiskWarnings"}\n')
122
+ const inLoops = await runActivationPredicate(grep('patchRiskWarnings'), [runA])
123
+ expect(inLoops.fired).toBe(true)
124
+ })
125
+
126
+ it('script kind fires on rc=0 in any run dir, not-fired otherwise', async () => {
127
+ const script = (s: string) => ({ description: 'd', kind: 'script' as const, script: s })
128
+ const hit = await runActivationPredicate(script('grep -q patchRiskWarnings driver.log'), [runA, runB])
129
+ expect(hit.fired).toBe(true)
130
+ expect(hit.evidence[0]).toContain(runB)
131
+ const miss = await runActivationPredicate(script('grep -q neverEverPresent driver.log'), [runA, runB])
132
+ expect(miss.fired).toBe(false)
133
+ })
134
+ })
135
+
136
+ describe('quarantine verdict path', () => {
137
+ const base = {
138
+ violations: [],
139
+ coverageComplete: true,
140
+ resolvedCount: 3,
141
+ parentResolvedCount: 1,
142
+ costRatio: 1.0,
143
+ costGuardRatio: 1.2,
144
+ }
145
+
146
+ it('quarantines a candidate whose mechanism never fired EVEN when its score improved', () => {
147
+ expect(decideVerdict({ ...base, activationFired: false })).toBe('quarantined-inactive')
148
+ })
149
+
150
+ it('accepts an improved candidate whose mechanism fired; gate-not-applicable behaves as before', () => {
151
+ expect(decideVerdict({ ...base, activationFired: true })).toBe('accepted')
152
+ expect(decideVerdict({ ...base, activationFired: null })).toBe('accepted')
153
+ expect(decideVerdict(base)).toBe('accepted')
154
+ })
155
+
156
+ it('out-of-space still wins over quarantine; quarantine wins over no-gain/cost/coverage', () => {
157
+ expect(decideVerdict({ ...base, violations: ['judge.py'], activationFired: false })).toBe('rejected-out-of-space')
158
+ expect(decideVerdict({ ...base, coverageComplete: false, activationFired: false })).toBe('quarantined-inactive')
159
+ expect(decideVerdict({ ...base, resolvedCount: 1, activationFired: false })).toBe('quarantined-inactive')
160
+ })
161
+
162
+ it('parseStaircaseRow accepts a quarantined row with gen-5 split + activation fields', () => {
163
+ const row: StaircaseRow = {
164
+ schema: STAIRCASE_SCHEMA,
165
+ round: 4,
166
+ generation: 0,
167
+ runId: 'r4-x',
168
+ at: new Date(0).toISOString(),
169
+ candidate: 'hash',
170
+ candidateCommit: 'c'.repeat(40),
171
+ parent: 'baseline',
172
+ parentResolvedCount: 1,
173
+ changedFiles: [],
174
+ changeSpaceViolations: [],
175
+ perInstance: [],
176
+ resolvedCount: 3,
177
+ coverageComplete: true,
178
+ wallS: 10,
179
+ baselineWallS: 10,
180
+ costRatio: 1,
181
+ costGuardRatio: 1.2,
182
+ internallyPromoted: true,
183
+ verdict: 'quarantined-inactive',
184
+ holdout: 'operator-approval-required',
185
+ armProvenance: null,
186
+ diffPath: null,
187
+ diffSha256: null,
188
+ split: {
189
+ publicInstances: ['a', 'b'],
190
+ privateInstances: ['c'],
191
+ publicResolvedCount: 2,
192
+ privateResolvedCount: 1,
193
+ },
194
+ activation: { present: true, description: 'd', fired: false, evidence: [], warnings: [] },
195
+ }
196
+ expect(parseStaircaseRow(JSON.stringify(row))).toEqual(row)
197
+ })
198
+ })
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Prefilter enforcement — a candidate without a parseable predicate is killed
202
+ // before any evaluation spend.
203
+ // ---------------------------------------------------------------------------
204
+
205
+ describe('activation-predicate prefilter', () => {
206
+ let loopsRepo: string
207
+ let outDir: string
208
+ let driverWt: string
209
+
210
+ beforeEach(async () => {
211
+ loopsRepo = await mkdtemp(join(tmpdir(), 'act-repo-'))
212
+ outDir = await mkdtemp(join(tmpdir(), 'act-out-'))
213
+ await runOk('git', ['init', '-q', '-b', 'main', loopsRepo])
214
+ await runOk('git', ['-C', loopsRepo, 'config', 'user.email', 't@t.dev'])
215
+ await runOk('git', ['-C', loopsRepo, 'config', 'user.name', 'T'])
216
+ await writeFile(join(loopsRepo, 'src.ts'), 'base\n')
217
+ await runOk('git', ['-C', loopsRepo, 'add', '-A'])
218
+ await runOk('git', ['-C', loopsRepo, 'commit', '-q', '-m', 'init'])
219
+ driverWt = join(outDir, 'driver-wt')
220
+ await runOk('git', ['-C', loopsRepo, 'worktree', 'add', '--detach', driverWt, 'HEAD'])
221
+ })
222
+
223
+ afterEach(async () => {
224
+ await rm(outDir, { recursive: true, force: true })
225
+ await rm(loopsRepo, { recursive: true, force: true })
226
+ })
227
+
228
+ const config = (proposers: ProposerSpec[]): OuterLoopConfig => ({
229
+ ...defaultRound4Config(),
230
+ loopsRepo,
231
+ outDir,
232
+ populationSize: proposers.length,
233
+ proposers,
234
+ activationGate: true,
235
+ })
236
+
237
+ const generatorArgs = (candidateIndex: number) => ({
238
+ worktreePath: driverWt,
239
+ report: undefined,
240
+ findings: [] as AnalystFinding[],
241
+ maxShots: 1,
242
+ signal: new AbortController().signal,
243
+ generation: 0,
244
+ candidateIndex,
245
+ })
246
+
247
+ it('kills a candidate without .improve/activation.json (stage activation-predicate) and passes one WITH it', async () => {
248
+ const proposers: ProposerSpec[] = [
249
+ { name: 'with-predicate', harness: 'claude' },
250
+ { name: 'without-predicate', harness: 'claude' },
251
+ { name: 'invalid-predicate', harness: 'claude' },
252
+ ]
253
+ const gen = fanOutLoopsGenerator(config(proposers), {
254
+ author: async (proposer, args) => {
255
+ await mkdir(join(args.worktreePath, 'extensions', 'pi'), { recursive: true })
256
+ await writeFile(join(args.worktreePath, 'extensions', 'pi', `${proposer.name}.ts`), 'x\n')
257
+ if (proposer.name === 'with-predicate') {
258
+ await mkdir(join(args.worktreePath, '.improve'), { recursive: true })
259
+ await writeFile(
260
+ join(args.worktreePath, ACTIVATION_PREDICATE_RELPATH),
261
+ JSON.stringify({ description: 'd', kind: 'grep', pattern: 'x' }),
262
+ )
263
+ } else if (proposer.name === 'invalid-predicate') {
264
+ await mkdir(join(args.worktreePath, '.improve'), { recursive: true })
265
+ await writeFile(join(args.worktreePath, ACTIVATION_PREDICATE_RELPATH), '{}')
266
+ }
267
+ return { applied: true, summary: `${proposer.name} edit` }
268
+ },
269
+ })
270
+
271
+ const survivor = await gen.generate(generatorArgs(0))
272
+ const missing = await gen.generate(generatorArgs(1))
273
+ const invalid = await gen.generate(generatorArgs(2))
274
+
275
+ expect(survivor.applied).toBe(true)
276
+ expect(missing.applied).toBe(false)
277
+ expect(invalid.applied).toBe(false)
278
+ const kills = gen.drainPrefilterKills()
279
+ expect(kills).toHaveLength(2)
280
+ expect(kills.find((k) => k.proposer === 'without-predicate')).toMatchObject({ stage: 'activation-predicate' })
281
+ expect(kills.find((k) => k.proposer === 'without-predicate')!.reason).toContain('missing')
282
+ expect(kills.find((k) => k.proposer === 'invalid-predicate')!.reason).toContain('invalid')
283
+ // The survivor's predicate landed on the driver worktree with the patch.
284
+ expect(existsSync(join(driverWt, ACTIVATION_PREDICATE_RELPATH))).toBe(true)
285
+ })
286
+
287
+ it('does not require a predicate when the gate is off (gen-4 behavior unchanged)', async () => {
288
+ const cfg = config([{ name: 'legacy', harness: 'claude' }])
289
+ cfg.activationGate = false
290
+ const gen = fanOutLoopsGenerator(cfg, {
291
+ author: async (_p, args) => {
292
+ await mkdir(join(args.worktreePath, 'extensions', 'pi'), { recursive: true })
293
+ await writeFile(join(args.worktreePath, 'extensions', 'pi', 'l.ts'), 'x\n')
294
+ return { applied: true, summary: 'edit' }
295
+ },
296
+ })
297
+ expect((await gen.generate(generatorArgs(0))).applied).toBe(true)
298
+ expect(gen.drainPrefilterKills()).toEqual([])
299
+ })
300
+ })
@@ -346,6 +346,14 @@ export interface ArmRunContext {
346
346
  instanceId: string
347
347
  image: string
348
348
  baseCommit: string
349
+ /**
350
+ * Workspace materialization override. Default (undefined) = SWE instance
351
+ * image materialization (docker cp of /testbed). Factory instances inject
352
+ * their archive-export + synthetic-history materialization here so the arm
353
+ * runners themselves stay instance-kind-agnostic. Must leave `dest` a git
354
+ * repo whose HEAD is the diff base for patch extraction (`baseCommit`).
355
+ */
356
+ materialize?: (dest: string) => Promise<void>
349
357
  problemStatement: string
350
358
  /** Self-repro verify command (bash -c, cwd = ws) — the MEASUREMENT gate. */
351
359
  verifyCmd: string
@@ -448,13 +456,17 @@ export async function runSoloArm(spec: SoloArmSpec, ctx: ArmRunContext): Promise
448
456
  const ws = join(runDir, 'ws')
449
457
  await mkdir(runDir, { recursive: true })
450
458
  const cell = await prepareIsolatedCellEnvironment(runDir)
451
- await materializeWorkspace({
452
- instanceId: ctx.instanceId,
453
- image: ctx.image,
454
- baseCommit: ctx.baseCommit,
455
- dest: ws,
456
- signal: ctx.signal,
457
- })
459
+ // A caller-supplied materializer (synthetic-history factory cells) replaces the
460
+ // SWE-bench image checkout, but still runs inside the isolated cell above.
461
+ if (ctx.materialize) await ctx.materialize(ws)
462
+ else
463
+ await materializeWorkspace({
464
+ instanceId: ctx.instanceId,
465
+ image: ctx.image,
466
+ baseCommit: ctx.baseCommit,
467
+ dest: ws,
468
+ signal: ctx.signal,
469
+ })
458
470
 
459
471
  const promptFile = join(runDir, 'prompt.txt')
460
472
  await writeFile(promptFile, ctx.problemStatement + (spec.promptSuffix ?? WORKER_PROMPT_SUFFIX))
@@ -578,13 +590,17 @@ export async function runSupervisorArm(spec: SupervisorArmSpec, ctx: ArmRunConte
578
590
  const ws = join(runDir, 'ws')
579
591
  await mkdir(runDir, { recursive: true })
580
592
  const cell = await prepareIsolatedCellEnvironment(runDir)
581
- await materializeWorkspace({
582
- instanceId: ctx.instanceId,
583
- image: ctx.image,
584
- baseCommit: ctx.baseCommit,
585
- dest: ws,
586
- signal: ctx.signal,
587
- })
593
+ // A caller-supplied materializer (synthetic-history factory cells) replaces the
594
+ // SWE-bench image checkout, but still runs inside the isolated cell above.
595
+ if (ctx.materialize) await ctx.materialize(ws)
596
+ else
597
+ await materializeWorkspace({
598
+ instanceId: ctx.instanceId,
599
+ image: ctx.image,
600
+ baseCommit: ctx.baseCommit,
601
+ dest: ws,
602
+ signal: ctx.signal,
603
+ })
588
604
 
589
605
  const paramsFile = join(runDir, 'params.json')
590
606
  await writeFile(
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Gen-5 author briefing — MAP + TOOLBOX + PERMISSION (gen5_design).
3
+ *
4
+ * Principle: fully agentic evidence access, NO pre-digestion machinery. The
5
+ * dossier-compiler / ranked-briefing pipeline is dead; instead each run writes
6
+ * an INDEX FILE (a map: one line per evidence path) and the author prompt
7
+ * names the pre-existing TOOLS the author may drive itself, plus explicit
8
+ * permission to spawn its own subagents under a research budget. The
9
+ * 3-analyst diagnosis ensemble stays as ONE input among these — no longer the
10
+ * sole channel.
11
+ *
12
+ * SELF-IMPROVING BRIEFING: the briefing text itself lives in the declared
13
+ * change-space — `extensions/pi/author-briefing.md` in the loops repo. When
14
+ * that file exists at the incumbent ref it REPLACES the default text below,
15
+ * so a future generation's author can rewrite its own research instructions
16
+ * and the gate decides whether that rewrite earns its keep. The default here
17
+ * is the versioned fallback (`AUTHOR_BRIEFING_VERSION`).
18
+ *
19
+ * PUBLIC/PRIVATE INTERACTION: the index is proposer-visible text, so
20
+ * per-instance evidence rows for PRIVATE instances (score-split.mts) are
21
+ * excluded and the writer fails loud if a private id leaks into the rendered
22
+ * index.
23
+ */
24
+
25
+ import { existsSync } from 'node:fs'
26
+ import { readdir, writeFile } from 'node:fs/promises'
27
+ import { join } from 'node:path'
28
+ import { leaksPrivateInstance, type ScoreSplit } from './score-split.mts'
29
+ import { run } from './proc.ts'
30
+
31
+ export const AUTHOR_BRIEFING_VERSION = 'map-toolbox-v1'
32
+
33
+ /** Loops-repo path (inside the declared change-space) whose content, when
34
+ * present at the incumbent ref, replaces the default briefing text. */
35
+ export const AUTHOR_BRIEFING_RELPATH = 'extensions/pi/author-briefing.md'
36
+
37
+ export const EVIDENCE_INDEX_FILENAME = 'evidence-index.md'
38
+
39
+ /** The default TOOLBOX + PERMISSION briefing. Every tool named here already
40
+ * exists — nothing bespoke is built for the author. */
41
+ export function defaultAuthorBriefing(): string {
42
+ return [
43
+ `<!-- author-briefing ${AUTHOR_BRIEFING_VERSION} (default; override by committing ${AUTHOR_BRIEFING_RELPATH}) -->`,
44
+ 'RESEARCH BRIEFING — you have hands. Read the evidence yourself before editing.',
45
+ '',
46
+ 'TOOLBOX (all pre-existing — drive them yourself):',
47
+ '- traces CLI (published): `npx --yes @tangle-network/traces@latest analyze --help` — analyze any',
48
+ ' harness run trace (worker/supervisor sessions) for failure patterns, tool-call churn, dead ends.',
49
+ '- agent-eval trace analysts (`@tangle-network/agent-eval/analyst`): structured extraction over run',
50
+ ' artifacts when you want findings objects rather than prose.',
51
+ '- AxLLM (`@ax-llm/ax`): question-answering over a corpus too large to read — point it at a run dir',
52
+ ' and ask targeted questions instead of paging through megabytes.',
53
+ '- Plain grep/jq over the evidence map below — often the fastest tool.',
54
+ '',
55
+ 'PERMISSION: you may spawn your own subagents (research fan-out) before writing any code.',
56
+ 'Research budget: up to ~15 minutes / ~200k tokens of reading+subagents before your first edit;',
57
+ 'spend it on the evidence paths in the map, not on re-deriving what they already record.',
58
+ '',
59
+ 'The diagnosis findings in this prompt come from a 3-analyst ensemble. Treat them as ONE input',
60
+ 'among the evidence sources above — verify any finding you build on against the raw artifacts.',
61
+ '',
62
+ `SELF-IMPROVEMENT: this briefing text is part of the change-space (${AUTHOR_BRIEFING_RELPATH}).`,
63
+ 'If your research process was hampered by these instructions, you may edit that file in your',
64
+ 'candidate alongside your main change; future generations will read your version.',
65
+ ].join('\n')
66
+ }
67
+
68
+ /** Resolve the briefing text: the change-space override at `ref` when present,
69
+ * else the versioned default. */
70
+ export async function resolveAuthorBriefing(
71
+ loopsRepo: string,
72
+ ref: string,
73
+ ): Promise<{ text: string; source: 'change-space' | 'default' }> {
74
+ const show = await run('git', ['-C', loopsRepo, 'show', `${ref}:${AUTHOR_BRIEFING_RELPATH}`])
75
+ if (show.code === 0 && show.stdout.trim().length > 0) {
76
+ return { text: show.stdout, source: 'change-space' }
77
+ }
78
+ return { text: defaultAuthorBriefing(), source: 'default' }
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // The evidence index — a MAP, not a briefing: one line per evidence path.
83
+ // ---------------------------------------------------------------------------
84
+
85
+ export interface EvidenceIndexRow {
86
+ path: string
87
+ note: string
88
+ /** Instance the row is specific to (private rows are dropped); null = run-level. */
89
+ iid: string | null
90
+ }
91
+
92
+ export interface EvidenceIndexArgs {
93
+ outDir: string
94
+ /** Staircase + round-summary home. */
95
+ roundsDir: string
96
+ /** Prior-round failure artifacts (dir + optional patch), with instance ids. */
97
+ seedArtifactRuns: Array<{ iid: string; arm: string; dir: string; patchPath?: string }>
98
+ /** Prior run outDirs (e.g. the gen-4 outDir) whose arm-runs/judge evidence
99
+ * the author may mine. */
100
+ priorEvidenceDirs?: string[]
101
+ /** Pareto parent diffs materialized to disk (written by the outer loop). */
102
+ paretoParentPatches?: Array<{ label: string; path: string }>
103
+ /** Public/private split; null = everything is public. */
104
+ split: ScoreSplit | null
105
+ }
106
+
107
+ /** Collect the index rows. Pure over the filesystem — reads directory names
108
+ * only, never file contents. Private-instance rows are excluded. */
109
+ export async function collectEvidenceIndexRows(args: EvidenceIndexArgs): Promise<EvidenceIndexRow[]> {
110
+ const priv = new Set(args.split?.privateInstances ?? [])
111
+ const rows: EvidenceIndexRow[] = []
112
+ const push = (path: string, note: string, iid: string | null = null): void => {
113
+ if (iid !== null && priv.has(iid)) return
114
+ rows.push({ path, note, iid })
115
+ }
116
+
117
+ // Staircase generations + round summaries (the improvement run's history).
118
+ for (const name of (await readdir(args.roundsDir).catch(() => [])).sort()) {
119
+ if (/^gen-\d+\.jsonl$/.test(name)) {
120
+ push(join(args.roundsDir, name), 'staircase rows — every prior candidate: diff, per-instance verdicts, kill reasons')
121
+ } else if (/^round\d+-summary-.*\.json$/.test(name)) {
122
+ push(join(args.roundsDir, name), 'round summary — baseline, winner, gate reasons, cost rollup')
123
+ }
124
+ }
125
+
126
+ // Prior-round seed artifacts (worker evidence, judge output, patches).
127
+ for (const seed of args.seedArtifactRuns) {
128
+ if (existsSync(seed.dir)) {
129
+ push(seed.dir, `prior ${seed.arm} run for ${seed.iid} — worker evidence, journal, driver log`, seed.iid)
130
+ }
131
+ if (seed.patchPath && existsSync(seed.patchPath)) {
132
+ push(seed.patchPath, `prior ${seed.arm} delivered patch for ${seed.iid}`, seed.iid)
133
+ }
134
+ }
135
+
136
+ // Prior run outDirs: candidate diffs, arm runs (incl. judge.json near-miss
137
+ // details per instance), proposer shot receipts.
138
+ for (const dir of args.priorEvidenceDirs ?? []) {
139
+ if (!existsSync(dir)) continue
140
+ const candidates = join(dir, 'candidates')
141
+ if (existsSync(candidates)) push(candidates, 'prior-run candidate diffs (one .patch per surface)')
142
+ const armRuns = join(dir, 'arm-runs')
143
+ for (const tag of (await readdir(armRuns).catch(() => [])).sort()) {
144
+ for (const rep of (await readdir(join(armRuns, tag)).catch(() => [])).sort()) {
145
+ const runsRoot = join(armRuns, tag, rep, 'runs')
146
+ for (const iid of (await readdir(runsRoot).catch(() => [])).sort()) {
147
+ push(
148
+ join(runsRoot, iid),
149
+ `prior arm run ${tag} ${rep} on ${iid} — result.json, judge.json (near-miss detail), brain.jsonl, ws/.loops worker evidence`,
150
+ iid,
151
+ )
152
+ }
153
+ }
154
+ }
155
+ const shots = join(dir, 'proposer-shots')
156
+ if (existsSync(shots)) push(shots, 'prior-run proposer shot receipts (what earlier authors tried)')
157
+ }
158
+
159
+ // Pareto parent diffs.
160
+ for (const parent of args.paretoParentPatches ?? []) {
161
+ if (existsSync(parent.path)) push(parent.path, `pareto parent diff — ${parent.label}`)
162
+ }
163
+
164
+ return rows
165
+ }
166
+
167
+ /** Render + write `<outDir>/evidence-index.md`. Fails loud if a private
168
+ * instance id leaks into the rendered text (never-surfaced invariant). */
169
+ export async function writeEvidenceIndex(args: EvidenceIndexArgs): Promise<{ path: string; rows: EvidenceIndexRow[] }> {
170
+ const rows = await collectEvidenceIndexRows(args)
171
+ const lines: string[] = [
172
+ `# Evidence map (${AUTHOR_BRIEFING_VERSION})`,
173
+ '',
174
+ 'One line per evidence path. This is a MAP, not a digest — open what you need.',
175
+ ...(args.split !== null
176
+ ? [
177
+ '',
178
+ `NOTE: ${args.split.privateInstances.length} improvement instance(s) are PRIVATE this run: their`,
179
+ 'identities and per-instance evidence are withheld from authors, but your candidate is still',
180
+ 'selected on the full public+private set. Do not overfit the visible instances.',
181
+ ]
182
+ : []),
183
+ '',
184
+ ...rows.map((r) => `- ${r.path} — ${r.note}`),
185
+ '',
186
+ ]
187
+ const text = lines.join('\n')
188
+ if (args.split !== null) {
189
+ const leaks = leaksPrivateInstance(text, args.split)
190
+ if (leaks.length > 0) {
191
+ throw new Error(`evidence-index: private instance id(s) leaked into the index: ${leaks.join(', ')}`)
192
+ }
193
+ }
194
+ const path = join(args.outDir, EVIDENCE_INDEX_FILENAME)
195
+ await writeFile(path, text)
196
+ return { path, rows }
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // The prompt section (MAP + TOOLBOX + PERMISSION), appended per author.
201
+ // ---------------------------------------------------------------------------
202
+
203
+ export interface BriefingContext {
204
+ indexPath: string
205
+ briefingText: string
206
+ briefingSource: 'change-space' | 'default'
207
+ }
208
+
209
+ export function briefingPromptSection(ctx: BriefingContext): string {
210
+ return [
211
+ `EVIDENCE MAP: ${ctx.indexPath}`,
212
+ 'Read it first — one line per evidence path (staircase history, prior arm runs + judge near-miss',
213
+ 'details, worker evidence, parent diffs). Open the paths you need; nothing is pre-digested for you.',
214
+ '',
215
+ ctx.briefingText.trimEnd(),
216
+ ].join('\n')
217
+ }