@tangle-network/agent-bench 0.3.6 → 0.3.8

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 (265) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/HARNESS.md +43 -0
  3. package/dist/adapters.js +24 -24
  4. package/dist/benchmarks/_harness.d.ts +1 -1
  5. package/dist/benchmarks/_harness.js +1 -1
  6. package/dist/benchmarks/aec-bench.js +2 -2
  7. package/dist/benchmarks/agentbench.js +2 -2
  8. package/dist/benchmarks/appworld.js +2 -2
  9. package/dist/benchmarks/bfcl.js +2 -2
  10. package/dist/benchmarks/commit0.js +2 -2
  11. package/dist/benchmarks/crag.js +2 -2
  12. package/dist/benchmarks/dabstep.js +2 -2
  13. package/dist/benchmarks/enterpriseops-gym.js +2 -2
  14. package/dist/benchmarks/finresearchbench.js +2 -2
  15. package/dist/benchmarks/humaneval.d.ts +10 -1
  16. package/dist/benchmarks/humaneval.js +5 -3
  17. package/dist/benchmarks/nomiracl.js +2 -2
  18. package/dist/benchmarks/open-rag-bench.js +2 -2
  19. package/dist/benchmarks/programbench.js +2 -2
  20. package/dist/benchmarks/ragbench.js +2 -2
  21. package/dist/benchmarks/swe-bench.js +2 -2
  22. package/dist/benchmarks/t2-ragbench.js +2 -2
  23. package/dist/benchmarks/tau-bench-shared.js +2 -2
  24. package/dist/benchmarks/tau2-bench.js +3 -3
  25. package/dist/benchmarks/tau3-banking.js +3 -3
  26. package/dist/benchmarks/terminal-bench.js +2 -2
  27. package/dist/benchmarks/toollm.js +2 -2
  28. package/dist/benchmarks/webarena-verified.js +2 -2
  29. package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
  30. package/dist/{chunk-PPYSEKFM.js → chunk-5H5XV76F.js} +73 -15
  31. package/dist/chunk-5H5XV76F.js.map +1 -0
  32. package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
  33. package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
  34. package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
  35. package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
  36. package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
  37. package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
  38. package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
  39. package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
  40. package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
  41. package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
  42. package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
  43. package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
  44. package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
  45. package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
  46. package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
  47. package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
  48. package/dist/{chunk-5SBJCB6W.js → chunk-V7AEBY6U.js} +22 -22
  49. package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
  50. package/dist/chunk-WSKWVEQB.js.map +1 -0
  51. package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
  52. package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
  53. package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
  54. package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
  55. package/dist/index.js +24 -24
  56. package/package.json +6 -5
  57. package/scripts/run-package-tests.mjs +30 -8
  58. package/scripts/verify-packed-consumer.mjs +1 -1
  59. package/scripts/verify-pier-agent.mts +1 -0
  60. package/src/benchmarks/_harness.ts +20 -2
  61. package/src/benchmarks/humaneval.test.mts +122 -0
  62. package/src/benchmarks/humaneval.ts +100 -27
  63. package/src/david-attribution.mts +78 -0
  64. package/src/david-goliath.mts +149 -0
  65. package/src/hev-improve.mts +25 -6
  66. package/src/humaneval-object-ablation.mts +201 -0
  67. package/src/live-improve-campaign-mbpp.mts +641 -0
  68. package/src/live-improve-campaign.mts +500 -0
  69. package/src/mbpp-structural.mts +12 -7
  70. package/src/quant-arena/README.md +144 -0
  71. package/src/quant-arena/backtest.test.mts +135 -0
  72. package/src/quant-arena/backtest.ts +218 -0
  73. package/src/quant-arena/data.test.mts +44 -0
  74. package/src/quant-arena/data.ts +141 -0
  75. package/src/quant-arena/driver.test.mts +253 -0
  76. package/src/quant-arena/driver.ts +219 -0
  77. package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
  78. package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
  79. package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
  80. package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
  81. package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
  82. package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
  83. package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
  84. package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
  85. package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
  86. package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
  87. package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
  88. package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
  89. package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
  90. package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
  91. package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
  92. package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
  93. package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
  94. package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
  95. package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
  96. package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
  97. package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
  98. package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
  99. package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
  100. package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
  101. package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
  102. package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
  103. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
  104. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
  105. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
  106. package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
  107. package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
  108. package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
  109. package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
  110. package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
  111. package/src/quant-arena/holdout-certify.mts +206 -0
  112. package/src/quant-arena/holdout-certify.test.mts +82 -0
  113. package/src/quant-arena/leak-audit.test.mts +79 -0
  114. package/src/quant-arena/leak-audit.ts +95 -0
  115. package/src/quant-arena/make-fixtures.mts +161 -0
  116. package/src/quant-arena/multiplicity.test.mts +68 -0
  117. package/src/quant-arena/multiplicity.ts +87 -0
  118. package/src/quant-arena/nautilus-certify.ts +31 -0
  119. package/src/quant-arena/oms.ts +90 -0
  120. package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
  121. package/src/quant-arena/python/pyproject.toml +8 -0
  122. package/src/quant-arena/python/uv.lock +1297 -0
  123. package/src/quant-arena/python/vbt-worker.py +192 -0
  124. package/src/quant-arena/quant-loop.mts +813 -0
  125. package/src/quant-arena/quant-loop.test.mts +75 -0
  126. package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
  127. package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
  128. package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
  129. package/src/quant-arena/types.ts +133 -0
  130. package/src/quant-arena/vbt-client.ts +321 -0
  131. package/src/quant-arena/vbt-parity.test.mts +183 -0
  132. package/src/quant-arena/windows.test.mts +45 -0
  133. package/src/quant-arena/windows.ts +54 -0
  134. package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
  135. package/src/rollout-ledger/backfill-swe-arena.test.mts +338 -0
  136. package/src/rollout-ledger/settle-capture.mts +442 -0
  137. package/src/rollout-ledger/settle-capture.test.mts +270 -0
  138. package/src/stream-observe.py +45 -0
  139. package/src/stream-observe.tpl.html +247 -0
  140. package/src/supervisor-arena.mts +816 -0
  141. package/src/swe-arena/activation.mts +228 -0
  142. package/src/swe-arena/activation.test.mts +303 -0
  143. package/src/swe-arena/analyze.ts +211 -0
  144. package/src/swe-arena/arms.ts +804 -0
  145. package/src/swe-arena/bootstrap-meta.mts +188 -0
  146. package/src/swe-arena/bootstrap-meta.test.mts +51 -0
  147. package/src/swe-arena/briefing.mts +217 -0
  148. package/src/swe-arena/briefing.test.mts +178 -0
  149. package/src/swe-arena/calibrate.ts +217 -0
  150. package/src/swe-arena/capabilities.mts +76 -0
  151. package/src/swe-arena/capabilities.test.mts +57 -0
  152. package/src/swe-arena/capacity.ts +194 -0
  153. package/src/swe-arena/cell-evidence.mts +437 -0
  154. package/src/swe-arena/cell-evidence.test.mts +248 -0
  155. package/src/swe-arena/diagnosis-ensemble.test.mts +210 -0
  156. package/src/swe-arena/diagnosis-ensemble.ts +520 -0
  157. package/src/swe-arena/execution.test.mts +1170 -0
  158. package/src/swe-arena/factory-command-container.ts +284 -0
  159. package/src/swe-arena/factory-judge-child.mts +228 -0
  160. package/src/swe-arena/factory.test.mts +643 -0
  161. package/src/swe-arena/fixtures/analyze.py +80 -0
  162. package/src/swe-arena/fixtures/excludes.txt +8 -0
  163. package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
  164. package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
  165. package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
  166. package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
  167. package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
  168. package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
  169. package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
  170. package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
  171. package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
  172. package/src/swe-arena/fixtures/gen1-salvage/README.md +45 -0
  173. package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +116 -0
  174. package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +293 -0
  175. package/src/swe-arena/fixtures/holdout-preregister.log +12 -0
  176. package/src/swe-arena/fixtures/holdout.json +44 -0
  177. package/src/swe-arena/fixtures/instances.json +146 -0
  178. package/src/swe-arena/fixtures/ledger.jsonl +12 -0
  179. package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +36 -0
  180. package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +33 -0
  181. package/src/swe-arena/fixtures/rejudge.jsonl +15 -0
  182. package/src/swe-arena/fixtures/rematch.jsonl +3 -0
  183. package/src/swe-arena/fixtures/rematch2.jsonl +3 -0
  184. package/src/swe-arena/fixtures/rematch3.jsonl +3 -0
  185. package/src/swe-arena/fixtures/run-report/README.md +43 -0
  186. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
  187. package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
  188. package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
  189. package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
  190. package/src/swe-arena/fixtures/sup-journal-true.json +19 -0
  191. package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +48 -0
  192. package/src/swe-arena/fixtures/verify/django__django-11532.sh +50 -0
  193. package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +76 -0
  194. package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +44 -0
  195. package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +32 -0
  196. package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +51 -0
  197. package/src/swe-arena/fixtures/worker-tokens.json +42 -0
  198. package/src/swe-arena/fixtures.ts +237 -0
  199. package/src/swe-arena/gepa-seat.mts +583 -0
  200. package/src/swe-arena/gepa-seat.test.mts +635 -0
  201. package/src/swe-arena/holdout-certify.mts +408 -0
  202. package/src/swe-arena/holdout-certify.test.mts +160 -0
  203. package/src/swe-arena/judge-child.mts +37 -0
  204. package/src/swe-arena/ledger-orphans.mts +77 -0
  205. package/src/swe-arena/ledger-orphans.test.mts +147 -0
  206. package/src/swe-arena/lineage-record.mts +164 -0
  207. package/src/swe-arena/lineage-record.test.mts +115 -0
  208. package/src/swe-arena/manifest.mts +293 -0
  209. package/src/swe-arena/manifest.test.mts +169 -0
  210. package/src/swe-arena/materialize.ts +142 -0
  211. package/src/swe-arena/outer-loop.mts +2795 -0
  212. package/src/swe-arena/outer-loop.test.mts +696 -0
  213. package/src/swe-arena/parity.test.mts +87 -0
  214. package/src/swe-arena/premeasured-from-cells.mts +281 -0
  215. package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
  216. package/src/swe-arena/proc.test.mts +174 -0
  217. package/src/swe-arena/proc.ts +260 -0
  218. package/src/swe-arena/profiles/default-author.profile.json +4 -0
  219. package/src/swe-arena/proposer-fanout.mts +770 -0
  220. package/src/swe-arena/proposer-fanout.test.mts +619 -0
  221. package/src/swe-arena/proposer-provenance.mts +177 -0
  222. package/src/swe-arena/proposer-provenance.test.mts +106 -0
  223. package/src/swe-arena/reconcile.ts +0 -0
  224. package/src/swe-arena/replay.mts +183 -0
  225. package/src/swe-arena/replay.test.mts +300 -0
  226. package/src/swe-arena/run-experiment.mts +727 -0
  227. package/src/swe-arena/run-report.mts +75 -0
  228. package/src/swe-arena/run-supervisor.mjs +297 -0
  229. package/src/swe-arena/run-supervisor.test.mts +500 -0
  230. package/src/swe-arena/score-split.mts +140 -0
  231. package/src/swe-arena/score-split.test.mts +123 -0
  232. package/src/swe-arena/serialized-judge.ts +414 -0
  233. package/src/swe-arena/types.ts +218 -0
  234. package/src/swe-code-improve.mts +328 -0
  235. package/src/swe-emit-patch.mts +104 -0
  236. package/src/swe-improve.mts +232 -0
  237. package/src/swe-jail.ts +2 -2
  238. package/src/swe-local-proof.mts +169 -0
  239. package/src/swe-repro-calibrate.mts +446 -0
  240. package/src/swe-stream.mts +1497 -0
  241. package/src/swe-structural.mts +245 -837
  242. package/dist/chunk-LRRD7NAG.js.map +0 -1
  243. package/dist/chunk-PPYSEKFM.js.map +0 -1
  244. /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
  245. /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
  246. /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
  247. /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
  248. /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
  249. /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
  250. /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
  251. /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
  252. /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
  253. /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
  254. /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
  255. /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
  256. /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
  257. /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
  258. /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
  259. /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
  260. /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
  261. /package/dist/{chunk-5SBJCB6W.js.map → chunk-V7AEBY6U.js.map} +0 -0
  262. /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
  263. /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
  264. /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
  265. /package/dist/{chunk-2XU6OGEN.js.map → chunk-Z4TZ76N7.js.map} +0 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Gen-5 activation gate (SOTA adoption #2, GSME-style "verify the mechanism
3
+ * fired before the score counts").
4
+ *
5
+ * Every proposer's deliverable must include a MACHINE-CHECKABLE ACTIVATION
6
+ * PREDICATE at `.improve/activation.json` (inside the change-space's metadata
7
+ * prefix, so the improvement driver's finalize commits it with the candidate):
8
+ * a grep pattern or script over the candidate's OWN campaign run artifacts
9
+ * that proves its mechanism actually fired (e.g. "the new prompt section
10
+ * rendered in worker prompts", "patchRiskWarnings emitted in >=1 settle").
11
+ *
12
+ * Enforcement is two-stage, both fail-closed:
13
+ * 1. PREFILTER — a candidate without a parseable predicate is killed before
14
+ * any evaluation spend (proposer-fanout.mts, stage 'activation-predicate').
15
+ * 2. POST-EVAL — the evaluator runs the predicate over the candidate's own
16
+ * cell run dirs; a candidate whose mechanism NEVER fired is QUARANTINED
17
+ * (staircase verdict 'quarantined-inactive': recorded, never promoted)
18
+ * even when its score improved — a score with an inactive mechanism is
19
+ * indistinguishable from luck or from gaming the visible instances.
20
+ */
21
+
22
+ import { readFile, readdir, stat } from 'node:fs/promises'
23
+ import { join, relative } from 'node:path'
24
+ import { run } from './proc.ts'
25
+
26
+ export const ACTIVATION_PREDICATE_RELPATH = '.improve/activation.json'
27
+
28
+ export interface ActivationPredicate {
29
+ version: 'v1'
30
+ /** One sentence: which mechanism this proves fired. */
31
+ description: string
32
+ kind: 'grep' | 'script'
33
+ /** kind 'grep': JS RegExp source tested line-by-line over run artifacts. */
34
+ pattern?: string
35
+ /** kind 'grep': optional relative-path substring filters (a file is searched
36
+ * when its run-dir-relative path contains ANY entry). Empty/absent = all. */
37
+ files?: string[]
38
+ /** kind 'script': bash script; run once per run dir with cwd=<runDir> and
39
+ * $RUN_DIR set; exit 0 in ANY run dir = mechanism fired. */
40
+ script?: string
41
+ }
42
+
43
+ export type ParsedPredicate = { ok: true; predicate: ActivationPredicate } | { ok: false; error: string }
44
+
45
+ export function parseActivationPredicate(raw: string): ParsedPredicate {
46
+ let value: unknown
47
+ try {
48
+ value = JSON.parse(raw)
49
+ } catch (cause) {
50
+ return { ok: false, error: `not valid JSON: ${(cause as Error).message}` }
51
+ }
52
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
53
+ return { ok: false, error: 'must be a JSON object' }
54
+ }
55
+ const p = value as Record<string, unknown>
56
+ if (p.version !== 'v1') return { ok: false, error: `version must be "v1", got ${JSON.stringify(p.version)}` }
57
+ if (typeof p.description !== 'string' || p.description.trim().length === 0) {
58
+ return { ok: false, error: 'description must be a non-empty string' }
59
+ }
60
+ if (p.kind === 'grep') {
61
+ if (typeof p.pattern !== 'string' || p.pattern.length === 0) {
62
+ return { ok: false, error: 'kind "grep" requires a non-empty pattern' }
63
+ }
64
+ try {
65
+ new RegExp(p.pattern)
66
+ } catch (cause) {
67
+ return { ok: false, error: `pattern is not a valid RegExp: ${(cause as Error).message}` }
68
+ }
69
+ if (p.files !== undefined && (!Array.isArray(p.files) || p.files.some((f) => typeof f !== 'string'))) {
70
+ return { ok: false, error: 'files must be an array of strings when present' }
71
+ }
72
+ } else if (p.kind === 'script') {
73
+ if (typeof p.script !== 'string' || p.script.trim().length === 0) {
74
+ return { ok: false, error: 'kind "script" requires a non-empty script' }
75
+ }
76
+ } else {
77
+ return { ok: false, error: `kind must be "grep" or "script", got ${JSON.stringify(p.kind)}` }
78
+ }
79
+ return { ok: true, predicate: p as unknown as ActivationPredicate }
80
+ }
81
+
82
+ /** The prompt-visible contract: template + worked example. */
83
+ export function activationPredicateInstruction(): string {
84
+ return [
85
+ 'ACTIVATION PREDICATE (required deliverable — a candidate without one is rejected before evaluation):',
86
+ `Write ${ACTIVATION_PREDICATE_RELPATH} in this worktree: a machine-checkable proof that YOUR mechanism`,
87
+ "actually fired during evaluation. The evaluator runs it over your candidate's own run artifacts",
88
+ '(each arm run dir: driver.log, brain.jsonl, result.json, ws/.loops/** journal + worker evidence);',
89
+ 'if it never fires, your candidate is QUARANTINED even when its score improved.',
90
+ '',
91
+ 'Template (kind "grep" — a RegExp tested over the run artifacts):',
92
+ ' {',
93
+ ' "version": "v1",',
94
+ ' "description": "patchRiskWarnings emitted in at least one settle",',
95
+ ' "kind": "grep",',
96
+ ' "pattern": "patchRiskWarnings|patch-risk",',
97
+ ' "files": ["journal.jsonl", "workers/"]',
98
+ ' }',
99
+ 'Or kind "script": {"version":"v1","description":"...","kind":"script","script":"grep -rq NEW_SECTION ws/.loops"}',
100
+ '(exit 0 in any run dir = fired).',
101
+ 'Pick a pattern that can ONLY appear when your mechanism ran — not one that matches the diff itself.',
102
+ ].join('\n')
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Predicate execution over run dirs.
107
+ // ---------------------------------------------------------------------------
108
+
109
+ export interface ActivationResult {
110
+ fired: boolean
111
+ /** Bounded evidence: matching `path:line` refs (grep) or script stdout tails. */
112
+ evidence: string[]
113
+ checkedRunDirs: number
114
+ checkedFiles: number
115
+ /** Bounded notes on skipped inputs (oversized files, walk caps). */
116
+ warnings: string[]
117
+ }
118
+
119
+ const MAX_FILES_PER_RUN_DIR = 4000
120
+ const MAX_FILE_BYTES = 32 * 1024 * 1024
121
+ const MAX_EVIDENCE = 10
122
+ const SCRIPT_TIMEOUT_MS = 120_000
123
+
124
+ /** Walk one run dir. The `ws/` workspace subtree (a whole checked-out repo) is
125
+ * skipped EXCEPT `ws/.loops/**` — the supervisor's own artifacts live there
126
+ * and are exactly where mechanism traces land. */
127
+ async function walkRunDir(runDir: string, warnings: string[]): Promise<string[]> {
128
+ const files: string[] = []
129
+ const queue: string[] = [runDir]
130
+ while (queue.length > 0) {
131
+ const dir = queue.shift()!
132
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
133
+ for (const entry of entries) {
134
+ const abs = join(dir, entry.name)
135
+ const rel = relative(runDir, abs)
136
+ if (entry.isDirectory()) {
137
+ if (rel === 'ws') {
138
+ queue.push(join(abs, '.loops'))
139
+ continue
140
+ }
141
+ queue.push(abs)
142
+ } else if (entry.isFile()) {
143
+ files.push(abs)
144
+ if (files.length >= MAX_FILES_PER_RUN_DIR) {
145
+ warnings.push(`${runDir}: file walk capped at ${MAX_FILES_PER_RUN_DIR} files`)
146
+ return files
147
+ }
148
+ }
149
+ }
150
+ }
151
+ return files
152
+ }
153
+
154
+ /** Run the predicate over the candidate's run dirs. Fail-closed: an unreadable
155
+ * artifact contributes nothing (with a warning) — it can never count as
156
+ * "fired". */
157
+ export async function runActivationPredicate(
158
+ predicate: ActivationPredicate,
159
+ runDirs: readonly string[],
160
+ ): Promise<ActivationResult> {
161
+ const result: ActivationResult = { fired: false, evidence: [], checkedRunDirs: 0, checkedFiles: 0, warnings: [] }
162
+ for (const runDir of runDirs) {
163
+ result.checkedRunDirs += 1
164
+ if (predicate.kind === 'script') {
165
+ const res = await run('bash', ['-c', predicate.script!], {
166
+ cwd: runDir,
167
+ timeoutMs: SCRIPT_TIMEOUT_MS,
168
+ env: { ...process.env, RUN_DIR: runDir },
169
+ })
170
+ if (res.code === 0) {
171
+ result.fired = true
172
+ if (result.evidence.length < MAX_EVIDENCE) {
173
+ result.evidence.push(`${runDir}: script rc=0${res.stdout.trim() ? ` — ${res.stdout.trim().slice(0, 200)}` : ''}`)
174
+ }
175
+ } else if (res.timedOut) {
176
+ result.warnings.push(`${runDir}: script timed out after ${SCRIPT_TIMEOUT_MS}ms (counted as not-fired)`)
177
+ }
178
+ continue
179
+ }
180
+ const regex = new RegExp(predicate.pattern!)
181
+ const filters = (predicate.files ?? []).filter((f) => f.length > 0)
182
+ for (const file of await walkRunDir(runDir, result.warnings)) {
183
+ const rel = relative(runDir, file)
184
+ if (filters.length > 0 && !filters.some((f) => rel.includes(f))) continue
185
+ const info = await stat(file).catch(() => null)
186
+ if (info === null) continue
187
+ if (info.size > MAX_FILE_BYTES) {
188
+ result.warnings.push(`${rel}: skipped (${info.size} bytes > ${MAX_FILE_BYTES})`)
189
+ continue
190
+ }
191
+ result.checkedFiles += 1
192
+ const content = await readFile(file, 'utf8').catch(() => null)
193
+ if (content === null) continue
194
+ const lines = content.split('\n')
195
+ for (let i = 0; i < lines.length; i++) {
196
+ if (regex.test(lines[i]!)) {
197
+ result.fired = true
198
+ if (result.evidence.length < MAX_EVIDENCE) {
199
+ result.evidence.push(`${file}:${i + 1}: ${lines[i]!.trim().slice(0, 200)}`)
200
+ }
201
+ break // one match per file is enough evidence
202
+ }
203
+ }
204
+ }
205
+ }
206
+ return result
207
+ }
208
+
209
+ /** Read + parse the predicate committed at a candidate's loops commit. */
210
+ export async function readCommittedPredicate(
211
+ loopsRepo: string,
212
+ commit: string,
213
+ ): Promise<{ raw: string; parsed: ParsedPredicate } | null> {
214
+ const show = await run('git', ['-C', loopsRepo, 'show', `${commit}:${ACTIVATION_PREDICATE_RELPATH}`])
215
+ if (show.code !== 0) return null
216
+ return { raw: show.stdout, parsed: parseActivationPredicate(show.stdout) }
217
+ }
218
+
219
+ /** The staircase row's activation record. */
220
+ export interface ActivationRecord {
221
+ /** Whether a parseable predicate was present on the candidate commit. */
222
+ present: boolean
223
+ description: string | null
224
+ /** null = not evaluated (no predicate / gate disabled / baseline). */
225
+ fired: boolean | null
226
+ evidence: string[]
227
+ warnings: string[]
228
+ }
@@ -0,0 +1,303 @@
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
+ version: 'v1',
30
+ description: 'patchRiskWarnings emitted in at least one settle',
31
+ kind: 'grep',
32
+ pattern: 'patchRiskWarnings|patch-risk',
33
+ files: ['journal.jsonl', 'workers/'],
34
+ }),
35
+ )
36
+ expect(parsed.ok).toBe(true)
37
+ })
38
+
39
+ it('accepts a valid script predicate', () => {
40
+ const parsed = parseActivationPredicate(
41
+ JSON.stringify({ version: 'v1', description: 'x', kind: 'script', script: 'grep -rq X .' }),
42
+ )
43
+ expect(parsed.ok).toBe(true)
44
+ })
45
+
46
+ it.each([
47
+ ['not json', 'not valid JSON'],
48
+ ['[]', 'JSON object'],
49
+ [JSON.stringify({ version: 'v2', description: 'x', kind: 'grep', pattern: 'a' }), 'version'],
50
+ [JSON.stringify({ version: 'v1', description: '', kind: 'grep', pattern: 'a' }), 'description'],
51
+ [JSON.stringify({ version: 'v1', description: 'x', kind: 'grep' }), 'pattern'],
52
+ [JSON.stringify({ version: 'v1', description: 'x', kind: 'grep', pattern: '(' }), 'RegExp'],
53
+ [JSON.stringify({ version: 'v1', description: 'x', kind: 'grep', pattern: 'a', files: [1] }), 'files'],
54
+ [JSON.stringify({ version: 'v1', description: 'x', kind: 'script' }), 'script'],
55
+ [JSON.stringify({ version: 'v1', description: 'x', kind: 'sql' }), 'kind'],
56
+ ])('rejects %s', (raw, want) => {
57
+ const parsed = parseActivationPredicate(raw)
58
+ expect(parsed.ok).toBe(false)
59
+ if (!parsed.ok) expect(parsed.error).toContain(want)
60
+ })
61
+
62
+ it('the prompt instruction carries the template and the quarantine warning', () => {
63
+ const text = activationPredicateInstruction()
64
+ expect(text).toContain(ACTIVATION_PREDICATE_RELPATH)
65
+ expect(text).toContain('QUARANTINED')
66
+ expect(text).toContain('"kind": "grep"')
67
+ })
68
+ })
69
+
70
+ describe('runActivationPredicate', () => {
71
+ let runA: string
72
+ let runB: string
73
+
74
+ beforeEach(async () => {
75
+ runA = await mkdtemp(join(tmpdir(), 'act-a-'))
76
+ runB = await mkdtemp(join(tmpdir(), 'act-b-'))
77
+ await writeFile(join(runA, 'driver.log'), 'boot\nno mechanism here\n')
78
+ await writeFile(join(runB, 'driver.log'), 'boot\npatchRiskWarnings: 2 warnings emitted\n')
79
+ })
80
+
81
+ afterEach(async () => {
82
+ await rm(runA, { recursive: true, force: true })
83
+ await rm(runB, { recursive: true, force: true })
84
+ })
85
+
86
+ const grep = (pattern: string, files?: string[]) => ({
87
+ version: 'v1' as const,
88
+ description: 'd',
89
+ kind: 'grep' as const,
90
+ pattern,
91
+ ...(files ? { files } : {}),
92
+ })
93
+
94
+ it('fires with path:line evidence when the pattern appears in ANY run dir', async () => {
95
+ const res = await runActivationPredicate(grep('patchRiskWarnings'), [runA, runB])
96
+ expect(res.fired).toBe(true)
97
+ expect(res.checkedRunDirs).toBe(2)
98
+ expect(res.evidence[0]).toContain(join(runB, 'driver.log'))
99
+ expect(res.evidence[0]).toContain('patchRiskWarnings')
100
+ })
101
+
102
+ it('does not fire when the pattern never appears (fail-closed)', async () => {
103
+ const res = await runActivationPredicate(grep('neverEverPresent'), [runA, runB])
104
+ expect(res.fired).toBe(false)
105
+ expect(res.evidence).toEqual([])
106
+ })
107
+
108
+ it('honors files filters (relative-path substring)', async () => {
109
+ await writeFile(join(runB, 'other.txt'), 'patchRiskWarnings\n')
110
+ const onlyOther = await runActivationPredicate(grep('patchRiskWarnings', ['other.txt']), [runB])
111
+ expect(onlyOther.fired).toBe(true)
112
+ const onlyMissing = await runActivationPredicate(grep('patchRiskWarnings', ['nope.bin']), [runB])
113
+ expect(onlyMissing.fired).toBe(false)
114
+ })
115
+
116
+ it('skips the ws/ workspace subtree EXCEPT ws/.loops (supervisor artifacts)', async () => {
117
+ // Marker only inside ws/ (a checked-out repo) → must NOT count as fired.
118
+ await mkdir(join(runA, 'ws', 'src'), { recursive: true })
119
+ await writeFile(join(runA, 'ws', 'src', 'code.py'), 'patchRiskWarnings in the repo source\n')
120
+ const inWs = await runActivationPredicate(grep('patchRiskWarnings'), [runA])
121
+ expect(inWs.fired).toBe(false)
122
+ // Marker in ws/.loops → the supervisor's own artifacts, searched.
123
+ await mkdir(join(runA, 'ws', '.loops', 'supervisor', 's1'), { recursive: true })
124
+ await writeFile(join(runA, 'ws', '.loops', 'supervisor', 's1', 'journal.jsonl'), '{"k":"patchRiskWarnings"}\n')
125
+ const inLoops = await runActivationPredicate(grep('patchRiskWarnings'), [runA])
126
+ expect(inLoops.fired).toBe(true)
127
+ })
128
+
129
+ it('script kind fires on rc=0 in any run dir, not-fired otherwise', async () => {
130
+ const script = (s: string) => ({ version: 'v1' as const, description: 'd', kind: 'script' as const, script: s })
131
+ const hit = await runActivationPredicate(script('grep -q patchRiskWarnings driver.log'), [runA, runB])
132
+ expect(hit.fired).toBe(true)
133
+ expect(hit.evidence[0]).toContain(runB)
134
+ const miss = await runActivationPredicate(script('grep -q neverEverPresent driver.log'), [runA, runB])
135
+ expect(miss.fired).toBe(false)
136
+ })
137
+ })
138
+
139
+ describe('quarantine verdict path', () => {
140
+ const base = {
141
+ violations: [],
142
+ coverageComplete: true,
143
+ resolvedCount: 3,
144
+ parentResolvedCount: 1,
145
+ costRatio: 1.0,
146
+ costGuardRatio: 1.2,
147
+ }
148
+
149
+ it('quarantines a candidate whose mechanism never fired EVEN when its score improved', () => {
150
+ expect(decideVerdict({ ...base, activationFired: false })).toBe('quarantined-inactive')
151
+ })
152
+
153
+ it('accepts an improved candidate whose mechanism fired; gate-not-applicable behaves as before', () => {
154
+ expect(decideVerdict({ ...base, activationFired: true })).toBe('accepted')
155
+ expect(decideVerdict({ ...base, activationFired: null })).toBe('accepted')
156
+ expect(decideVerdict(base)).toBe('accepted')
157
+ })
158
+
159
+ it('out-of-space still wins over quarantine; quarantine wins over no-gain/cost/coverage', () => {
160
+ expect(decideVerdict({ ...base, violations: ['judge.py'], activationFired: false })).toBe('rejected-out-of-space')
161
+ expect(decideVerdict({ ...base, coverageComplete: false, activationFired: false })).toBe('quarantined-inactive')
162
+ expect(decideVerdict({ ...base, resolvedCount: 1, activationFired: false })).toBe('quarantined-inactive')
163
+ })
164
+
165
+ it('parseStaircaseRow accepts a quarantined row with gen-5 split + activation fields', () => {
166
+ const row: StaircaseRow = {
167
+ schema: STAIRCASE_SCHEMA,
168
+ round: 4,
169
+ generation: 0,
170
+ runId: 'r4-x',
171
+ at: new Date(0).toISOString(),
172
+ candidate: 'hash',
173
+ candidateCommit: 'c'.repeat(40),
174
+ parent: 'baseline',
175
+ parentResolvedCount: 1,
176
+ changedFiles: [],
177
+ changeSpaceViolations: [],
178
+ perInstance: [],
179
+ resolvedCount: 3,
180
+ coverageComplete: true,
181
+ wallS: 10,
182
+ baselineWallS: 10,
183
+ costRatio: 1,
184
+ costGuardRatio: 1.2,
185
+ internallyPromoted: true,
186
+ verdict: 'quarantined-inactive',
187
+ holdout: 'operator-approval-required',
188
+ armProvenance: null,
189
+ diffPath: null,
190
+ diffSha256: null,
191
+ split: {
192
+ publicInstances: ['a', 'b'],
193
+ privateInstances: ['c'],
194
+ publicResolvedCount: 2,
195
+ privateResolvedCount: 1,
196
+ },
197
+ activation: { present: true, description: 'd', fired: false, evidence: [], warnings: [] },
198
+ }
199
+ expect(parseStaircaseRow(JSON.stringify(row))).toEqual(row)
200
+ })
201
+ })
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Prefilter enforcement — a candidate without a parseable predicate is killed
205
+ // before any evaluation spend.
206
+ // ---------------------------------------------------------------------------
207
+
208
+ describe('activation-predicate prefilter', () => {
209
+ let loopsRepo: string
210
+ let outDir: string
211
+ let driverWt: string
212
+
213
+ beforeEach(async () => {
214
+ loopsRepo = await mkdtemp(join(tmpdir(), 'act-repo-'))
215
+ outDir = await mkdtemp(join(tmpdir(), 'act-out-'))
216
+ await runOk('git', ['init', '-q', '-b', 'main', loopsRepo])
217
+ await runOk('git', ['-C', loopsRepo, 'config', 'user.email', 't@t.dev'])
218
+ await runOk('git', ['-C', loopsRepo, 'config', 'user.name', 'T'])
219
+ await writeFile(join(loopsRepo, 'src.ts'), 'base\n')
220
+ await runOk('git', ['-C', loopsRepo, 'add', '-A'])
221
+ await runOk('git', ['-C', loopsRepo, 'commit', '-q', '-m', 'init'])
222
+ driverWt = join(outDir, 'driver-wt')
223
+ await runOk('git', ['-C', loopsRepo, 'worktree', 'add', '--detach', driverWt, 'HEAD'])
224
+ })
225
+
226
+ afterEach(async () => {
227
+ await rm(outDir, { recursive: true, force: true })
228
+ await rm(loopsRepo, { recursive: true, force: true })
229
+ })
230
+
231
+ const config = (proposers: ProposerSpec[]): OuterLoopConfig => ({
232
+ ...defaultRound4Config(),
233
+ loopsRepo,
234
+ outDir,
235
+ populationSize: proposers.length,
236
+ proposers,
237
+ activationGate: true,
238
+ })
239
+
240
+ const generatorArgs = (candidateIndex: number) => ({
241
+ worktreePath: driverWt,
242
+ report: undefined,
243
+ findings: [] as AnalystFinding[],
244
+ maxShots: 1,
245
+ signal: new AbortController().signal,
246
+ generation: 0,
247
+ candidateIndex,
248
+ })
249
+
250
+ it('kills a candidate without .improve/activation.json (stage activation-predicate) and passes one WITH it', async () => {
251
+ const proposers: ProposerSpec[] = [
252
+ { name: 'with-predicate', harness: 'claude' },
253
+ { name: 'without-predicate', harness: 'claude' },
254
+ { name: 'invalid-predicate', harness: 'claude' },
255
+ ]
256
+ const gen = fanOutLoopsGenerator(config(proposers), {
257
+ author: async (proposer, args) => {
258
+ await mkdir(join(args.worktreePath, 'extensions', 'pi'), { recursive: true })
259
+ await writeFile(join(args.worktreePath, 'extensions', 'pi', `${proposer.name}.ts`), 'x\n')
260
+ if (proposer.name === 'with-predicate') {
261
+ await mkdir(join(args.worktreePath, '.improve'), { recursive: true })
262
+ await writeFile(
263
+ join(args.worktreePath, ACTIVATION_PREDICATE_RELPATH),
264
+ JSON.stringify({ version: 'v1', description: 'd', kind: 'grep', pattern: 'x' }),
265
+ )
266
+ } else if (proposer.name === 'invalid-predicate') {
267
+ await mkdir(join(args.worktreePath, '.improve'), { recursive: true })
268
+ await writeFile(join(args.worktreePath, ACTIVATION_PREDICATE_RELPATH), '{"version":"v1"}')
269
+ }
270
+ return { applied: true, summary: `${proposer.name} edit` }
271
+ },
272
+ })
273
+
274
+ const survivor = await gen.generate(generatorArgs(0))
275
+ const missing = await gen.generate(generatorArgs(1))
276
+ const invalid = await gen.generate(generatorArgs(2))
277
+
278
+ expect(survivor.applied).toBe(true)
279
+ expect(missing.applied).toBe(false)
280
+ expect(invalid.applied).toBe(false)
281
+ const kills = gen.drainPrefilterKills()
282
+ expect(kills).toHaveLength(2)
283
+ expect(kills.find((k) => k.proposer === 'without-predicate')).toMatchObject({ stage: 'activation-predicate' })
284
+ expect(kills.find((k) => k.proposer === 'without-predicate')!.reason).toContain('missing')
285
+ expect(kills.find((k) => k.proposer === 'invalid-predicate')!.reason).toContain('invalid')
286
+ // The survivor's predicate landed on the driver worktree with the patch.
287
+ expect(existsSync(join(driverWt, ACTIVATION_PREDICATE_RELPATH))).toBe(true)
288
+ })
289
+
290
+ it('does not require a predicate when the gate is off (gen-4 behavior unchanged)', async () => {
291
+ const cfg = config([{ name: 'legacy', harness: 'claude' }])
292
+ cfg.activationGate = false
293
+ const gen = fanOutLoopsGenerator(cfg, {
294
+ author: async (_p, args) => {
295
+ await mkdir(join(args.worktreePath, 'extensions', 'pi'), { recursive: true })
296
+ await writeFile(join(args.worktreePath, 'extensions', 'pi', 'l.ts'), 'x\n')
297
+ return { applied: true, summary: 'edit' }
298
+ },
299
+ })
300
+ expect((await gen.generate(generatorArgs(0))).applied).toBe(true)
301
+ expect(gen.drainPrefilterKills()).toEqual([])
302
+ })
303
+ })