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