@tachikomagundam/abathur 0.2.4 → 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 +1 -1
- package/dist/core/evolve/run-bench.js +4 -3
- 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/historian-grader-integrity.test.js +987 -2
- package/dist/test/historian-grader-io.test.js +4 -0
- package/dist/test/historian-run-scenario.test.js +42 -2
- package/dist/test/selfmode-literal.test.js +153 -0
- package/dist/test/stats.test.js +18 -0
- package/graders/historian/grader-core.d.mts +80 -2
- package/graders/historian/grader-core.mjs +754 -3
- package/graders/historian/grader.mjs +21 -2
- package/graders/historian/judge-poststage.mjs +200 -0
- package/graders/historian/mutate.sh +59 -19
- package/graders/historian/run-scenario-17.sh +34 -0
- package/graders/historian/run-scenario.sh +8 -1
- package/package.json +1 -1
- package/plugin/abathur.ts +1 -1
|
@@ -138,16 +138,35 @@ if (APPLICABLE[scenarioNo] !== undefined) {
|
|
|
138
138
|
}
|
|
139
139
|
const sandboxRows = state.post
|
|
140
140
|
.filter((r) => isSandboxPath(r.path))
|
|
141
|
-
.map((r) => ({ path: r.path, id: String(r.id), description: String(r.description ?? "") }));
|
|
141
|
+
.map((r) => ({ path: r.path, locale: String(r.locale ?? "en"), id: String(r.id), description: String(r.description ?? "") }));
|
|
142
142
|
obs.tools = scanToolEvents(transcriptText);
|
|
143
|
-
|
|
143
|
+
const integrity = {
|
|
144
144
|
sandboxRows,
|
|
145
145
|
content: state.content,
|
|
146
146
|
rowIdByPath: new Map(sandboxRows.map((r) => [r.path, r.id])),
|
|
147
|
+
rowIdByLocalePath: new Map(sandboxRows.map((r) => [`${r.path}\u0000${r.locale}`, r.id])),
|
|
147
148
|
descByPath: new Map(sandboxRows.map((r) => [r.path, r.description])),
|
|
148
149
|
seedDescByPath: new Map(seed.rows.map((r) => [r.path, String(r.description ?? "")])),
|
|
150
|
+
seedRowIdByLocalePath: new Map(seed.rows.map((r) => [`${String(r.path)}\u0000${String(r.locale ?? "en")}`, String(r.id)])),
|
|
149
151
|
seedContent: Object.fromEntries(seed.rows.map((r) => [String(r.id), seed.content[String(r.id)] ?? ""])),
|
|
150
152
|
};
|
|
153
|
+
// s17 (蜂判首卷): the run-stage poststage's instrument output is OBSERVABLE
|
|
154
|
+
// STATE at .bench/judge-verdicts.json (same flow as .bench/transcripts).
|
|
155
|
+
// Absent ⇒ undefined (branch 17 fails the J leg closed with an explicit note);
|
|
156
|
+
// unparseable ⇒ {parseError}. Never thrown here — scoring stays deterministic
|
|
157
|
+
// given inputs, and an honest 0 beats a vacuous crash-to-inconclusive.
|
|
158
|
+
if (scenarioNo === 17) {
|
|
159
|
+
const verdictsPath = path.join(process.cwd(), ".bench", "judge-verdicts.json");
|
|
160
|
+
try {
|
|
161
|
+
integrity.judgeVerdicts = JSON.parse(readFileSync(verdictsPath, "utf8"));
|
|
162
|
+
} catch (cause) {
|
|
163
|
+
const code = cause && typeof cause === "object" ? String(cause.code ?? "") : "";
|
|
164
|
+
integrity.judgeVerdicts = code === "ENOENT"
|
|
165
|
+
? undefined
|
|
166
|
+
: { parseError: cause instanceof Error ? cause.message : String(cause) };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
obs.integrity = integrity;
|
|
151
170
|
}
|
|
152
171
|
|
|
153
172
|
const result = scoreUnit(obs);
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* judge-poststage.mjs — scenario-17 蜂判 run-stage poststage (deterministic
|
|
4
|
+
* plumbing, zero deps, argv-only spawns).
|
|
5
|
+
*
|
|
6
|
+
* Doctrine binding (historian .omo/evidence/good-wiki-readability-doctrine-FINAL.md):
|
|
7
|
+
* 总则2 [蜂判] = 传感器:盲评双跑、分歧仲裁、一致率入法官健康台账。
|
|
8
|
+
* 总则4 grader 只算结构/状态/工具事件;语义一律蜂判 ⇒ the LLM calls live
|
|
9
|
+
* HERE (run stage), never inside the shipped deterministic grader.
|
|
10
|
+
* B4 §4 arbitration = fresh isolated third run on the SAME (rubric, page),
|
|
11
|
+
* appended as rep3; majority 2/3; unresolved never agrees.
|
|
12
|
+
*
|
|
13
|
+
* Flow: materialize the created dossier pair (live wiki fetch by (path, locale)
|
|
14
|
+
* — read-only — or --files locale=path for offline fixture runs) → invoke
|
|
15
|
+
* judge-bench.mjs matrix (the certified instrument, imported by CLI spawn, NOT
|
|
16
|
+
* re-implemented) → map ledger rows onto {page, locale} → for every agree=false
|
|
17
|
+
* row, one `pair --reps 1` arbitration run appended as rep3 → fold the majority
|
|
18
|
+
* per row → write .bench/judge-verdicts.json (grader consumes it as observable
|
|
19
|
+
* state; s17FoldJudgeVerdicts re-derives everything fail-closed).
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* node judge-poststage.mjs --page _sandbox/eval17/gpu-warm-pool-dossier \
|
|
23
|
+
* --wiki-base http://localhost:3000 [--out .bench/judge-verdicts.json] \
|
|
24
|
+
* [--ledger .bench/judge-ledger.jsonl] [--reps 2] [--sleep-s 2] \
|
|
25
|
+
* [--timeout-s 240] [--model local-qwen/qwen3.8-flash-next] \
|
|
26
|
+
* [--rubrics a.md,b.md,c.md] [--bench judge-bench.mjs]
|
|
27
|
+
* node judge-poststage.mjs --page <label> --files en=g.md,zh=d.md [...]
|
|
28
|
+
*/
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
30
|
+
import { execFileSync } from "node:child_process";
|
|
31
|
+
import { appendFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
32
|
+
import os from "node:os";
|
|
33
|
+
import path from "node:path";
|
|
34
|
+
|
|
35
|
+
// D7 zero-literals: the bee-judge bench lives outside the repo (campaign
|
|
36
|
+
// evidence dir); operators wire it via ABATHUR_JUDGE_BENCH. Unset => the
|
|
37
|
+
// judge stage fails closed BEFORE spawn and the grader fail-closes the legs.
|
|
38
|
+
const JUDGE_BENCH = process.env.ABATHUR_JUDGE_BENCH ?? null;
|
|
39
|
+
const DEFAULTS = {
|
|
40
|
+
bench: JUDGE_BENCH ?? "",
|
|
41
|
+
rubrics: JUDGE_BENCH === null ? [] : ["R1-semantic", "R4-duty-v2", "R5-flavor"].map(
|
|
42
|
+
(r) => path.join(path.dirname(JUDGE_BENCH), "rubrics", `${r}.md`),
|
|
43
|
+
),
|
|
44
|
+
model: "local-qwen/qwen3.8-flash-next",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function parseArgs(argv) {
|
|
48
|
+
const o = { ...DEFAULTS, out: path.join(".bench", "judge-verdicts.json"), ledger: null, reps: 2, sleepS: 2, timeoutS: 240, page: null, wikiBase: null, files: null };
|
|
49
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
50
|
+
const a = argv[i];
|
|
51
|
+
const need = () => { i += 1; if (i >= argv.length) throw new Error(`${a} needs a value`); return argv[i]; };
|
|
52
|
+
if (a === "--page") o.page = need();
|
|
53
|
+
else if (a === "--wiki-base") o.wikiBase = need();
|
|
54
|
+
else if (a === "--files") o.files = need();
|
|
55
|
+
else if (a === "--rubrics") o.rubrics = need().split(",").filter(Boolean);
|
|
56
|
+
else if (a === "--bench") o.bench = need();
|
|
57
|
+
else if (a === "--model") o.model = need();
|
|
58
|
+
else if (a === "--out") o.out = need();
|
|
59
|
+
else if (a === "--ledger") o.ledger = need();
|
|
60
|
+
else if (a === "--reps") o.reps = Number(need());
|
|
61
|
+
else if (a === "--sleep-s") o.sleepS = Number(need());
|
|
62
|
+
else if (a === "--timeout-s") o.timeoutS = Number(need());
|
|
63
|
+
else throw new Error(`unknown arg: ${a}`);
|
|
64
|
+
}
|
|
65
|
+
if (o.page === null) throw new Error("--page <wiki path or label> required");
|
|
66
|
+
if (o.wikiBase === null && o.files === null) throw new Error("need --wiki-base <url> (live) or --files en=<f>,zh=<f> (offline fixtures)");
|
|
67
|
+
if (Number.isNaN(o.reps) || o.reps < 2) throw new Error("--reps must be ≥2 (盲评双跑 is the certified mode)");
|
|
68
|
+
return o;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const t0 = Date.now();
|
|
72
|
+
const log = (m) => process.stderr.write(`[judge-poststage] ${m}\n`);
|
|
73
|
+
|
|
74
|
+
async function fetchLocaleBody(base, pagePath, locale) {
|
|
75
|
+
const token = readFileSync(path.join(process.env.HOME ?? "", ".wikijs-api-key"), "utf8").trim();
|
|
76
|
+
const gql = async (query) => {
|
|
77
|
+
const res = await fetch(`${base.replace(/\/$/, "")}/graphql`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
80
|
+
body: JSON.stringify({ query }),
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok) throw new Error(`graphql http ${String(res.status)}`);
|
|
83
|
+
const doc = await res.json();
|
|
84
|
+
if (doc.errors !== undefined) throw new Error(`graphql ${JSON.stringify(doc.errors).slice(0, 200)}`);
|
|
85
|
+
return doc.data;
|
|
86
|
+
};
|
|
87
|
+
const list = (await gql("{ pages { list { id path locale } } }")).pages.list;
|
|
88
|
+
const row = list.find((r) => r.path === pagePath && String(r.locale ?? "en") === locale);
|
|
89
|
+
if (row === undefined) return null;
|
|
90
|
+
const one = (await gql(`{ pages { single(id: ${String(row.id)}) { content } } }`)).pages.single;
|
|
91
|
+
return one === null ? null : String(one.content ?? "");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function runBench(bench, args) {
|
|
95
|
+
// argv-only spawn (no shell), same contract as judge-bench's own isolation.
|
|
96
|
+
return execFileSync(process.execPath, [bench, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024 });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function ledgerRows(ledgerPath) {
|
|
100
|
+
try {
|
|
101
|
+
return readFileSync(ledgerPath, "utf8").split("\n").filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function foldMajority(row) {
|
|
108
|
+
const okRep = (rep, repNo) => rep !== null && rep !== undefined && rep.rep === repNo && rep.status === "ok" && (rep.score === 0 || rep.score === 1);
|
|
109
|
+
if (!okRep(row.rep1, 1) || !okRep(row.rep2, 2)) return null;
|
|
110
|
+
if (row.rep1.score === row.rep2.score) return row.rep1.score;
|
|
111
|
+
if (!okRep(row.rep3, 3)) return null; // unresolved (B4 §4-c): never silently agrees
|
|
112
|
+
return row.rep1.score === row.rep3.score ? row.rep1.score : row.rep2.score;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
116
|
+
|
|
117
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
118
|
+
if (opts.bench === "") { console.error("judge-poststage: no bee-judge bench wired — set ABATHUR_JUDGE_BENCH (or pass --bench); grader fail-closes the judge legs without it."); process.exit(2); }
|
|
119
|
+
(async () => {
|
|
120
|
+
try {
|
|
121
|
+
const work = mkdtempSync(path.join(os.tmpdir(), "s17-judge-"));
|
|
122
|
+
const localeFile = new Map();
|
|
123
|
+
const targets = [["en", "en.md"], ["zh", "zh.md"]];
|
|
124
|
+
for (const [locale, fname] of targets) {
|
|
125
|
+
let body = null;
|
|
126
|
+
if (opts.files !== null) {
|
|
127
|
+
const pair = opts.files.split(",").map((s) => s.split("=")).find(([k]) => k === locale);
|
|
128
|
+
if (pair === undefined) { log(`--files has no ${locale} leg — skipped (coverage fails closed downstream)`); continue; }
|
|
129
|
+
body = readFileSync(path.resolve(pair[1]), "utf8");
|
|
130
|
+
} else {
|
|
131
|
+
body = await fetchLocaleBody(opts.wikiBase, opts.page, locale);
|
|
132
|
+
if (body === null) { log(`live wiki has no (${opts.page}, ${locale}) row — skipped (coverage fails closed downstream)`); continue; }
|
|
133
|
+
}
|
|
134
|
+
const f = path.join(work, fname); // neutral names: page label never enters the judge channel
|
|
135
|
+
writeFileSync(f, body);
|
|
136
|
+
localeFile.set(locale, f);
|
|
137
|
+
}
|
|
138
|
+
if (localeFile.size === 0) {
|
|
139
|
+
rmSync(work, { recursive: true, force: true });
|
|
140
|
+
throw new Error("nothing to judge — both locale bodies missing; write no verdicts file (grader fail-closes the 蜂判 legs)");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const ledgerPath = opts.ledger ?? path.join(".bench", "judge-ledger.jsonl");
|
|
144
|
+
mkdirSync(path.dirname(ledgerPath), { recursive: true });
|
|
145
|
+
const files = [...localeFile.values()].join(",");
|
|
146
|
+
log(`matrix: ${opts.rubrics.length} rubrics × ${String(localeFile.size)} locale file(s) × ${String(opts.reps)} reps = ${String(opts.rubrics.length * localeFile.size * opts.reps)} calls`);
|
|
147
|
+
runBench(opts.bench, ["matrix", "--rubrics", opts.rubrics.join(","), "--pages", files, "--reps", String(opts.reps),
|
|
148
|
+
"--sleep-s", String(opts.sleepS), "--timeout-s", String(opts.timeoutS), "--model", opts.model, "--ledger", ledgerPath]);
|
|
149
|
+
const fileToTag = new Map([...localeFile.entries()].map(([loc, f]) => [f, loc]));
|
|
150
|
+
let rows = ledgerRows(ledgerPath).filter((r) => fileToTag.has(r.page));
|
|
151
|
+
if (rows.length === 0) {
|
|
152
|
+
rmSync(work, { recursive: true, force: true });
|
|
153
|
+
throw new Error("judge-bench emitted no ledger rows for the staged pages (instrument failure, not a verdict)");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const arbLedger = `${ledgerPath}.arb`;
|
|
157
|
+
let arbitrations = 0;
|
|
158
|
+
for (const row of rows) {
|
|
159
|
+
if (row.agree === true) continue;
|
|
160
|
+
arbitrations += 1;
|
|
161
|
+
log(`arbitration (B4 §4-b): ${row.rubric} × ${fileToTag.get(row.page)} — fresh isolated rep3`);
|
|
162
|
+
try {
|
|
163
|
+
runBench(opts.bench, ["pair", "--rubric", opts.rubrics.find((r) => path.basename(r, ".md") === row.rubric) ?? row.rubric,
|
|
164
|
+
"--page", row.page, "--reps", "1", "--sleep-s", String(opts.sleepS), "--timeout-s", String(opts.timeoutS),
|
|
165
|
+
"--model", opts.model, "--ledger", arbLedger]);
|
|
166
|
+
const arb = ledgerRows(arbLedger).filter((r) => r.rubric === row.rubric && r.page === row.page).pop();
|
|
167
|
+
if (arb?.rep1 !== undefined) row.rep3 = { ...arb.rep1, rep: 3 };
|
|
168
|
+
} catch (e) {
|
|
169
|
+
log(`arbitration call failed: ${String(e.message).slice(0, 160)} — row stays UNRESOLVED`);
|
|
170
|
+
}
|
|
171
|
+
row.majority = foldMajority(row);
|
|
172
|
+
}
|
|
173
|
+
for (const row of rows) {
|
|
174
|
+
row.majority = row.majority ?? foldMajority(row);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const doc = {
|
|
178
|
+
generated: new Date().toISOString(),
|
|
179
|
+
unit: "scenario-17",
|
|
180
|
+
page: opts.page,
|
|
181
|
+
mode: opts.files !== null ? "offline-files" : "live-wiki",
|
|
182
|
+
model: opts.model,
|
|
183
|
+
instrument: { bench: opts.bench, rubrics: opts.rubrics.map((r) => ({ file: r, name: path.basename(r, ".md"), sha256: sha256(readFileSync(r, "utf8")) })), reps: opts.reps, sleepS: opts.sleepS, timeoutS: opts.timeoutS, ledger: ledgerPath, arbitrations },
|
|
184
|
+
wall_ms: Date.now() - t0,
|
|
185
|
+
rows: rows.map((r) => ({ ...r, page: opts.page, locale: fileToTag.get(r.page) ?? String(r.page) })),
|
|
186
|
+
};
|
|
187
|
+
mkdirSync(path.dirname(opts.out), { recursive: true });
|
|
188
|
+
writeFileSync(opts.out, JSON.stringify(doc, null, 1) + "\n");
|
|
189
|
+
rmSync(work, { recursive: true, force: true });
|
|
190
|
+
const counts = { ok: 0, zero: 0, unresolved: 0 };
|
|
191
|
+
for (const r of doc.rows) { if (r.majority === 1) counts.ok += 1; else if (r.majority === 0) counts.zero += 1; else counts.unresolved += 1; }
|
|
192
|
+
log(`wrote ${opts.out}: ${String(doc.rows.length)} rows, majority 1×${String(counts.ok)} 0×${String(counts.zero)} unresolved×${String(counts.unresolved)}, wall ${String(Math.round(doc.wall_ms / 1000))}s, arbitrations ${String(arbitrations)}`);
|
|
193
|
+
process.stdout.write(JSON.stringify({ out: opts.out, rows: doc.rows.length, ...counts, arbitrations, wall_ms: doc.wall_ms }) + "\n");
|
|
194
|
+
} catch (e) {
|
|
195
|
+
// instrument failure ≠ a verdict: exit 2 (inconclusive plumbing), no file —
|
|
196
|
+
// the grader's fail-closed 蜂判 legs own the honest 0 with an explicit note.
|
|
197
|
+
process.stderr.write(`[judge-poststage] FATAL ${e instanceof Error ? e.message : String(e)}\n`);
|
|
198
|
+
process.exit(2);
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
@@ -36,10 +36,10 @@ raw="${ABATHUR_MUTATOR_RAW:-/tmp/abathur-mutate-raw.jsonl}"
|
|
|
36
36
|
rc=$?
|
|
37
37
|
echo "mutate: opencode rc=$rc raw=$raw" >&2
|
|
38
38
|
|
|
39
|
-
python3 - "$raw" "$canonical" "$rc" <<'PY'
|
|
40
|
-
import json, re, sys
|
|
39
|
+
python3 - "$raw" "$canonical" "$rc" "$worktree" <<'PY'
|
|
40
|
+
import json, os, re, sys
|
|
41
41
|
|
|
42
|
-
raw, canonical_path, rc = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
|
42
|
+
raw, canonical_path, rc, worktree = sys.argv[1], sys.argv[2], int(sys.argv[3]), sys.argv[4]
|
|
43
43
|
|
|
44
44
|
def create_diff(path, text):
|
|
45
45
|
lines = text.split("\n")
|
|
@@ -73,19 +73,55 @@ for text in reversed(last_text_events(raw)):
|
|
|
73
73
|
fence = re.search(r"```(?:json)?\s*(\{.*\})\s*```", candidate, re.S)
|
|
74
74
|
if fence:
|
|
75
75
|
candidate = fence.group(1)
|
|
76
|
-
|
|
77
|
-
if
|
|
76
|
+
end = candidate.rfind("}")
|
|
77
|
+
if end < 0:
|
|
78
78
|
continue
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
# prose preambles can contain set-notation braces like {症状/Symptoms, ...} —
|
|
80
|
+
# the first '{' is not necessarily JSON; try every open brace from the LAST
|
|
81
|
+
# backwards so the outermost-to-last-close object wins when one parses.
|
|
82
|
+
opens = [i for i, ch in enumerate(candidate) if ch == "{" and i < end]
|
|
83
|
+
doc = None
|
|
84
|
+
for start in reversed(opens):
|
|
85
|
+
try:
|
|
86
|
+
parsed = json.loads(candidate[start : end + 1])
|
|
87
|
+
except ValueError:
|
|
88
|
+
continue
|
|
89
|
+
if isinstance(parsed, dict):
|
|
90
|
+
doc = parsed
|
|
91
|
+
break
|
|
92
|
+
if doc is not None:
|
|
84
93
|
proposal = doc
|
|
85
94
|
break
|
|
86
95
|
|
|
87
96
|
spec_text = open(canonical_path, encoding="utf-8").read()
|
|
88
|
-
|
|
97
|
+
if not spec_text.endswith("\n"):
|
|
98
|
+
spec_text += "\n"
|
|
99
|
+
# genome.jsonc in the base tree was written by this wrapper's own prior round,
|
|
100
|
+
# so a replace-all modify hunk always anchors deterministically at line 1.
|
|
101
|
+
# A CREATE here would break apply the moment the file lands in the base (udiff
|
|
102
|
+
# refuses create-over-existing and discards the WHOLE candidate).
|
|
103
|
+
gen_path = os.path.join(worktree, "genome.jsonc")
|
|
104
|
+
try:
|
|
105
|
+
with open(gen_path, encoding="utf-8") as fh:
|
|
106
|
+
base_text = fh.read()
|
|
107
|
+
except FileNotFoundError:
|
|
108
|
+
packaging = create_diff("genome.jsonc", spec_text)
|
|
109
|
+
except OSError as err:
|
|
110
|
+
sys.stderr.write("mutate: genome.jsonc unreadable: %s\n" % err)
|
|
111
|
+
sys.exit(3)
|
|
112
|
+
else:
|
|
113
|
+
if base_text == spec_text:
|
|
114
|
+
packaging = None
|
|
115
|
+
else:
|
|
116
|
+
base_body = base_text.split("\n")
|
|
117
|
+
if base_body and base_body[-1] == "":
|
|
118
|
+
base_body.pop()
|
|
119
|
+
old_h = "".join("-" + ln + "\n" for ln in base_body)
|
|
120
|
+
new_h = "".join("+" + ln + "\n" for ln in spec_text.split("\n")[:-1])
|
|
121
|
+
packaging = (
|
|
122
|
+
"--- a/genome.jsonc\n+++ b/genome.jsonc\n"
|
|
123
|
+
"@@ -1,%d +1,%d @@\n%s%s" % (len(base_body), len(spec_text.split("\n")) - 1, old_h, new_h)
|
|
124
|
+
)
|
|
89
125
|
|
|
90
126
|
if isinstance(proposal, dict):
|
|
91
127
|
path = proposal.get("path")
|
|
@@ -103,15 +139,19 @@ if isinstance(proposal, dict):
|
|
|
103
139
|
content = content.replace("\r", "")
|
|
104
140
|
if not content.endswith("\n"):
|
|
105
141
|
content += "\n"
|
|
106
|
-
|
|
142
|
+
diffs = [create_diff(path, content)] + ([packaging] if packaging else [])
|
|
143
|
+
candidate = {"id": "mutate-live", "rationale": rationale, "diffs": diffs}
|
|
107
144
|
print(json.dumps({"candidates": [candidate]}))
|
|
108
145
|
sys.exit(0)
|
|
109
146
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
147
|
+
if packaging:
|
|
148
|
+
print(json.dumps({
|
|
149
|
+
"candidates": [{
|
|
150
|
+
"id": "mutate-package-only",
|
|
151
|
+
"rationale": "packaging-only candidate: model output unparseable or path rejected (opencode rc=%d); lineage genome.jsonc added, no skill mutation proposed" % rc,
|
|
152
|
+
"diffs": [packaging],
|
|
153
|
+
}]
|
|
154
|
+
}))
|
|
155
|
+
else:
|
|
156
|
+
print(json.dumps({"candidates": []}))
|
|
117
157
|
PY
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scenario-17 run hook = run-scenario.sh (agent stage, c7 ≤8min band) + the 蜂判
|
|
3
|
+
# poststage (judge-poststage.mjs) against the ACTUAL pages the agent produced.
|
|
4
|
+
# Mirrors the .bench/transcripts flow: the shipped grader never calls a model;
|
|
5
|
+
# the LLM measurement is instrument output staged at run time into
|
|
6
|
+
# .bench/judge-verdicts.json, which grader.mjs reads as observable state
|
|
7
|
+
# (fail-closed when absent). The LAST stdout line stays run-scenario.sh's
|
|
8
|
+
# {unit,tokensEst,turns,opencodeExit} meta JSON — parseRunMeta's contract — so
|
|
9
|
+
# the engine-facing format is byte-compatible with units 01–16.
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
unit_id="${1:?usage: run-scenario-17.sh <unitId> <scenario-file> [repoRoot]}"
|
|
12
|
+
scenario_file="${2:?usage: run-scenario-17.sh <unitId> <scenario-file> [repoRoot]}"
|
|
13
|
+
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
dossier="${S17_DOSSIER:-_sandbox/eval17/gpu-warm-pool-dossier}"
|
|
15
|
+
wiki_base="${ABATHUR_WIKI_BASE:-http://localhost:3000}"
|
|
16
|
+
meta=""
|
|
17
|
+
agent_s=0
|
|
18
|
+
judge_s=0
|
|
19
|
+
if [ "${S17_SKIP_AGENT:-0}" != "1" ]; then
|
|
20
|
+
t=$SECONDS
|
|
21
|
+
meta="$(bash "$here/run-scenario.sh" "$@")"
|
|
22
|
+
agent_s=$((SECONDS - t))
|
|
23
|
+
fi
|
|
24
|
+
t=$SECONDS
|
|
25
|
+
node "$here/judge-poststage.mjs" --page "$dossier" --wiki-base "$wiki_base" \
|
|
26
|
+
--out "${ABATHUR_JUDGE_VERDICTS:-.bench/judge-verdicts.json}" \
|
|
27
|
+
--ledger "${ABATHUR_JUDGE_LEDGER:-.bench/judge-ledger-${unit_id}.jsonl}" 1>&2 || echo "[run-scenario-17] judge stage FAILED (no verdicts file — grader fail-closes the 蜂判 legs)" >&2
|
|
28
|
+
judge_s=$((SECONDS - t))
|
|
29
|
+
echo "[run-scenario-17] stage walls: agent=${agent_s}s judge=${judge_s}s (unit=${unit_id})" >&2
|
|
30
|
+
if [ -n "$meta" ]; then
|
|
31
|
+
printf '%s\n' "$meta"
|
|
32
|
+
else
|
|
33
|
+
printf '%s\n' "{\"unit\": \"$unit_id\", \"tokensEst\": 0, \"turns\": 0, \"opencodeExit\": -1}"
|
|
34
|
+
fi
|
|
@@ -57,7 +57,14 @@ model_args=()
|
|
|
57
57
|
if [ -n "${ABATHUR_AGENT_MODEL:-}" ]; then
|
|
58
58
|
model_args=(--model "$ABATHUR_AGENT_MODEL")
|
|
59
59
|
fi
|
|
60
|
-
"$bin" run --command historian --auto --format json "${model_args[@]}" --message "$brief" >"$ABATHUR_TRANSCRIPT" 2>/dev/null || status=$?
|
|
60
|
+
"$bin" run --command historian --auto --format json "${model_args[@]}" --message "$brief" >"${ABATHUR_TRANSCRIPT}.raw" 2>/dev/null || status=$?
|
|
61
|
+
# Campaign-1 channel fix (task-07-launch-blockers.md): plugin startup banners print to
|
|
62
|
+
# the agent's stdout and contaminate the captured JSONL. ONLY the known
|
|
63
|
+
# `[persistence-watchdog]` line is stripped into the grader-facing transcript —
|
|
64
|
+
# unknown non-JSON lines SURVIVE so the grader's corruption fail-closed stays
|
|
65
|
+
# honest — and a clean stream passes through byte-identical (F1 invariant).
|
|
66
|
+
# The verbatim capture remains at ${ABATHUR_TRANSCRIPT}.raw for audit.
|
|
67
|
+
LC_ALL=C grep -v '^\[persistence-watchdog\]' "${ABATHUR_TRANSCRIPT}.raw" >"$ABATHUR_TRANSCRIPT" || true
|
|
61
68
|
python3 - "$ABATHUR_TRANSCRIPT" "$unit_id" "$status" <<'PY'
|
|
62
69
|
import json, sys
|
|
63
70
|
path, unit, status = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
package/package.json
CHANGED
package/plugin/abathur.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// abathur-opencode-plugin v0.2.
|
|
1
|
+
// abathur-opencode-plugin v0.2.5
|
|
2
2
|
// Official opencode plugin adapter for the abathur evolution harness.
|
|
3
3
|
// Registers ONE agent tool, `abathur`, that shells out to the abathur CLI —
|
|
4
4
|
// argv-only (node:child_process execFile, never a shell), top-level commands
|