@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,10 +8,20 @@ export interface WikiRow {
|
|
|
8
8
|
readonly locale: string;
|
|
9
9
|
readonly updatedAt: string;
|
|
10
10
|
readonly title?: string | undefined;
|
|
11
|
+
readonly description?: string | undefined;
|
|
11
12
|
readonly isPublished?: boolean | undefined;
|
|
12
13
|
readonly isPrivate?: boolean | undefined;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/** One completed-or-not tool part of an opencode JSONL transcript, in order. */
|
|
17
|
+
export interface ToolEvent {
|
|
18
|
+
readonly index: number;
|
|
19
|
+
readonly tool: string;
|
|
20
|
+
readonly status: string;
|
|
21
|
+
readonly input: Readonly<Record<string, unknown>>;
|
|
22
|
+
readonly output: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
export interface TouchedPage {
|
|
16
26
|
readonly path: string;
|
|
17
27
|
readonly locale: string;
|
|
@@ -53,5 +63,6 @@ export interface DiffWikiRequest {
|
|
|
53
63
|
|
|
54
64
|
export function isSandboxPath(p: string): boolean;
|
|
55
65
|
export function parseTranscript(text: string): TranscriptMeta;
|
|
66
|
+
export function scanToolEvents(text: string): ToolEvent[];
|
|
56
67
|
export function scenarioNoFromUnit(unitId: string): number;
|
|
57
68
|
export function diffWiki(req: DiffWikiRequest): WikiDiff;
|
|
@@ -57,6 +57,34 @@ export function scenarioNoFromUnit(unitId) {
|
|
|
57
57
|
return Number(m[1]);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* opencode --format json tool events, in transcript order. Tolerant: garbage
|
|
62
|
+
* lines skipped (malformed_input ⇒ missing evidence scores 0, never crashes).
|
|
63
|
+
* Verbatim port of the task-05 proposal (historian baseline/grader-proposal/
|
|
64
|
+
* proposed-core.mjs) — unlike parseTranscript this must NOT throw: the I/J
|
|
65
|
+
* dims read it for evidence and absence of evidence scores 0.
|
|
66
|
+
*/
|
|
67
|
+
export function scanToolEvents(text) {
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const [i, raw] of text.split("\n").entries()) {
|
|
70
|
+
const line = raw.trim();
|
|
71
|
+
if (line.length === 0) continue;
|
|
72
|
+
let doc;
|
|
73
|
+
try { doc = JSON.parse(line); } catch { continue; }
|
|
74
|
+
const part = doc?.part;
|
|
75
|
+
if (part?.type !== "tool") continue;
|
|
76
|
+
const state = part.state ?? {};
|
|
77
|
+
out.push({
|
|
78
|
+
index: i,
|
|
79
|
+
tool: String(part.tool ?? ""),
|
|
80
|
+
status: String(state.status ?? ""),
|
|
81
|
+
input: state.input ?? {},
|
|
82
|
+
output: typeof state.output === "string" ? state.output : JSON.stringify(state.output ?? ""),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
60
88
|
function lastSegment(p) {
|
|
61
89
|
const segs = p.split("/");
|
|
62
90
|
return segs[segs.length - 1] ?? p;
|
|
@@ -8,14 +8,22 @@
|
|
|
8
8
|
// .bench/transcripts/<unitId>.jsonl recorded `opencode run --format json`
|
|
9
9
|
// .bench/wiki-pre.json post-seed row snapshot (seed-wrapped.sh)
|
|
10
10
|
// $ABATHUR_GRADER_STATE optional offline state file {post, content, urlStatus}
|
|
11
|
+
// argv[1] is the unit scenario rendered by the engine as `{repoRoot}/{unit.path}`
|
|
12
|
+
// — the bench's ACTIVE TREE (incumbent repoPath / candidate worktree, S4 seam).
|
|
13
|
+
// It is read only when it exists (offline fixtures may omit it) and must agree
|
|
14
|
+
// with the unit id on the scenario number: disagreement means the command was
|
|
15
|
+
// baked against the wrong tree, so the grader exits nonzero (inconclusive).
|
|
11
16
|
// Live mode queries the wiki GraphQL list, fetches _sandbox page bodies, and
|
|
12
17
|
// (scenario 07 only) anonymously probes every reported page URL for HTTP 200.
|
|
18
|
+
// Integrity units (scenario-10/11/12) additionally REQUIRE .bench/seed-state.json
|
|
19
|
+
// (the per-unit seed capture) — without it they exit nonzero (inconclusive),
|
|
20
|
+
// never scoring vacuously against missing evidence.
|
|
13
21
|
|
|
14
22
|
import { readFileSync } from "node:fs";
|
|
15
23
|
import path from "node:path";
|
|
16
24
|
|
|
17
|
-
import { scoreUnit } from "./grader-core.mjs";
|
|
18
|
-
import { diffWiki, parseTranscript, scenarioNoFromUnit } from "./grader-support.mjs";
|
|
25
|
+
import { APPLICABLE, scoreUnit } from "./grader-core.mjs";
|
|
26
|
+
import { diffWiki, isSandboxPath, parseTranscript, scanToolEvents, scenarioNoFromUnit } from "./grader-support.mjs";
|
|
19
27
|
|
|
20
28
|
function fail(msg) {
|
|
21
29
|
process.stderr.write(`grader: ${msg}\n`);
|
|
@@ -35,15 +43,38 @@ if (unitId === undefined || unitId.length === 0 || scenarioPath === undefined) {
|
|
|
35
43
|
fail("usage: grader.mjs <unitId> <scenarioPath> [wikiBase]");
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
let transcriptText;
|
|
38
47
|
let meta;
|
|
39
48
|
try {
|
|
40
|
-
|
|
49
|
+
transcriptText = readFileSync(path.join(process.cwd(), ".bench", "transcripts", `${unitId}.jsonl`), "utf8");
|
|
50
|
+
meta = parseTranscript(transcriptText);
|
|
41
51
|
} catch (cause) {
|
|
42
52
|
fail(`transcript unusable: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
const pre = readJson(path.join(process.cwd(), ".bench", "wiki-pre.json"));
|
|
46
56
|
const scenarioNo = scenarioNoFromUnit(unitId);
|
|
57
|
+
|
|
58
|
+
// Active-tree scenario resolution mirrors the ABATHUR_GRADER_STATE pattern at
|
|
59
|
+
// the bottom of this file: the received path is authoritative, consulted only
|
|
60
|
+
// when the file exists. A readable scenario must carry the unit's number —
|
|
61
|
+
// file basename digits vs unitId digits, both Number()-normalized ("09" ⇒ 9).
|
|
62
|
+
function resolveScenario(id, p) {
|
|
63
|
+
if (p === undefined || p.length === 0) return null;
|
|
64
|
+
const abs = path.resolve(p);
|
|
65
|
+
try {
|
|
66
|
+
readFileSync(abs, "utf8");
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const fileNo = /(\d+)/.exec(path.basename(abs));
|
|
71
|
+
const unitNo = /(\d+)/.exec(id);
|
|
72
|
+
if (fileNo !== null && unitNo !== null && Number(fileNo[1]) !== Number(unitNo[1])) {
|
|
73
|
+
fail(`scenario file ${abs} (number ${fileNo[1]}) does not match unit '${id}' (number ${unitNo[1]})`);
|
|
74
|
+
}
|
|
75
|
+
return abs;
|
|
76
|
+
}
|
|
77
|
+
const scenarioFile = resolveScenario(unitId, scenarioPath);
|
|
47
78
|
const URL_RE = /https?:\/\/\S+\/(?:en|zh)\/_sandbox\/\S+/g;
|
|
48
79
|
|
|
49
80
|
function reportedUrls(message) {
|
|
@@ -66,7 +97,7 @@ async function liveWikiState() {
|
|
|
66
97
|
if (doc.errors !== undefined) throw new Error(`graphql ${JSON.stringify(doc.errors).slice(0, 200)}`);
|
|
67
98
|
return doc.data;
|
|
68
99
|
};
|
|
69
|
-
const list = await gql("{ pages { list { id path locale title updatedAt } } }");
|
|
100
|
+
const list = await gql("{ pages { list { id path locale title description updatedAt } } }");
|
|
70
101
|
const post = list.pages.list;
|
|
71
102
|
const content = {};
|
|
72
103
|
for (const row of post) {
|
|
@@ -94,7 +125,51 @@ const state =
|
|
|
94
125
|
|
|
95
126
|
const diff = diffWiki({ pre, post: state.post, content: state.content, scenarioNo });
|
|
96
127
|
const urlChecks = Object.entries(state.urlStatus ?? {}).map(([url, status]) => ({ url, status }));
|
|
97
|
-
const
|
|
128
|
+
const obs = { ...diff, scenarioNo, finalMessage: meta.finalMessage, urlChecks };
|
|
129
|
+
|
|
130
|
+
// Integrity units (s10/11/12): transcript tool events + the seed capture feed
|
|
131
|
+
// the I/J checkers. A missing/unshaped seed-state.json exits nonzero here —
|
|
132
|
+
// fail-closed (inconclusive), never vacuous or guessed (task-05 G4 doctrine).
|
|
133
|
+
if (APPLICABLE[scenarioNo] !== undefined) {
|
|
134
|
+
const seedPath = path.join(process.cwd(), ".bench", "seed-state.json");
|
|
135
|
+
const seed = readJson(seedPath);
|
|
136
|
+
if (!Array.isArray(seed?.rows) || seed.content === null || typeof seed.content !== "object") {
|
|
137
|
+
fail(`seed-state unusable: ${seedPath} must be {rows:[], content:{}}`);
|
|
138
|
+
}
|
|
139
|
+
const sandboxRows = state.post
|
|
140
|
+
.filter((r) => isSandboxPath(r.path))
|
|
141
|
+
.map((r) => ({ path: r.path, locale: String(r.locale ?? "en"), id: String(r.id), description: String(r.description ?? "") }));
|
|
142
|
+
obs.tools = scanToolEvents(transcriptText);
|
|
143
|
+
const integrity = {
|
|
144
|
+
sandboxRows,
|
|
145
|
+
content: state.content,
|
|
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])),
|
|
148
|
+
descByPath: new Map(sandboxRows.map((r) => [r.path, r.description])),
|
|
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)])),
|
|
151
|
+
seedContent: Object.fromEntries(seed.rows.map((r) => [String(r.id), seed.content[String(r.id)] ?? ""])),
|
|
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;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const result = scoreUnit(obs);
|
|
98
173
|
|
|
99
174
|
process.stdout.write(
|
|
100
175
|
`${JSON.stringify({
|
|
@@ -108,6 +183,7 @@ process.stdout.write(
|
|
|
108
183
|
total: result.total,
|
|
109
184
|
applicableWeight: result.applicableWeight,
|
|
110
185
|
notes: result.notes,
|
|
186
|
+
scenarioFile,
|
|
111
187
|
},
|
|
112
188
|
})}\n`,
|
|
113
189
|
);
|
|
@@ -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
|
+
})();
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
# as `bash mutate.sh <brief-file> <worktree> <model>` with cwd = the throwaway
|
|
4
4
|
# launch worktree (reflect.ts contract). The REAL candidate source is a headless
|
|
5
5
|
# `opencode run` child: it reads the reflection brief and proposes exactly one
|
|
6
|
-
# mutation — a new
|
|
6
|
+
# mutation — a new RUN CARD under abathur-notes/ (never a sealed path;
|
|
7
7
|
# kernel.immutableGlobs is enforced independently by the driver's PathPolicy).
|
|
8
|
-
#
|
|
8
|
+
# Run cards are the delivery seam (S4): run-scenario.sh appends them VERBATIM to
|
|
9
|
+
# every scenario brief under a `## Run card` heading, so a candidate tree's
|
|
10
|
+
# mutation genuinely steers the benched agent and score deltas are attributable.
|
|
11
|
+
# The wrapper deterministically packages the model's run card as a unified CREATE
|
|
9
12
|
# diff (creation diffs have no context to drift) and appends a genome.jsonc
|
|
10
13
|
# create-diff carrying the resolved spec bytes (plan 184: every sealed tree
|
|
11
14
|
# must contain genome.jsonc so bundle export self-describes; plan 181 requires
|
|
@@ -21,7 +24,7 @@ cp -f "$brief_file" "${ABATHUR_MUTATOR_RAW:-/tmp/abathur-mutate-raw.jsonl}.brief
|
|
|
21
24
|
[ -f "$canonical" ] || { echo "mutate: missing canonical spec $canonical" >&2; exit 1; }
|
|
22
25
|
bin="${ABATHUR_OPENCODE_BIN:-opencode}"
|
|
23
26
|
|
|
24
|
-
prompt="You are the genome mutator of the Abathur evolution harness. Read the reflection brief below (evidence from the incumbent bench of the historian wiki-skill genome). Propose exactly ONE improvement the genome owner can act on: create a NEW
|
|
27
|
+
prompt="You are the genome mutator of the Abathur evolution harness. Read the reflection brief below (evidence from the incumbent bench of the historian wiki-skill genome). Propose exactly ONE improvement the genome owner can act on: create a NEW RUN CARD under the path prefix abathur-notes/ (slug .md). A run card is the genome's mutation delivery channel: at bench time run-scenario.sh appends abathur-notes/*.md VERBATIM to every scenario brief, fenced under a \`## Run card\` heading, so the bench agent reads your card as part of its own instructions — write direct, actionable guidance the historian agent can follow, not a description of the change. Output ONLY a single JSON object, no prose, shape:
|
|
25
28
|
{\"rationale\": \"<one sentence, <=400 chars>\", \"path\": \"abathur-notes/<slug>.md\", \"content\": \"<full markdown file contents>\"}
|
|
26
29
|
Do not modify any other path. Scenarios, rubric.md, seed_sandbox.sh, baseline/ and README.md are immutable.
|
|
27
30
|
|
|
@@ -33,10 +36,10 @@ raw="${ABATHUR_MUTATOR_RAW:-/tmp/abathur-mutate-raw.jsonl}"
|
|
|
33
36
|
rc=$?
|
|
34
37
|
echo "mutate: opencode rc=$rc raw=$raw" >&2
|
|
35
38
|
|
|
36
|
-
python3 - "$raw" "$canonical" "$rc" <<'PY'
|
|
37
|
-
import json, re, sys
|
|
39
|
+
python3 - "$raw" "$canonical" "$rc" "$worktree" <<'PY'
|
|
40
|
+
import json, os, re, sys
|
|
38
41
|
|
|
39
|
-
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]
|
|
40
43
|
|
|
41
44
|
def create_diff(path, text):
|
|
42
45
|
lines = text.split("\n")
|
|
@@ -70,19 +73,55 @@ for text in reversed(last_text_events(raw)):
|
|
|
70
73
|
fence = re.search(r"```(?:json)?\s*(\{.*\})\s*```", candidate, re.S)
|
|
71
74
|
if fence:
|
|
72
75
|
candidate = fence.group(1)
|
|
73
|
-
|
|
74
|
-
if
|
|
76
|
+
end = candidate.rfind("}")
|
|
77
|
+
if end < 0:
|
|
75
78
|
continue
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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:
|
|
81
93
|
proposal = doc
|
|
82
94
|
break
|
|
83
95
|
|
|
84
96
|
spec_text = open(canonical_path, encoding="utf-8").read()
|
|
85
|
-
|
|
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
|
+
)
|
|
86
125
|
|
|
87
126
|
if isinstance(proposal, dict):
|
|
88
127
|
path = proposal.get("path")
|
|
@@ -100,15 +139,19 @@ if isinstance(proposal, dict):
|
|
|
100
139
|
content = content.replace("\r", "")
|
|
101
140
|
if not content.endswith("\n"):
|
|
102
141
|
content += "\n"
|
|
103
|
-
|
|
142
|
+
diffs = [create_diff(path, content)] + ([packaging] if packaging else [])
|
|
143
|
+
candidate = {"id": "mutate-live", "rationale": rationale, "diffs": diffs}
|
|
104
144
|
print(json.dumps({"candidates": [candidate]}))
|
|
105
145
|
sys.exit(0)
|
|
106
146
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
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": []}))
|
|
114
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
|
|
@@ -6,9 +6,20 @@
|
|
|
6
6
|
# "<scenario Brief>"` — the Brief section of the scenario file is the prompt.
|
|
7
7
|
# --auto auto-approves the plugin tools (the wiki sandbox is the blast radius;
|
|
8
8
|
# kernel seals + _sandbox-only writes bound it, plan todo 14).
|
|
9
|
+
#
|
|
10
|
+
# S4 engine seam — RUN CARDS: the bench's ACTIVE TREE may carry operator/
|
|
11
|
+
# mutator run cards under abathur-notes/*.md (the genome's entire mutation
|
|
12
|
+
# space). When present they are appended VERBATIM to the brief fenced under a
|
|
13
|
+
# `## Run card` heading, so candidate mutations actually reach the evaluated
|
|
14
|
+
# agent (the severed-channel fix, swarm-A finding #1). repoRoot comes from the
|
|
15
|
+
# OPTIONAL 3rd argv, else it is derived from the scenario path: runCommand
|
|
16
|
+
# renders `{repoRoot}/{unit.path}` with units under scenarios/, so
|
|
17
|
+
# repoRoot = dirname(dirname(scenario_file)). With no notes the brief — and
|
|
18
|
+
# therefore the transcript — stays byte-identical to the pre-seam script
|
|
19
|
+
# (F1 invariant: incumbent runs must never grow a run-card section).
|
|
9
20
|
set -euo pipefail
|
|
10
|
-
unit_id="${1:?usage: run-scenario.sh <unitId> <scenario-file>}"
|
|
11
|
-
scenario_file="${2:?usage: run-scenario.sh <unitId> <scenario-file>}"
|
|
21
|
+
unit_id="${1:?usage: run-scenario.sh <unitId> <scenario-file> [repoRoot]}"
|
|
22
|
+
scenario_file="${2:?usage: run-scenario.sh <unitId> <scenario-file> [repoRoot]}"
|
|
12
23
|
[ -f "$scenario_file" ] || { echo "run-scenario: missing scenario file $scenario_file" >&2; exit 2; }
|
|
13
24
|
[ -n "${ABATHUR_TRANSCRIPT:-}" ] || { echo "run-scenario: ABATHUR_TRANSCRIPT not set" >&2; exit 2; }
|
|
14
25
|
key="${ABATHUR_WIKI_KEY_FILE:-}"
|
|
@@ -19,9 +30,41 @@ fi
|
|
|
19
30
|
bin="${opencodeBin:-opencode}"
|
|
20
31
|
brief="$(awk '/^##[[:space:]]*Brief/{f=1;next} f && /^## /{exit} f' "$scenario_file")"
|
|
21
32
|
[ -n "$brief" ] || { echo "run-scenario: empty Brief section in $scenario_file" >&2; exit 2; }
|
|
33
|
+
repo_root="${3:-$(dirname "$(dirname "$scenario_file")")}"
|
|
34
|
+
notes_dir="$repo_root/abathur-notes"
|
|
35
|
+
if [ -d "$notes_dir" ]; then
|
|
36
|
+
notes=()
|
|
37
|
+
while IFS= read -r -d '' note; do notes+=("$note"); done \
|
|
38
|
+
< <(find "$notes_dir" -maxdepth 1 -type f -name '*.md' -print0 | LC_ALL=C sort -z)
|
|
39
|
+
if [ "${#notes[@]}" -gt 0 ]; then
|
|
40
|
+
card=""
|
|
41
|
+
card_sep=""
|
|
42
|
+
for note in "${notes[@]}"; do
|
|
43
|
+
card+="${card_sep}$(cat "$note")"
|
|
44
|
+
card_sep=$'\n\n'
|
|
45
|
+
done
|
|
46
|
+
brief="$brief"$'\n\n## Run card\n\n'"$card"
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
22
49
|
mkdir -p "$(dirname "$ABATHUR_TRANSCRIPT")"
|
|
23
50
|
status=0
|
|
24
|
-
|
|
51
|
+
# Model pinning: the transcript carries no model identity and benchProvenance
|
|
52
|
+
# only copies spec.bench.agentModel (declaration-vs-declaration), so the run
|
|
53
|
+
# MUST execute the claimed model or A/B honesty dies to provider-default drift.
|
|
54
|
+
# The engine already exports ABATHUR_AGENT_MODEL in the unit sandbox env
|
|
55
|
+
# (fixture.ts sandboxEnv); unset/empty keeps the argv byte-identical (F1).
|
|
56
|
+
model_args=()
|
|
57
|
+
if [ -n "${ABATHUR_AGENT_MODEL:-}" ]; then
|
|
58
|
+
model_args=(--model "$ABATHUR_AGENT_MODEL")
|
|
59
|
+
fi
|
|
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
|
|
25
68
|
python3 - "$ABATHUR_TRANSCRIPT" "$unit_id" "$status" <<'PY'
|
|
26
69
|
import json, sys
|
|
27
70
|
path, unit, status = sys.argv[1], sys.argv[2], int(sys.argv[3])
|
|
@@ -21,7 +21,7 @@ import json, os, pathlib, sys, urllib.request
|
|
|
21
21
|
base, token = sys.argv[1].rstrip("/"), open(pathlib.Path.home() / ".wikijs-api-key").read().strip()
|
|
22
22
|
req = urllib.request.Request(
|
|
23
23
|
base + "/graphql",
|
|
24
|
-
data=json.dumps({"query": "{ pages { list { id path locale updatedAt } } }"}).encode(),
|
|
24
|
+
data=json.dumps({"query": "{ pages { list { id path locale updatedAt description } } }"}).encode(),
|
|
25
25
|
headers={"Content-Type": "application/json", "Authorization": "Bearer " + token},
|
|
26
26
|
)
|
|
27
27
|
doc = json.load(urllib.request.urlopen(req, timeout=20))
|
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
|