@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.
- package/CHANGELOG.md +11 -0
- package/HARNESS.md +43 -0
- package/dist/adapters.js +24 -24
- package/dist/benchmarks/_harness.d.ts +1 -1
- package/dist/benchmarks/_harness.js +1 -1
- package/dist/benchmarks/aec-bench.js +2 -2
- package/dist/benchmarks/agentbench.js +2 -2
- package/dist/benchmarks/appworld.js +2 -2
- package/dist/benchmarks/bfcl.js +2 -2
- package/dist/benchmarks/commit0.js +2 -2
- package/dist/benchmarks/crag.js +2 -2
- package/dist/benchmarks/dabstep.js +2 -2
- package/dist/benchmarks/enterpriseops-gym.js +2 -2
- package/dist/benchmarks/finresearchbench.js +2 -2
- package/dist/benchmarks/humaneval.d.ts +10 -1
- package/dist/benchmarks/humaneval.js +5 -3
- package/dist/benchmarks/nomiracl.js +2 -2
- package/dist/benchmarks/open-rag-bench.js +2 -2
- package/dist/benchmarks/programbench.js +2 -2
- package/dist/benchmarks/ragbench.js +2 -2
- package/dist/benchmarks/swe-bench.js +2 -2
- package/dist/benchmarks/t2-ragbench.js +2 -2
- package/dist/benchmarks/tau-bench-shared.js +2 -2
- package/dist/benchmarks/tau2-bench.js +3 -3
- package/dist/benchmarks/tau3-banking.js +3 -3
- package/dist/benchmarks/terminal-bench.js +2 -2
- package/dist/benchmarks/toollm.js +2 -2
- package/dist/benchmarks/webarena-verified.js +2 -2
- package/dist/{chunk-CKUVRZ2T.js → chunk-3U5TXJZS.js} +2 -2
- package/dist/{chunk-PPYSEKFM.js → chunk-5H5XV76F.js} +73 -15
- package/dist/chunk-5H5XV76F.js.map +1 -0
- package/dist/{chunk-YCGY7UIZ.js → chunk-7GRVHU22.js} +2 -2
- package/dist/{chunk-Z7ML6L77.js → chunk-HWST3SED.js} +2 -2
- package/dist/{chunk-SYDW647C.js → chunk-IA2FBTWC.js} +2 -2
- package/dist/{chunk-R67DFVLO.js → chunk-IFVINJ4B.js} +2 -2
- package/dist/{chunk-R36V2VP7.js → chunk-IZ5M6OAC.js} +2 -2
- package/dist/{chunk-ODT47UAY.js → chunk-K3BQGZCT.js} +2 -2
- package/dist/{chunk-IFAV6KEM.js → chunk-KP5KD6EN.js} +2 -2
- package/dist/{chunk-ZEWMTR5M.js → chunk-MQMRLGOG.js} +2 -2
- package/dist/{chunk-TSWPNOYM.js → chunk-NQG5XDSB.js} +2 -2
- package/dist/{chunk-7WSD27QQ.js → chunk-PB64GYIG.js} +2 -2
- package/dist/{chunk-HBSWHQNJ.js → chunk-RCYQEFNX.js} +3 -3
- package/dist/{chunk-J3KDJNX2.js → chunk-RH5F53JT.js} +2 -2
- package/dist/{chunk-UAIOHCUK.js → chunk-SFLA7OH3.js} +3 -3
- package/dist/{chunk-Y6O2OCUO.js → chunk-SHM6MRRF.js} +2 -2
- package/dist/{chunk-KDIKRJGB.js → chunk-SHYIRB7I.js} +2 -2
- package/dist/{chunk-HHXFIHXC.js → chunk-SVR2LKYI.js} +2 -2
- package/dist/{chunk-5SBJCB6W.js → chunk-V7AEBY6U.js} +22 -22
- package/dist/{chunk-LRRD7NAG.js → chunk-WSKWVEQB.js} +18 -2
- package/dist/chunk-WSKWVEQB.js.map +1 -0
- package/dist/{chunk-2PVVP7GN.js → chunk-XKEFIFIC.js} +2 -2
- package/dist/{chunk-JRWWGMK7.js → chunk-XYA4XSNU.js} +2 -2
- package/dist/{chunk-X5YKXC6V.js → chunk-YSMEKBTD.js} +2 -2
- package/dist/{chunk-2XU6OGEN.js → chunk-Z4TZ76N7.js} +2 -2
- package/dist/index.js +24 -24
- package/package.json +6 -5
- package/scripts/run-package-tests.mjs +30 -8
- package/scripts/verify-packed-consumer.mjs +1 -1
- package/scripts/verify-pier-agent.mts +1 -0
- package/src/benchmarks/_harness.ts +20 -2
- package/src/benchmarks/humaneval.test.mts +122 -0
- package/src/benchmarks/humaneval.ts +100 -27
- package/src/david-attribution.mts +78 -0
- package/src/david-goliath.mts +149 -0
- package/src/hev-improve.mts +25 -6
- package/src/humaneval-object-ablation.mts +201 -0
- package/src/live-improve-campaign-mbpp.mts +641 -0
- package/src/live-improve-campaign.mts +500 -0
- package/src/mbpp-structural.mts +12 -7
- package/src/quant-arena/README.md +144 -0
- package/src/quant-arena/backtest.test.mts +135 -0
- package/src/quant-arena/backtest.ts +218 -0
- package/src/quant-arena/data.test.mts +44 -0
- package/src/quant-arena/data.ts +141 -0
- package/src/quant-arena/driver.test.mts +253 -0
- package/src/quant-arena/driver.ts +219 -0
- package/src/quant-arena/fixtures/data/PROVENANCE.md +26 -0
- package/src/quant-arena/fixtures/data/holdout/IDX.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S01.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S02.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S03.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S04.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S05.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S06.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S07.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S08.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S09.csv +523 -0
- package/src/quant-arena/fixtures/data/holdout/S10.csv +523 -0
- package/src/quant-arena/fixtures/data/insample/IDX.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S01.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S02.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S03.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S04.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S05.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S06.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S07.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S08.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S09.csv +2087 -0
- package/src/quant-arena/fixtures/data/insample/S10.csv +2087 -0
- package/src/quant-arena/fixtures/demo-campaign/cost-ledger.jsonl +16 -0
- package/src/quant-arena/fixtures/demo-campaign/notebook.jsonl +5 -0
- package/src/quant-arena/fixtures/demo-campaign/rollout-manifest.json +171 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-001-default-author/strategy.ts +119 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-002-default-author/strategy.ts +119 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-003-quant-researcher/strategy.ts +105 -0
- package/src/quant-arena/fixtures/demo-campaign/strategies/cand-004-quant-researcher/strategy.ts +102 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/cost-ledger.jsonl +4 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/notebook.jsonl +2 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/rollout-manifest.json +84 -0
- package/src/quant-arena/fixtures/demo-campaign-v2/strategies/cand-001-quant-researcher/strategy.ts +117 -0
- package/src/quant-arena/holdout-certify.mts +206 -0
- package/src/quant-arena/holdout-certify.test.mts +82 -0
- package/src/quant-arena/leak-audit.test.mts +79 -0
- package/src/quant-arena/leak-audit.ts +95 -0
- package/src/quant-arena/make-fixtures.mts +161 -0
- package/src/quant-arena/multiplicity.test.mts +68 -0
- package/src/quant-arena/multiplicity.ts +87 -0
- package/src/quant-arena/nautilus-certify.ts +31 -0
- package/src/quant-arena/oms.ts +90 -0
- package/src/quant-arena/profiles/quant-researcher.profile.json +7 -0
- package/src/quant-arena/python/pyproject.toml +8 -0
- package/src/quant-arena/python/uv.lock +1297 -0
- package/src/quant-arena/python/vbt-worker.py +192 -0
- package/src/quant-arena/quant-loop.mts +813 -0
- package/src/quant-arena/quant-loop.test.mts +75 -0
- package/src/quant-arena/strategies/buy-hold-index/strategy.ts +11 -0
- package/src/quant-arena/strategies/equal-weight/strategy.ts +20 -0
- package/src/quant-arena/strategies/sma-crossover/strategy.ts +42 -0
- package/src/quant-arena/types.ts +133 -0
- package/src/quant-arena/vbt-client.ts +321 -0
- package/src/quant-arena/vbt-parity.test.mts +183 -0
- package/src/quant-arena/windows.test.mts +45 -0
- package/src/quant-arena/windows.ts +54 -0
- package/src/rollout-ledger/backfill-swe-arena.mts +606 -0
- package/src/rollout-ledger/backfill-swe-arena.test.mts +338 -0
- package/src/rollout-ledger/settle-capture.mts +442 -0
- package/src/rollout-ledger/settle-capture.test.mts +270 -0
- package/src/stream-observe.py +45 -0
- package/src/stream-observe.tpl.html +247 -0
- package/src/supervisor-arena.mts +816 -0
- package/src/swe-arena/activation.mts +228 -0
- package/src/swe-arena/activation.test.mts +303 -0
- package/src/swe-arena/analyze.ts +211 -0
- package/src/swe-arena/arms.ts +804 -0
- package/src/swe-arena/bootstrap-meta.mts +188 -0
- package/src/swe-arena/bootstrap-meta.test.mts +51 -0
- package/src/swe-arena/briefing.mts +217 -0
- package/src/swe-arena/briefing.test.mts +178 -0
- package/src/swe-arena/calibrate.ts +217 -0
- package/src/swe-arena/capabilities.mts +76 -0
- package/src/swe-arena/capabilities.test.mts +57 -0
- package/src/swe-arena/capacity.ts +194 -0
- package/src/swe-arena/cell-evidence.mts +437 -0
- package/src/swe-arena/cell-evidence.test.mts +248 -0
- package/src/swe-arena/diagnosis-ensemble.test.mts +210 -0
- package/src/swe-arena/diagnosis-ensemble.ts +520 -0
- package/src/swe-arena/execution.test.mts +1170 -0
- package/src/swe-arena/factory-command-container.ts +284 -0
- package/src/swe-arena/factory-judge-child.mts +228 -0
- package/src/swe-arena/factory.test.mts +643 -0
- package/src/swe-arena/fixtures/analyze.py +80 -0
- package/src/swe-arena/fixtures/excludes.txt +8 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/calibration.md +51 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/manifest.json +29 -0
- package/src/swe-arena/fixtures/factory/agent-eval-309/spec.md +64 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/calibration.md +48 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/manifest.json +29 -0
- package/src/swe-arena/fixtures/factory/agent-runtime-232/spec.md +48 -0
- package/src/swe-arena/fixtures/factory/loops-28/calibration.md +47 -0
- package/src/swe-arena/fixtures/factory/loops-28/manifest.json +30 -0
- package/src/swe-arena/fixtures/factory/loops-28/spec.md +50 -0
- package/src/swe-arena/fixtures/gen1-salvage/README.md +45 -0
- package/src/swe-arena/fixtures/gen1-salvage/cand0-e6d7361.diff +116 -0
- package/src/swe-arena/fixtures/gen1-salvage/cand1-76a8590.diff +293 -0
- package/src/swe-arena/fixtures/holdout-preregister.log +12 -0
- package/src/swe-arena/fixtures/holdout.json +44 -0
- package/src/swe-arena/fixtures/instances.json +146 -0
- package/src/swe-arena/fixtures/ledger.jsonl +12 -0
- package/src/swe-arena/fixtures/patches/pallets__flask-5014.solo.patch +36 -0
- package/src/swe-arena/fixtures/patches/pydata__xarray-4687.sup.patch +33 -0
- package/src/swe-arena/fixtures/rejudge.jsonl +15 -0
- package/src/swe-arena/fixtures/rematch.jsonl +3 -0
- package/src/swe-arena/fixtures/rematch2.jsonl +3 -0
- package/src/swe-arena/fixtures/rematch3.jsonl +3 -0
- package/src/swe-arena/fixtures/run-report/README.md +43 -0
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.json +173 -0
- package/src/swe-arena/fixtures/run-report/factory-agent-eval-309-FSUP0.md +100 -0
- package/src/swe-arena/fixtures/run-report/gen3-rollup.json +551 -0
- package/src/swe-arena/fixtures/run-report/gen3-rollup.md +64 -0
- package/src/swe-arena/fixtures/sup-journal-true.json +19 -0
- package/src/swe-arena/fixtures/verify/astropy__astropy-13033.sh +48 -0
- package/src/swe-arena/fixtures/verify/django__django-11532.sh +50 -0
- package/src/swe-arena/fixtures/verify/matplotlib__matplotlib-20826.sh +76 -0
- package/src/swe-arena/fixtures/verify/pydata__xarray-4687.sh +44 -0
- package/src/swe-arena/fixtures/verify/pytest-dev__pytest-6197.sh +32 -0
- package/src/swe-arena/fixtures/verify/sphinx-doc__sphinx-9658.sh +51 -0
- package/src/swe-arena/fixtures/worker-tokens.json +42 -0
- package/src/swe-arena/fixtures.ts +237 -0
- package/src/swe-arena/gepa-seat.mts +583 -0
- package/src/swe-arena/gepa-seat.test.mts +635 -0
- package/src/swe-arena/holdout-certify.mts +408 -0
- package/src/swe-arena/holdout-certify.test.mts +160 -0
- package/src/swe-arena/judge-child.mts +37 -0
- package/src/swe-arena/ledger-orphans.mts +77 -0
- package/src/swe-arena/ledger-orphans.test.mts +147 -0
- package/src/swe-arena/lineage-record.mts +164 -0
- package/src/swe-arena/lineage-record.test.mts +115 -0
- package/src/swe-arena/manifest.mts +293 -0
- package/src/swe-arena/manifest.test.mts +169 -0
- package/src/swe-arena/materialize.ts +142 -0
- package/src/swe-arena/outer-loop.mts +2795 -0
- package/src/swe-arena/outer-loop.test.mts +696 -0
- package/src/swe-arena/parity.test.mts +87 -0
- package/src/swe-arena/premeasured-from-cells.mts +281 -0
- package/src/swe-arena/premeasured-from-cells.test.mts +180 -0
- package/src/swe-arena/proc.test.mts +174 -0
- package/src/swe-arena/proc.ts +260 -0
- package/src/swe-arena/profiles/default-author.profile.json +4 -0
- package/src/swe-arena/proposer-fanout.mts +770 -0
- package/src/swe-arena/proposer-fanout.test.mts +619 -0
- package/src/swe-arena/proposer-provenance.mts +177 -0
- package/src/swe-arena/proposer-provenance.test.mts +106 -0
- package/src/swe-arena/reconcile.ts +0 -0
- package/src/swe-arena/replay.mts +183 -0
- package/src/swe-arena/replay.test.mts +300 -0
- package/src/swe-arena/run-experiment.mts +727 -0
- package/src/swe-arena/run-report.mts +75 -0
- package/src/swe-arena/run-supervisor.mjs +297 -0
- package/src/swe-arena/run-supervisor.test.mts +500 -0
- package/src/swe-arena/score-split.mts +140 -0
- package/src/swe-arena/score-split.test.mts +123 -0
- package/src/swe-arena/serialized-judge.ts +414 -0
- package/src/swe-arena/types.ts +218 -0
- package/src/swe-code-improve.mts +328 -0
- package/src/swe-emit-patch.mts +104 -0
- package/src/swe-improve.mts +232 -0
- package/src/swe-jail.ts +2 -2
- package/src/swe-local-proof.mts +169 -0
- package/src/swe-repro-calibrate.mts +446 -0
- package/src/swe-stream.mts +1497 -0
- package/src/swe-structural.mts +245 -837
- package/dist/chunk-LRRD7NAG.js.map +0 -1
- package/dist/chunk-PPYSEKFM.js.map +0 -1
- /package/dist/{chunk-CKUVRZ2T.js.map → chunk-3U5TXJZS.js.map} +0 -0
- /package/dist/{chunk-YCGY7UIZ.js.map → chunk-7GRVHU22.js.map} +0 -0
- /package/dist/{chunk-Z7ML6L77.js.map → chunk-HWST3SED.js.map} +0 -0
- /package/dist/{chunk-SYDW647C.js.map → chunk-IA2FBTWC.js.map} +0 -0
- /package/dist/{chunk-R67DFVLO.js.map → chunk-IFVINJ4B.js.map} +0 -0
- /package/dist/{chunk-R36V2VP7.js.map → chunk-IZ5M6OAC.js.map} +0 -0
- /package/dist/{chunk-ODT47UAY.js.map → chunk-K3BQGZCT.js.map} +0 -0
- /package/dist/{chunk-IFAV6KEM.js.map → chunk-KP5KD6EN.js.map} +0 -0
- /package/dist/{chunk-ZEWMTR5M.js.map → chunk-MQMRLGOG.js.map} +0 -0
- /package/dist/{chunk-TSWPNOYM.js.map → chunk-NQG5XDSB.js.map} +0 -0
- /package/dist/{chunk-7WSD27QQ.js.map → chunk-PB64GYIG.js.map} +0 -0
- /package/dist/{chunk-HBSWHQNJ.js.map → chunk-RCYQEFNX.js.map} +0 -0
- /package/dist/{chunk-J3KDJNX2.js.map → chunk-RH5F53JT.js.map} +0 -0
- /package/dist/{chunk-UAIOHCUK.js.map → chunk-SFLA7OH3.js.map} +0 -0
- /package/dist/{chunk-Y6O2OCUO.js.map → chunk-SHM6MRRF.js.map} +0 -0
- /package/dist/{chunk-KDIKRJGB.js.map → chunk-SHYIRB7I.js.map} +0 -0
- /package/dist/{chunk-HHXFIHXC.js.map → chunk-SVR2LKYI.js.map} +0 -0
- /package/dist/{chunk-5SBJCB6W.js.map → chunk-V7AEBY6U.js.map} +0 -0
- /package/dist/{chunk-2PVVP7GN.js.map → chunk-XKEFIFIC.js.map} +0 -0
- /package/dist/{chunk-JRWWGMK7.js.map → chunk-XYA4XSNU.js.map} +0 -0
- /package/dist/{chunk-X5YKXC6V.js.map → chunk-YSMEKBTD.js.map} +0 -0
- /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
|
+
})
|