@tachikomagundam/abathur 0.2.3 → 0.2.5
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/config/genomes/historian.example.jsonc +9 -4
- package/dist/bench/adapter.js +22 -4
- package/dist/bench/fixture.js +8 -4
- package/dist/bench/toy.js +3 -3
- package/dist/commands/status.js +6 -1
- package/dist/core/evolve/run-bench.js +6 -4
- package/dist/core/evolve/run-loop.js +10 -4
- package/dist/core/evolve/run-plan.js +7 -3
- package/dist/core/spec.js +5 -1
- package/dist/core/stats.js +33 -15
- package/dist/test/bench-adapter.test.js +24 -0
- package/dist/test/bench-reporoot.test.js +197 -0
- package/dist/test/historian-grader-integrity.test.js +1423 -0
- package/dist/test/historian-grader-io.test.js +281 -1
- package/dist/test/historian-run-scenario.test.js +245 -0
- package/dist/test/selfmode-literal.test.js +153 -0
- package/dist/test/stats.test.js +18 -0
- package/dist/test/status-repo.test.js +72 -0
- package/graders/historian/grader-core.d.mts +121 -0
- package/graders/historian/grader-core.mjs +966 -5
- package/graders/historian/grader-support.d.mts +11 -0
- package/graders/historian/grader-support.mjs +28 -0
- package/graders/historian/grader.mjs +81 -5
- package/graders/historian/judge-poststage.mjs +200 -0
- package/graders/historian/mutate.sh +65 -22
- package/graders/historian/run-scenario-17.sh +34 -0
- package/graders/historian/run-scenario.sh +46 -3
- package/graders/historian/seed-wrapped.sh +1 -1
- package/package.json +1 -1
- package/plugin/abathur.ts +1 -1
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
//
|
|
9
9
|
// ${ABATHUR_HISTORIAN_REPO} absolute path of the historian bench repo
|
|
10
10
|
// (contains scenarios/, rubric.md, seed_sandbox.sh)
|
|
11
|
+
// — repoPath ONLY: the S4 engine seam delivers the
|
|
12
|
+
// bench's ACTIVE TREE (incumbent repoPath /
|
|
13
|
+
// candidate worktree) to the commands below as the
|
|
14
|
+
// {repoRoot} template var, so unit/seed paths must
|
|
15
|
+
// NOT bake a materialized repo literal here.
|
|
11
16
|
// ${ABATHUR_REPO} absolute path of THIS Abathur checkout
|
|
12
17
|
// (hosts graders/historian/*)
|
|
13
18
|
// ${ABATHUR_WIKI_BASE} canonical wiki root, e.g. http://localhost:3000
|
|
@@ -47,11 +52,11 @@
|
|
|
47
52
|
{ "id": "scenario-08", "path": "scenarios/08-current-state-g5.md", "split": "train" },
|
|
48
53
|
{ "id": "scenario-09", "path": "scenarios/09-timeline-week-groups.md", "split": "val" }
|
|
49
54
|
],
|
|
50
|
-
"seedCommand": "bash ${ABATHUR_REPO}/graders/historian/seed-wrapped.sh
|
|
55
|
+
"seedCommand": "bash ${ABATHUR_REPO}/graders/historian/seed-wrapped.sh {repoRoot}/seed_sandbox.sh",
|
|
51
56
|
"resetCommand": "bash ${ABATHUR_REPO}/graders/historian/reset-sandbox.sh",
|
|
52
|
-
"runCommand": "bash ${ABATHUR_REPO}/graders/historian/run-scenario.sh {unit.id}
|
|
53
|
-
"graderCommand": "node ${ABATHUR_REPO}/graders/historian/grader.mjs {unit.id}
|
|
54
|
-
"agentModel": "
|
|
57
|
+
"runCommand": "bash ${ABATHUR_REPO}/graders/historian/run-scenario.sh {unit.id} {repoRoot}/{unit.path}",
|
|
58
|
+
"graderCommand": "node ${ABATHUR_REPO}/graders/historian/grader.mjs {unit.id} {repoRoot}/{unit.path} ${ABATHUR_WIKI_BASE}",
|
|
59
|
+
"agentModel": "replace-me/provider/model", // operator: substitute the real <provider>/<model> when copying
|
|
55
60
|
// Real agent runs, not unit tests: 1200s caps one stuck scenario without
|
|
56
61
|
// poisoning the generation (timeout => unscored/inconclusive, excluded by
|
|
57
62
|
// the stats layer). seed+reset hooks share a FIXED 60s adapter budget
|
package/dist/bench/adapter.js
CHANGED
|
@@ -10,11 +10,29 @@
|
|
|
10
10
|
// excluded from score distributions (hr discipline), not zeroed.
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
12
|
import { cannotAnswer } from "../exit.js";
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* `{repoRoot}` = the bench's ACTIVE TREE: the genome repoPath for the incumbent,
|
|
15
|
+
* the sealed candidate worktree for a candidate (run-loop.ts swaps spec.repoPath
|
|
16
|
+
* per target; adapters resolve it through effectiveRepoPath at construction).
|
|
17
|
+
* Omitting repoRoot keeps `{repoRoot}` an UNKNOWN placeholder — fail-closed
|
|
18
|
+
* exit 2, never a silently half-substituted command.
|
|
19
|
+
*/
|
|
20
|
+
export function unitVars(unit, sandboxDir, repoRoot) {
|
|
21
|
+
const vars = {
|
|
22
|
+
"unit.path": unit.path,
|
|
23
|
+
"unit.id": unit.id,
|
|
24
|
+
sandbox: sandboxDir,
|
|
25
|
+
workdir: sandboxDir,
|
|
26
|
+
};
|
|
27
|
+
if (repoRoot !== undefined)
|
|
28
|
+
vars["repoRoot"] = repoRoot;
|
|
29
|
+
return vars;
|
|
15
30
|
}
|
|
16
|
-
export function sandboxVars(sandboxDir) {
|
|
17
|
-
|
|
31
|
+
export function sandboxVars(sandboxDir, repoRoot) {
|
|
32
|
+
const vars = { sandbox: sandboxDir, workdir: sandboxDir };
|
|
33
|
+
if (repoRoot !== undefined)
|
|
34
|
+
vars["repoRoot"] = repoRoot;
|
|
35
|
+
return vars;
|
|
18
36
|
}
|
|
19
37
|
const PLACEHOLDER = /\{([A-Za-z0-9_.]+)\}/g;
|
|
20
38
|
/**
|
package/dist/bench/fixture.js
CHANGED
|
@@ -32,6 +32,7 @@ import { cannotAnswer, ExitSignal } from "../exit.js";
|
|
|
32
32
|
import { resolveConfigDir } from "../config.js";
|
|
33
33
|
import { fingerprint } from "../core/ids.js";
|
|
34
34
|
import { acquireGenomeLock, Ledger } from "../core/ledger.js";
|
|
35
|
+
import { effectiveRepoPath } from "../core/spec.js";
|
|
35
36
|
import { childStatus, firstLine, inconclusive, parseGraderLine, renderCommand, runChild, sandboxVars, unitVars, ZERO_METRICS, } from "./adapter.js";
|
|
36
37
|
import { buildManifest, gateVal, mirrorSandboxHome, parseRunMeta, sandboxHomeDir, transcriptPathFor, writeManifest, } from "./fixture-support.js";
|
|
37
38
|
import { probeEngines, resolveOpencodeBin } from "./fixture-probe.js";
|
|
@@ -52,7 +53,10 @@ export class FixtureScenariosAdapter {
|
|
|
52
53
|
if (spec.bench.type !== "opencode-fixture-scenarios") {
|
|
53
54
|
cannotAnswer(`fixture adapter: bench.type '${spec.bench.type}' is not "opencode-fixture-scenarios"`, "use ToyBenchAdapter for bench.type 'toy'");
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
+
// Active-tree resolution follows the run-loop notion (run-loop.ts:142): a
|
|
57
|
+
// concrete repoPath resolves exactly as before; a `${VAR}` env literal
|
|
58
|
+
// resolves at THIS fs boundary or exits 2 naming the variable.
|
|
59
|
+
const root = effectiveRepoPath(spec.repoPath);
|
|
56
60
|
if (!statSync(root, { throwIfNoEntry: false })?.isDirectory()) {
|
|
57
61
|
cannotAnswer(`fixture: repoPath '${spec.repoPath}' is not a readable directory`);
|
|
58
62
|
}
|
|
@@ -84,7 +88,7 @@ export class FixtureScenariosAdapter {
|
|
|
84
88
|
const transcript = transcriptPathFor(sandboxDir, unit.id);
|
|
85
89
|
mkdirSync(path.dirname(transcript), { recursive: true });
|
|
86
90
|
const outcome = await runChild({
|
|
87
|
-
argv: renderCommand(this.spec.bench.runCommand, unitVars(unit, sandboxDir)),
|
|
91
|
+
argv: renderCommand(this.spec.bench.runCommand, unitVars(unit, sandboxDir, this.repoRoot)),
|
|
88
92
|
cwd: sandboxDir,
|
|
89
93
|
timeoutS,
|
|
90
94
|
env: this.sandboxEnv(sandboxDir, unit),
|
|
@@ -109,7 +113,7 @@ export class FixtureScenariosAdapter {
|
|
|
109
113
|
cannotAnswer("fixture adapter: score() called before any reset()/seed()/run() — no sandbox yet", "drive the adapter in reset→seed→run→score order");
|
|
110
114
|
}
|
|
111
115
|
const outcome = await runChild({
|
|
112
|
-
argv: renderCommand(this.spec.bench.graderCommand, unitVars(unit, sandbox)),
|
|
116
|
+
argv: renderCommand(this.spec.bench.graderCommand, unitVars(unit, sandbox, this.repoRoot)),
|
|
113
117
|
cwd: sandbox,
|
|
114
118
|
timeoutS: this.spec.bench.timeoutS,
|
|
115
119
|
env: this.sandboxEnv(sandbox),
|
|
@@ -211,7 +215,7 @@ export class FixtureScenariosAdapter {
|
|
|
211
215
|
if (template === undefined)
|
|
212
216
|
return;
|
|
213
217
|
const outcome = await runChild({
|
|
214
|
-
argv: renderCommand(template, sandboxVars(sandboxDir)),
|
|
218
|
+
argv: renderCommand(template, sandboxVars(sandboxDir, this.repoRoot)),
|
|
215
219
|
cwd: sandboxDir,
|
|
216
220
|
timeoutS: HOOK_TIMEOUT_S,
|
|
217
221
|
env: this.sandboxEnv(sandboxDir),
|
package/dist/bench/toy.js
CHANGED
|
@@ -53,7 +53,7 @@ export class ToyBenchAdapter {
|
|
|
53
53
|
async run(unit, sandboxDir, timeoutS) {
|
|
54
54
|
this.activeSandbox = sandboxDir;
|
|
55
55
|
const outcome = await runChild({
|
|
56
|
-
argv: renderCommand(this.spec.bench.runCommand, unitVars(unit, sandboxDir)),
|
|
56
|
+
argv: renderCommand(this.spec.bench.runCommand, unitVars(unit, sandboxDir, this.repoRoot)),
|
|
57
57
|
cwd: sandboxDir,
|
|
58
58
|
timeoutS,
|
|
59
59
|
...(this.opts.onChild === undefined ? {} : { onChild: this.opts.onChild }),
|
|
@@ -75,7 +75,7 @@ export class ToyBenchAdapter {
|
|
|
75
75
|
cannotAnswer("toy adapter: score() called before any reset()/seed()/run() — no sandbox yet", "drive the adapter in reset→seed→run→score order");
|
|
76
76
|
}
|
|
77
77
|
const outcome = await runChild({
|
|
78
|
-
argv: renderCommand(this.spec.bench.graderCommand, unitVars(unit, sandbox)),
|
|
78
|
+
argv: renderCommand(this.spec.bench.graderCommand, unitVars(unit, sandbox, this.repoRoot)),
|
|
79
79
|
cwd: sandbox,
|
|
80
80
|
timeoutS: this.spec.bench.timeoutS,
|
|
81
81
|
...(this.opts.onChild === undefined ? {} : { onChild: this.opts.onChild }),
|
|
@@ -109,7 +109,7 @@ export class ToyBenchAdapter {
|
|
|
109
109
|
if (template === undefined)
|
|
110
110
|
return;
|
|
111
111
|
const outcome = await runChild({
|
|
112
|
-
argv: renderCommand(template, sandboxVars(sandboxDir)),
|
|
112
|
+
argv: renderCommand(template, sandboxVars(sandboxDir, this.repoRoot)),
|
|
113
113
|
cwd: sandboxDir,
|
|
114
114
|
timeoutS: HOOK_TIMEOUT_S,
|
|
115
115
|
...(this.opts.onChild === undefined ? {} : { onChild: this.opts.onChild }),
|
package/dist/commands/status.js
CHANGED
|
@@ -15,6 +15,7 @@ import { tryGit } from "../util/git.js";
|
|
|
15
15
|
import { readRegistry } from "../core/genome.js";
|
|
16
16
|
import { LEDGER_KIND_GRAFT_IMPORT, decodeGraftImport, listGraftQueue, } from "../core/graft-support.js";
|
|
17
17
|
import { resolveUniqueEntry } from "./run.js";
|
|
18
|
+
import { effectiveRepoPath } from "../core/spec.js";
|
|
18
19
|
const KIND_GENERATION = "generation_complete";
|
|
19
20
|
const KIND_PROMOTE = "promote";
|
|
20
21
|
const KIND_TOMBSTONE = "tombstone";
|
|
@@ -113,7 +114,11 @@ async function runStatus(args) {
|
|
|
113
114
|
renderQueue(queue);
|
|
114
115
|
return EXIT_OK;
|
|
115
116
|
}
|
|
116
|
-
|
|
117
|
+
// Resolve at the fs seam exactly like run/genome/kernel (run-loop.ts:142):
|
|
118
|
+
// feeding the stored UNRESOLVED `${VAR}` literal to git/ledger paths makes
|
|
119
|
+
// Node report the missing cwd as the misleading `spawn git ENOENT`; an unset
|
|
120
|
+
// variable is now a clean exit 2 that names it.
|
|
121
|
+
const repo = effectiveRepoPath(entry.spec.repoPath);
|
|
117
122
|
const records = readLedgerRecords(ledgerPath(repo));
|
|
118
123
|
const gens = generations(records);
|
|
119
124
|
writeStdout(`genome: ${flat(entry.label)} (${entry.fingerprint}) repo: ${flat(repo, 200)}`);
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { cannotAnswer } from "../../exit.js";
|
|
5
5
|
import { runId } from "../ids.js";
|
|
6
6
|
import { buildManifest } from "../kernel.js";
|
|
7
|
+
import { effectiveRepoPath } from "../spec.js";
|
|
7
8
|
import { budgetExhausted } from "../stats.js";
|
|
8
9
|
import { ToyBenchAdapter } from "../../bench/toy.js";
|
|
9
10
|
import { FixtureScenariosAdapter } from "../../bench/fixture.js";
|
|
@@ -15,10 +16,11 @@ export function cloneMatrixRow(u) {
|
|
|
15
16
|
export function copyProvenance(p) {
|
|
16
17
|
return { benchType: p.benchType, versions: p.versions.map((v) => ({ bin: v.bin, version: v.version })) };
|
|
17
18
|
}
|
|
18
|
-
//
|
|
19
|
-
//
|
|
19
|
+
// Invariant: unscored units (timeout/infra_failed/budget cut) stay in the matrix —
|
|
20
|
+
// dropping them asymmetrically shifts one arm's aggregate pool (campaign-6 artifact);
|
|
21
|
+
// evaluate() fail-closes any arm's n<2 as indeterminate before any aggregation.
|
|
20
22
|
export function asReplicates(rows) {
|
|
21
|
-
return rows.
|
|
23
|
+
return rows.map((u) => ({ unitId: u.unitId, split: u.split, scores: u.scores }));
|
|
22
24
|
}
|
|
23
25
|
export * from "./run-rows.js";
|
|
24
26
|
/** toy ⇒ stateless adapter; fixture ⇒ ctor acquires the fingerprint lock immediately. */
|
|
@@ -164,7 +166,7 @@ export async function benchTarget(o) {
|
|
|
164
166
|
spent,
|
|
165
167
|
provenance: provenance ?? { benchType: o.spec.bench.type, versions: [] },
|
|
166
168
|
complete,
|
|
167
|
-
manifest: buildManifest(
|
|
169
|
+
manifest: buildManifest(effectiveRepoPath(o.spec.repoPath), o.spec.kernel.immutableGlobs),
|
|
168
170
|
failures,
|
|
169
171
|
};
|
|
170
172
|
}
|
|
@@ -32,7 +32,7 @@ import { compactUtc, fingerprint, genId } from "../ids.js";
|
|
|
32
32
|
import { LEDGER_KIND_GENERATION_COMPLETE, acquireGenomeLock, Ledger } from "../ledger.js";
|
|
33
33
|
import { openGenome } from "../worktree.js";
|
|
34
34
|
import { clampReps, evaluate, needsExit } from "../stats.js";
|
|
35
|
-
import { effectiveRepoPath, isEnvRepoLiteral } from "../spec.js";
|
|
35
|
+
import { effectiveRepoPath, envRepoName, isEnvRepoLiteral } from "../spec.js";
|
|
36
36
|
import { ChildTracker, reapOrphans } from "./child-track.js";
|
|
37
37
|
import { buildBrief } from "./brief.js";
|
|
38
38
|
import { startRunFriction } from "./run-friction.js";
|
|
@@ -101,7 +101,13 @@ async function evolve(opts, mutatorCommand, ledger, caps, reps, env, now) {
|
|
|
101
101
|
const spec = opts.entry.spec;
|
|
102
102
|
const lines = [];
|
|
103
103
|
const genomeRepo = effectiveRepoPath(spec.repoPath);
|
|
104
|
-
|
|
104
|
+
// Engine-self ONLY: a `${VAR}` literal is a path-injection convenience any
|
|
105
|
+
// harness repo may use (campaign-1 misroute pin: selfmode-literal.test.ts).
|
|
106
|
+
const selfMode = envRepoName(spec.repoPath) === "ABATHUR_SELF_REPO";
|
|
107
|
+
// Env-literals resolve to concrete paths exactly once, here at the FS
|
|
108
|
+
// boundary; bench/reflect drivers below never see the raw literal.
|
|
109
|
+
// Concrete repoPaths flow through byte-identical to before.
|
|
110
|
+
const loopSpec = isEnvRepoLiteral(spec.repoPath) ? { ...spec, repoPath: genomeRepo } : spec;
|
|
105
111
|
const fric = opts.friction === undefined ? null : startRunFriction(opts.friction);
|
|
106
112
|
// Reap only under the genome lock: every remaining log entry belongs to a dead run.
|
|
107
113
|
const reap = reapOrphans(genomeRepo);
|
|
@@ -131,7 +137,7 @@ async function evolve(opts, mutatorCommand, ledger, caps, reps, env, now) {
|
|
|
131
137
|
else {
|
|
132
138
|
const gen = genId(fingerprint({ incumbent: opened.headCommit, at: now().getTime() }), now());
|
|
133
139
|
const out = await benchTarget({
|
|
134
|
-
spec,
|
|
140
|
+
spec: loopSpec,
|
|
135
141
|
genId: gen,
|
|
136
142
|
reps,
|
|
137
143
|
caps,
|
|
@@ -181,7 +187,7 @@ async function evolve(opts, mutatorCommand, ledger, caps, reps, env, now) {
|
|
|
181
187
|
const brief = buildBrief(spec, evidence, counters);
|
|
182
188
|
tracker.phase(`mutator-${invId}`, "mutator-session");
|
|
183
189
|
const session = await runMutatorSession({
|
|
184
|
-
spec:
|
|
190
|
+
spec: loopSpec,
|
|
185
191
|
brief,
|
|
186
192
|
mutatorCommand,
|
|
187
193
|
...(opts.opencodeBin === undefined ? {} : { opencodeBin: opts.opencodeBin }),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// --dry-run plan rendering + budget-cap resolution (todo 9). Presentation of
|
|
2
2
|
// the resolved startup state only — the dry-run gate in run-loop.ts calls this
|
|
3
3
|
// AFTER the kernel audit and BEFORE any ledger/lock/spawn touch.
|
|
4
|
-
import
|
|
4
|
+
import { effectiveRepoPath } from "../spec.js";
|
|
5
5
|
import { peekPlanState } from "./run-rows.js";
|
|
6
6
|
/** `--max-candidates` clamps DOWN to the genome's own cap, never above. */
|
|
7
7
|
export function effectiveCaps(spec, clamp) {
|
|
@@ -15,12 +15,16 @@ export function effectiveCaps(spec, clamp) {
|
|
|
15
15
|
}
|
|
16
16
|
export function planLines(req) {
|
|
17
17
|
const spec = req.entry.spec;
|
|
18
|
-
|
|
18
|
+
// `${VAR}` literals resolve through the same FS-boundary contract as every
|
|
19
|
+
// other consumer; the plan line + resume peek must never join the raw
|
|
20
|
+
// literal against cwd.
|
|
21
|
+
const genomeRepo = effectiveRepoPath(spec.repoPath);
|
|
22
|
+
const peek = peekPlanState(genomeRepo);
|
|
19
23
|
const specMax = spec.budget.maxCandidates;
|
|
20
24
|
const unitList = spec.bench.units.map((u) => `${u.id}(${u.split})`).join(" ");
|
|
21
25
|
return [
|
|
22
26
|
`run plan — genome '${spec.label}' (${req.entry.fingerprint})`,
|
|
23
|
-
` repo: ${
|
|
27
|
+
` repo: ${genomeRepo}`,
|
|
24
28
|
` bench: ${spec.bench.type} — timeoutS=${String(spec.bench.timeoutS)} stats minEffect=${String(spec.bench.stats.minEffect)} halfWidth=${String(spec.bench.stats.halfWidth)}`,
|
|
25
29
|
` units: ${unitList}`,
|
|
26
30
|
...(req.requiresProbed === undefined
|
package/dist/core/spec.js
CHANGED
|
@@ -126,7 +126,11 @@ export function errorText(cause) {
|
|
|
126
126
|
* resolved ONLY at FS boundaries, never inside the spec.
|
|
127
127
|
*/
|
|
128
128
|
const ENV_REPO_LITERAL = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
|
|
129
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* True when the spec stores an unresolved `${VAR}` env literal — a path-
|
|
131
|
+
* injection convenience ANY genome may use, NOT an engine-self discriminator
|
|
132
|
+
* (for that compare envRepoName against ABATHUR_SELF_REPO).
|
|
133
|
+
*/
|
|
130
134
|
export function isEnvRepoLiteral(repoPath) {
|
|
131
135
|
return ENV_REPO_LITERAL.test(repoPath);
|
|
132
136
|
}
|
package/dist/core/stats.js
CHANGED
|
@@ -132,29 +132,47 @@ export function evaluate(input) {
|
|
|
132
132
|
const candByUnit = new Map(candidate.units.map((u) => [u.unitId, u]));
|
|
133
133
|
const incByUnit = new Map(incumbent.units.map((u) => [u.unitId, u]));
|
|
134
134
|
const failures = [];
|
|
135
|
-
// 2)
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
// 2) Cross-arm replicate rule. A unit measured cleanly (n>=2) on ONE arm but
|
|
136
|
+
// not on the other leaves that comparison's variance undefined ⇒
|
|
137
|
+
// indeterminate, never a silent cull and never a nomination (campaign-6:
|
|
138
|
+
// the candidate's train unit scored while the incumbent's had timed out;
|
|
139
|
+
// the old one-sided drop shifted the incumbent aggregate to the val
|
|
140
|
+
// fallback and minted a bogus negative gain). A unit unmeasured on BOTH
|
|
141
|
+
// arms is excluded symmetrically — every aggregate and comparison then
|
|
142
|
+
// runs on a pool both arms share replicate-for-replicate.
|
|
143
|
+
const excluded = new Set();
|
|
144
|
+
for (const unitId of new Set([...incByUnit.keys(), ...candByUnit.keys()])) {
|
|
145
|
+
const cn = candByUnit.get(unitId)?.scores.length ?? 0;
|
|
146
|
+
const inn = incByUnit.get(unitId)?.scores.length ?? 0;
|
|
147
|
+
if (cn >= 2 && inn < 2) {
|
|
148
|
+
failures.push(`n=${inn} on incumbent unit '${unitId}': baseline variance undefined — indeterminate, never nominated`);
|
|
140
149
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
else if (inn >= 2 && cn < 2) {
|
|
151
|
+
if (incByUnit.get(unitId)?.split === "val") {
|
|
152
|
+
failures.push(`no replicates for val unit '${unitId}' on candidate '${candidate.runId}' — variance undefined, never nominated`);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
failures.push(`n=${cn} on unit '${unitId}': variance undefined — indeterminate, never nominated`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (cn < 2 && inn < 2) {
|
|
159
|
+
excluded.add(unitId);
|
|
145
160
|
}
|
|
146
161
|
}
|
|
147
162
|
if (failures.length > 0) {
|
|
148
|
-
return { ...base, verdict: "indeterminate", exitCode: needsExit("indeterminate"), gain:
|
|
163
|
+
return { ...base, verdict: "indeterminate", exitCode: needsExit("indeterminate"), gain: null, unitComparisons: [], failures };
|
|
149
164
|
}
|
|
165
|
+
const candKept = candidate.units.filter((u) => !excluded.has(u.unitId));
|
|
166
|
+
const incKept = incumbent.units.filter((u) => !excluded.has(u.unitId));
|
|
167
|
+
const candByUnitKept = new Map(candKept.map((u) => [u.unitId, u]));
|
|
150
168
|
// 3) Per-val-unit comparisons + the two precision gates.
|
|
151
169
|
const unitComparisons = [];
|
|
152
170
|
for (const [unitId, incUnit] of incByUnit) {
|
|
153
|
-
if (incUnit.split !== "val")
|
|
171
|
+
if (incUnit.split !== "val" || excluded.has(unitId))
|
|
154
172
|
continue;
|
|
155
|
-
const candUnit =
|
|
173
|
+
const candUnit = candByUnitKept.get(unitId);
|
|
156
174
|
if (candUnit === undefined)
|
|
157
|
-
continue; //
|
|
175
|
+
continue; // excluded/unreachable
|
|
158
176
|
const cs = summarizeUnit(candUnit.scores, alpha);
|
|
159
177
|
const incumbentMean = summarizeUnit(incUnit.scores).mean;
|
|
160
178
|
const delta = cs.mean - incumbentMean;
|
|
@@ -174,8 +192,8 @@ export function evaluate(input) {
|
|
|
174
192
|
failures.push(`regression on unit '${unitId}': delta ${fmt(delta)} < ${fmt(-cs.ciHalfWidth)} (mean ${fmt(cs.mean)} < ${fmt(incumbentMean)} - ${fmt(cs.ciHalfWidth)})`);
|
|
175
193
|
}
|
|
176
194
|
}
|
|
177
|
-
// 4) Effect-size floor on the aggregate gain.
|
|
178
|
-
const gain = aggregateScore(
|
|
195
|
+
// 4) Effect-size floor on the aggregate gain (shared, symmetric pool).
|
|
196
|
+
const gain = aggregateScore(candKept) - aggregateScore(incKept);
|
|
179
197
|
if (gain < stats.minEffect) {
|
|
180
198
|
failures.push(`gain ${fmt(gain)} < minEffect ${fmt(stats.minEffect)}`);
|
|
181
199
|
}
|
|
@@ -31,3 +31,27 @@ test("renderCommand: quote-aware argv, placeholders, fail-closed junk (exit 2)",
|
|
|
31
31
|
expectExit2(() => renderCommand(" ", unitVars(unit, "/s")), "empty");
|
|
32
32
|
expectExit2(() => renderCommand('node "oops', unitVars(unit, "/s")), "unbalanced quote");
|
|
33
33
|
});
|
|
34
|
+
// S4 engine seam: {repoRoot} = the bench's ACTIVE TREE, threaded through the
|
|
35
|
+
// shared CommandVars constructors so both adapters resolve it identically.
|
|
36
|
+
// Without a threaded repoRoot the placeholder must stay FAIL-CLOSED (exit 2),
|
|
37
|
+
// exactly like any unknown placeholder — never a half-substituted command.
|
|
38
|
+
test("{repoRoot}: unitVars/sandboxVars carry the active tree into run/grader/hook templates", () => {
|
|
39
|
+
const unit = { id: "u1", path: "scenarios/a.md", split: "train" };
|
|
40
|
+
assert.deepEqual(renderCommand("bash run-scenario.sh {unit.id} {repoRoot}/{unit.path}", unitVars(unit, "/sbx", "/active/tree")), ["bash", "run-scenario.sh", "u1", "/active/tree/scenarios/a.md"]);
|
|
41
|
+
assert.deepEqual(renderCommand("bash seed-wrapped.sh {repoRoot}/seed_sandbox.sh", sandboxVars("/sbx", "/active/tree")), ["bash", "seed-wrapped.sh", "/active/tree/seed_sandbox.sh"]);
|
|
42
|
+
// pre-existing keys untouched by the seam
|
|
43
|
+
assert.deepEqual(unitVars(unit, "/sbx", "/r"), {
|
|
44
|
+
"unit.path": "scenarios/a.md",
|
|
45
|
+
"unit.id": "u1",
|
|
46
|
+
sandbox: "/sbx",
|
|
47
|
+
workdir: "/sbx",
|
|
48
|
+
repoRoot: "/r",
|
|
49
|
+
});
|
|
50
|
+
assert.deepEqual(sandboxVars("/sbx", "/r"), { sandbox: "/sbx", workdir: "/sbx", repoRoot: "/r" });
|
|
51
|
+
});
|
|
52
|
+
test("{repoRoot} unthreaded ⇒ fail-closed exit 2 (unknown placeholder behavior UNCHANGED)", () => {
|
|
53
|
+
const unit = { id: "u1", path: "p", split: "train" };
|
|
54
|
+
expectExit2(() => renderCommand("node x {repoRoot}", unitVars(unit, "/s")), "unknown placeholder");
|
|
55
|
+
expectExit2(() => renderCommand("node x {repoRoot}", sandboxVars("/s")), "unknown placeholder");
|
|
56
|
+
expectExit2(() => renderCommand("node x {repoRoot}", { worktree: "/w", brief: "/b" }), "unknown placeholder");
|
|
57
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// S4 engine seam — the {repoRoot} command variable. The mutation channel's
|
|
2
|
+
// delivery hinge: run/grader/seed/reset templates must resolve {repoRoot} to
|
|
3
|
+
// the bench's ACTIVE TREE — the genome repoPath for the incumbent, the sealed
|
|
4
|
+
// candidate worktree for a candidate (run-loop.ts swaps `spec.repoPath` before
|
|
5
|
+
// benching each candidate; the adapter must follow that same notion, including
|
|
6
|
+
// effectiveRepoPath's env-literal resolution at the FS boundary).
|
|
7
|
+
// Unknown placeholders stay fail-closed (exit 2). Offline: capture scripts
|
|
8
|
+
// record the rendered argv; the fake opencode only answers --version.
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { chmodSync, cpSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
12
|
+
import * as os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { test } from "node:test";
|
|
15
|
+
import { ExitSignal } from "../exit.js";
|
|
16
|
+
import { parseGenomeSpecDocument } from "../core/spec.js";
|
|
17
|
+
import { FixtureScenariosAdapter } from "../bench/fixture.js";
|
|
18
|
+
// ------------------------------------------------------------------- helpers
|
|
19
|
+
async function freshDir(t, prefix) {
|
|
20
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), `abathur-${prefix}-`));
|
|
21
|
+
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
async function writeScript(dir, name, body) {
|
|
25
|
+
const filePath = path.join(dir, name);
|
|
26
|
+
await writeFile(filePath, body, "utf8");
|
|
27
|
+
chmodSync(filePath, 0o755);
|
|
28
|
+
return filePath;
|
|
29
|
+
}
|
|
30
|
+
/** Records `argv[mode, out, ...rest]`: rest joined per line into `out`; prints the metric/score line. */
|
|
31
|
+
const CAPTURE = `#!/usr/bin/env node
|
|
32
|
+
import { writeFileSync } from "node:fs";
|
|
33
|
+
const [mode, out, ...rest] = process.argv.slice(2);
|
|
34
|
+
writeFileSync(out, rest.join("\\n") + "\\n", "utf8");
|
|
35
|
+
if (mode === "score") {
|
|
36
|
+
process.stdout.write(JSON.stringify({ unit: "one", score: 1, pass: true, metrics: { tokensEst: 3, turns: 1 } }) + "\\n");
|
|
37
|
+
} else {
|
|
38
|
+
process.stdout.write(JSON.stringify({ tokensEst: 3, turns: 1 }) + "\\n");
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
const FAKE_OC = `#!/usr/bin/env bash
|
|
42
|
+
if [ "\${1:-}" = "--version" ]; then echo "1.2.3"; exit 0; fi
|
|
43
|
+
exit 0
|
|
44
|
+
`;
|
|
45
|
+
const UNIT = { id: "one", path: "scenarios/one.md", split: "train" };
|
|
46
|
+
const VAL_UNIT = { id: "v1", path: "scenarios/v1.md", split: "val" };
|
|
47
|
+
async function seamFixture(t, genDir) {
|
|
48
|
+
const root = await freshDir(t, "seam-ad");
|
|
49
|
+
const capPath = await writeScript(root, "cap.mjs", CAPTURE);
|
|
50
|
+
const binDir = await freshDir(t, "seam-bin");
|
|
51
|
+
const binPath = await writeScript(binDir, "oc-fake", FAKE_OC);
|
|
52
|
+
const home = await freshDir(t, "seam-home");
|
|
53
|
+
const configDir = await freshDir(t, "seam-cfg");
|
|
54
|
+
const outDir = path.join(root, "caps");
|
|
55
|
+
mkdirSync(outDir, { recursive: true });
|
|
56
|
+
const out = (name) => path.join(outDir, `${name}.txt`);
|
|
57
|
+
const spec = parseGenomeSpecDocument({
|
|
58
|
+
label: "seam-germ",
|
|
59
|
+
repoPath: genDir,
|
|
60
|
+
bench: {
|
|
61
|
+
type: "opencode-fixture-scenarios",
|
|
62
|
+
units: [UNIT, VAL_UNIT],
|
|
63
|
+
runCommand: `node ${capPath} run ${out("run")} {repoRoot} {unit.path}`,
|
|
64
|
+
seedCommand: `node ${capPath} seed ${out("seed")} {repoRoot}`,
|
|
65
|
+
resetCommand: `node ${capPath} reset ${out("reset")} {repoRoot}`,
|
|
66
|
+
graderCommand: `node ${capPath} score ${out("grader")} {repoRoot}`,
|
|
67
|
+
agentModel: "test/agent",
|
|
68
|
+
timeoutS: 30,
|
|
69
|
+
stats: { halfWidth: 0.15, minEffect: 0.1, nReps: { initial: 1, max: 2 } },
|
|
70
|
+
},
|
|
71
|
+
budget: { maxCandidates: 2, maxModelCalls: 4, maxTokens: 10000, maxWallS: 600 },
|
|
72
|
+
kernel: { immutableGlobs: [] },
|
|
73
|
+
requires: [{ cmd: "node", args: ["--version"], probeExit: 0 }],
|
|
74
|
+
opencodeBinVersion: { minVersion: "1.0.0" },
|
|
75
|
+
}, "<test>");
|
|
76
|
+
const sandbox = path.join(root, "sandbox");
|
|
77
|
+
return {
|
|
78
|
+
spec,
|
|
79
|
+
genDir,
|
|
80
|
+
sandbox,
|
|
81
|
+
home,
|
|
82
|
+
cap: capPath,
|
|
83
|
+
binPath,
|
|
84
|
+
out,
|
|
85
|
+
adapter: (s) => new FixtureScenariosAdapter(s, { env: { HOME: home }, home, configDir, opencodeBin: binPath }),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Minimal genome dir (the fixture adapter only requires repoPath to be a directory). */
|
|
89
|
+
async function genomeTree(t, name = "germ") {
|
|
90
|
+
const genRoot = await freshDir(t, `seam-${name}`);
|
|
91
|
+
const genDir = path.join(genRoot, "germ");
|
|
92
|
+
mkdirSync(path.join(genDir, "scenarios"), { recursive: true });
|
|
93
|
+
writeFileSync(path.join(genDir, "scenarios", "one.md"), "# one\n\n## Brief\ndo it\n", "utf8");
|
|
94
|
+
writeFileSync(path.join(genDir, "scenarios", "v1.md"), "# v1\n\n## Brief\ndo it val\n", "utf8");
|
|
95
|
+
return genDir;
|
|
96
|
+
}
|
|
97
|
+
function readCap(file) {
|
|
98
|
+
return readFileSync(file, "utf8").trim();
|
|
99
|
+
}
|
|
100
|
+
function isExit2Matching(needle) {
|
|
101
|
+
return (cause) => cause instanceof ExitSignal && cause.code === 2 && needle.test(cause.message);
|
|
102
|
+
}
|
|
103
|
+
// --------------------------------------------------------------------- pins
|
|
104
|
+
test("{repoRoot} renders to the incumbent genome repoPath in run + grader + hooks", async (t) => {
|
|
105
|
+
const genDir = await genomeTree(t);
|
|
106
|
+
const f = await seamFixture(t, genDir);
|
|
107
|
+
const adapter = f.adapter(f.spec);
|
|
108
|
+
try {
|
|
109
|
+
await adapter.reset(f.sandbox);
|
|
110
|
+
await adapter.seed(f.sandbox);
|
|
111
|
+
const run = await adapter.run(UNIT, f.sandbox, 30);
|
|
112
|
+
assert.equal(run.status, "ok");
|
|
113
|
+
const score = await adapter.score(UNIT);
|
|
114
|
+
assert.equal(score.kind, "scored");
|
|
115
|
+
assert.equal(readCap(f.out("run")), `${genDir}\nscenarios/one.md`);
|
|
116
|
+
assert.equal(readCap(f.out("grader")), genDir);
|
|
117
|
+
assert.equal(readCap(f.out("seed")), genDir);
|
|
118
|
+
assert.equal(readCap(f.out("reset")), genDir);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
adapter.release();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
test("{repoRoot} follows the candidate worktree when spec.repoPath is swapped (run-loop.ts pattern)", async (t) => {
|
|
125
|
+
const genDir = await genomeTree(t);
|
|
126
|
+
const f = await seamFixture(t, genDir);
|
|
127
|
+
// simulate run-loop.ts:279 — benchTarget receives { ...spec, repoPath: worktreePath }
|
|
128
|
+
const worktree = path.join(path.dirname(genDir), "wt");
|
|
129
|
+
cpSync(genDir, worktree, { recursive: true });
|
|
130
|
+
mkdirSync(path.join(worktree, "abathur-notes"), { recursive: true });
|
|
131
|
+
writeFileSync(path.join(worktree, "abathur-notes", "card.md"), "planted run card\n", "utf8");
|
|
132
|
+
const candidate = f.adapter({ ...f.spec, repoPath: worktree });
|
|
133
|
+
try {
|
|
134
|
+
const sandbox = path.join(f.sandbox, "cand");
|
|
135
|
+
await candidate.reset(sandbox);
|
|
136
|
+
await candidate.seed(sandbox);
|
|
137
|
+
const run = await candidate.run(UNIT, sandbox, 30);
|
|
138
|
+
assert.equal(run.status, "ok");
|
|
139
|
+
const score = await candidate.score(UNIT);
|
|
140
|
+
assert.equal(score.kind, "scored");
|
|
141
|
+
assert.equal(readCap(f.out("run")), `${worktree}\nscenarios/one.md`);
|
|
142
|
+
assert.equal(readCap(f.out("grader")), worktree);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
candidate.release();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
test("fixture ctor resolves an env-literal repoPath via effectiveRepoPath (run-loop.ts:142 pattern)", async (t) => {
|
|
149
|
+
const genDir = await genomeTree(t, "lit");
|
|
150
|
+
const f = await seamFixture(t, genDir);
|
|
151
|
+
const litSpec = { ...f.spec, repoPath: "${ABATHUR_SEAM_LIT_REPO}" };
|
|
152
|
+
const base = {
|
|
153
|
+
env: { HOME: f.home },
|
|
154
|
+
home: f.home,
|
|
155
|
+
configDir: path.join(f.home, ".config", "abathur"),
|
|
156
|
+
opencodeBin: f.binPath,
|
|
157
|
+
};
|
|
158
|
+
delete process.env["ABATHUR_SEAM_LIT_REPO"];
|
|
159
|
+
try {
|
|
160
|
+
// unset env ⇒ clean exit 2 NAMING the variable (never a garbage-path spawn error)
|
|
161
|
+
assert.throws(() => new FixtureScenariosAdapter(litSpec, base), isExit2Matching(/requires env var ABATHUR_SEAM_LIT_REPO/));
|
|
162
|
+
// set env ⇒ resolves at the FS boundary; {repoRoot} renders to the resolved tree
|
|
163
|
+
process.env["ABATHUR_SEAM_LIT_REPO"] = genDir;
|
|
164
|
+
const adapter = new FixtureScenariosAdapter(litSpec, base);
|
|
165
|
+
try {
|
|
166
|
+
const sandbox = path.join(f.sandbox, "lit");
|
|
167
|
+
await adapter.reset(sandbox);
|
|
168
|
+
await adapter.seed(sandbox);
|
|
169
|
+
const run = await adapter.run(UNIT, sandbox, 30);
|
|
170
|
+
assert.equal(run.status, "ok");
|
|
171
|
+
assert.equal(readCap(f.out("run")), `${genDir}\nscenarios/one.md`);
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
adapter.release();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
delete process.env["ABATHUR_SEAM_LIT_REPO"];
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
test("fail-closed UNCHANGED: unknown placeholder through the adapter is still exit 2", async (t) => {
|
|
182
|
+
const genDir = await genomeTree(t, "unk");
|
|
183
|
+
const f = await seamFixture(t, genDir);
|
|
184
|
+
const bad = {
|
|
185
|
+
...f.spec,
|
|
186
|
+
bench: { ...f.spec.bench, runCommand: `node ${f.cap} run ${f.out("bad")} {nope}` },
|
|
187
|
+
};
|
|
188
|
+
const adapter = f.adapter(bad);
|
|
189
|
+
try {
|
|
190
|
+
await adapter.reset(f.sandbox);
|
|
191
|
+
await adapter.seed(f.sandbox);
|
|
192
|
+
await assert.rejects(() => adapter.run(UNIT, f.sandbox, 30), isExit2Matching(/unknown placeholder/));
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
adapter.release();
|
|
196
|
+
}
|
|
197
|
+
});
|