@tachikomagundam/abathur 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,212 @@
1
+ // re-adjudicate (core) tests. The mechanism under test: replay the CURRENT gate over
2
+ // archived generation rows and append a corrected verdict with provenance, files immutable.
3
+ //
4
+ // Coverage: verdict flip culled->nominated (synthetic), honest no-flip (culled stays
5
+ // culled), idempotence after append, promote-row refusal, missing-incumbent refusal,
6
+ // corrupt-archive notice (never a crash), and the historical anchor: a sandboxed COPY of
7
+ // the real historian archives must reproduce the c17 re-adjudication (NOMINATED,
8
+ // gain 0.2917) that the driver computed offline on 2026-09-26 — the engine's own replay
9
+ // of the incident that born this command.
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, copyFileSync } from "node:fs";
13
+ import { mkdtempSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import path from "node:path";
16
+ import { readjudicateGeneration } from "../core/readjudicate.js";
17
+ import { ledgerRecordSchema, LEDGER_KIND_GENERATION_COMPLETE, LEDGER_KIND_PROMOTE } from "../core/ledger.js";
18
+ import { decodeGenerationRecord } from "../core/evolve/run-rows.js";
19
+ const STATS = { halfWidth: 0.15, minEffect: 0.1, nReps: { initial: 2, max: 3 } };
20
+ const BUDGET = { maxCandidates: 1, maxModelCalls: 96, maxTokens: 25_000_000, maxWallS: 26_000 };
21
+ function entryFor(repo) {
22
+ return {
23
+ fingerprint: "f".repeat(16),
24
+ label: "test-genome",
25
+ registryFile: "/dev/null",
26
+ storedText: "",
27
+ spec: {
28
+ label: "test-genome",
29
+ repoPath: repo,
30
+ bench: {
31
+ type: "opencode-fixture-scenarios",
32
+ units: [
33
+ { id: "scenario-19", split: "train", path: "scenarios/19.md" },
34
+ { id: "scenario-13", split: "val", path: "scenarios/13.md" },
35
+ ],
36
+ runCommand: "true",
37
+ graderCommand: "true",
38
+ timeoutS: 1800,
39
+ stats: STATS,
40
+ },
41
+ budget: BUDGET,
42
+ kernel: { immutableGlobs: ["scenarios/**"] },
43
+ },
44
+ };
45
+ }
46
+ function row(o) {
47
+ const data = {
48
+ source: o.source,
49
+ headCommit: "1".repeat(40),
50
+ complete: true,
51
+ reps: o.units[0]?.scores.length ?? 0,
52
+ units: o.units.map((u) => ({ unitId: u.unitId, split: u.split, scores: u.scores, runIds: u.scores.map((_, i) => `r-${u.unitId}-${String(i)}`), failures: u.scores.filter((s) => s < 1).map((s) => `${u.unitId}: scored ${String(s)}`) })),
53
+ counters: { candidates: o.source === "candidate" ? 1 : 0, modelCalls: 10, tokens: 1000, wallS: 100 },
54
+ manifest: [],
55
+ benchProvenance: { benchType: "opencode-fixture-scenarios", versions: [{ bin: "opencode", version: "test" }] },
56
+ };
57
+ if (o.source === "candidate") {
58
+ data.candidateId = "mutate-live";
59
+ data.commitSha = "2".repeat(40);
60
+ data.treeSha = o.treeSha ?? "3".repeat(64);
61
+ }
62
+ if (o.verdict !== undefined)
63
+ data.verdict = o.verdict;
64
+ if (o.gain !== undefined)
65
+ data.gain = o.gain;
66
+ return `${JSON.stringify({ v: 1, ts: o.ts, kind: LEDGER_KIND_GENERATION_COMPLETE, genId: o.genId, runId: o.runId, data })}\n`;
67
+ }
68
+ function sandbox(t) {
69
+ const dir = mkdtempSync(path.join(tmpdir(), "readjud-"));
70
+ const state = path.join(dir, ".state", "abathur");
71
+ mkdirSync(state, { recursive: true });
72
+ writeFileSync(path.join(state, "ledger.jsonl"), "");
73
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
74
+ return { dir, state };
75
+ }
76
+ function cfg(t) {
77
+ const d = mkdtempSync(path.join(tmpdir(), "readjud-cfg-"));
78
+ t.after(() => rmSync(d, { recursive: true, force: true }));
79
+ return d;
80
+ }
81
+ function lastRows(stateDir) {
82
+ return readFileSync(path.join(stateDir, "ledger.jsonl"), "utf8")
83
+ .split("\n")
84
+ .filter((l) => l.trim().length > 0)
85
+ .map((l) => {
86
+ const parsed = ledgerRecordSchema.parse(JSON.parse(l));
87
+ return parsed;
88
+ })
89
+ .filter((r) => r.kind === LEDGER_KIND_GENERATION_COMPLETE)
90
+ .map((r) => {
91
+ try {
92
+ return decodeGenerationRecord(r);
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ })
98
+ .filter((d) => d !== null);
99
+ }
100
+ test("flip: culled row re-judged nominated by the current gate; corrected row appended with provenance", (t) => {
101
+ const { dir, state } = sandbox(t);
102
+ // one rotated archive with the whole epoch (activation law: bank needs an archive)
103
+ const lines = [
104
+ // historical incumbent groups that price a small-ish sigma on both units
105
+ row({ genId: "g-old-a", runId: "r-old", ts: "2026-09-20T00:00:00.000Z", source: "incumbent", units: [{ unitId: "scenario-19", split: "train", scores: [0.875, 1, 0.875, 1] }, { unitId: "scenario-13", split: "val", scores: [1, 1, 1, 1] }] }),
106
+ // the epoch under re-adjudication
107
+ row({ genId: "g-epoch", runId: "run-1", ts: "2026-09-21T00:00:00.000Z", source: "incumbent", units: [{ unitId: "scenario-19", split: "train", scores: [0.75, 0.875] }, { unitId: "scenario-13", split: "val", scores: [1, 1] }], verdict: undefined }),
108
+ row({ genId: "g-epoch-cand", runId: "run-1", ts: "2026-09-21T01:00:00.000Z", source: "candidate", units: [{ unitId: "scenario-19", split: "train", scores: [1, 1] }, { unitId: "scenario-13", split: "val", scores: [1, 1] }], verdict: "culled", gain: 0.1 }),
109
+ ];
110
+ writeFileSync(path.join(state, "ledger.campaign1-2026-09-21.jsonl"), lines.join(""));
111
+ const out = readjudicateGeneration({ entry: entryFor(dir), configDir: cfg(t), genId: "g-epoch-cand", now: () => new Date("2026-09-22T00:00:00Z") });
112
+ assert.equal(out.appended, true, out.lines.join("\n"));
113
+ assert.equal(out.verdict, "nominated", out.lines.join("\n"));
114
+ const appended = lastRows(state).at(-1);
115
+ assert.ok(appended !== undefined);
116
+ assert.equal(appended.verdict, "nominated");
117
+ assert.ok(appended.readjudication !== undefined, "provenance must be present");
118
+ assert.equal(appended.readjudication.replacedVerdict, "culled");
119
+ assert.equal(appended.readjudication.gate, "acceptance");
120
+ assert.equal(appended.readjudication.sourceRows.candidate.file, "ledger.campaign1-2026-09-21.jsonl");
121
+ // archive untouched: still exactly 3 rows, original culled row intact
122
+ assert.equal(readFileSync(path.join(state, "ledger.campaign1-2026-09-21.jsonl"), "utf8").trim().split("\n").length, 3);
123
+ // idempotence: second pass sees the corrected last row -> no-op
124
+ const again = readjudicateGeneration({ entry: entryFor(dir), configDir: cfg(t), genId: "g-epoch-cand", now: () => new Date("2026-09-22T00:00:00Z") });
125
+ assert.equal(again.appended, false, again.lines.join("\n"));
126
+ assert.match(again.lines.join("\n"), /no-op/);
127
+ });
128
+ test("honest replay: a weak candidate stays culled — the tool re-judges, it never flips", (t) => {
129
+ const { dir, state } = sandbox(t);
130
+ const lines = [
131
+ row({ genId: "g-old-a", runId: "r-old", ts: "2026-09-20T00:00:00.000Z", source: "incumbent", units: [{ unitId: "scenario-19", split: "train", scores: [1, 1, 1, 1] }, { unitId: "scenario-13", split: "val", scores: [1, 1, 1, 1] }] }),
132
+ row({ genId: "g-epoch", runId: "run-1", ts: "2026-09-21T00:00:00.000Z", source: "incumbent", units: [{ unitId: "scenario-19", split: "train", scores: [1, 1] }, { unitId: "scenario-13", split: "val", scores: [1, 1] }] }),
133
+ row({ genId: "g-epoch-cand", runId: "run-1", ts: "2026-09-21T01:00:00.000Z", source: "candidate", units: [{ unitId: "scenario-19", split: "train", scores: [1, 1] }, { unitId: "scenario-13", split: "val", scores: [1, 1] }], verdict: "culled", gain: 0 }),
134
+ ];
135
+ writeFileSync(path.join(state, "ledger.campaign1-2026-09-21.jsonl"), lines.join(""));
136
+ const out = readjudicateGeneration({ entry: entryFor(dir), configDir: cfg(t), genId: "g-epoch-cand", now: () => new Date() });
137
+ assert.equal(out.verdict, "culled", out.lines.join("\n"));
138
+ assert.equal(out.appended, false, "already-agreeing row must not append");
139
+ });
140
+ test("refusals: promoted gen, missing incumbent, unknown gen — all block before any append", (t) => {
141
+ const { dir, state } = sandbox(t);
142
+ const epoch = [
143
+ row({ genId: "g-epoch", runId: "run-9", ts: "2026-09-21T00:00:00.000Z", source: "incumbent", units: [{ unitId: "scenario-19", split: "train", scores: [0.75, 1] }, { unitId: "scenario-13", split: "val", scores: [1, 1] }] }),
144
+ row({ genId: "g-epoch-cand", runId: "run-9", ts: "2026-09-21T01:00:00.000Z", source: "candidate", units: [{ unitId: "scenario-19", split: "train", scores: [1, 1] }, { unitId: "scenario-13", split: "val", scores: [1, 1] }], verdict: "culled", gain: 0.1 }),
145
+ ];
146
+ writeFileSync(path.join(state, "ledger.campaign1-2026-09-21.jsonl"), epoch.join(""));
147
+ // promoted epoch is untouchable
148
+ const promotedRun = "run-p";
149
+ writeFileSync(path.join(state, "ledger.campaign2-2026-09-22.jsonl"), epoch.join("") + `${JSON.stringify({ v: 1, ts: "2026-09-22T00:00:00.000Z", kind: LEDGER_KIND_PROMOTE, genId: "g-epoch-cand", runId: promotedRun, data: { actor: "cli", genId: "g-epoch-cand", from: null, to: "2".repeat(40) } })}\n`);
150
+ assert.throws(() => readjudicateGeneration({ entry: entryFor(dir), configDir: cfg(t), genId: "g-epoch-cand", now: () => new Date() }), (e) => /already promoted/.test(String(e.message ?? e)));
151
+ // unknown gen
152
+ assert.throws(() => readjudicateGeneration({ entry: entryFor(dir), configDir: cfg(t), genId: "g-nope", now: () => new Date() }), (e) => /no candidate generation_complete row/.test(String(e.message ?? e)));
153
+ // candidate without its incumbent: run-1 incumbent exists here, so cut a state with candidate only
154
+ const { dir: d2, state: s2 } = sandbox(t);
155
+ writeFileSync(path.join(s2, "ledger.campaign1-2026-09-21.jsonl"), row({ genId: "g-solo", runId: "run-s", ts: "2026-09-21T01:00:00.000Z", source: "candidate", units: [{ unitId: "scenario-19", split: "train", scores: [1, 1] }, { unitId: "scenario-13", split: "val", scores: [1, 1] }], verdict: "culled", gain: 0.1 }));
156
+ assert.throws(() => readjudicateGeneration({ entry: entryFor(d2), configDir: cfg(t), genId: "g-solo", now: () => new Date() }), (e) => /no incumbent baseline row/.test(String(e.message ?? e)));
157
+ });
158
+ test("corrupt archive lines are notices, never a crash, and never poison the replay", (t) => {
159
+ const { dir, state } = sandbox(t);
160
+ const good = [
161
+ row({ genId: "g-old-a", runId: "r-old", ts: "2026-09-20T00:00:00.000Z", source: "incumbent", units: [{ unitId: "scenario-19", split: "train", scores: [0.875, 1, 0.875, 1] }, { unitId: "scenario-13", split: "val", scores: [1, 1, 1, 1] }] }),
162
+ row({ genId: "g-epoch", runId: "run-1", ts: "2026-09-21T00:00:00.000Z", source: "incumbent", units: [{ unitId: "scenario-19", split: "train", scores: [0.75, 0.875] }, { unitId: "scenario-13", split: "val", scores: [1, 1] }] }),
163
+ row({ genId: "g-epoch-cand", runId: "run-1", ts: "2026-09-21T01:00:00.000Z", source: "candidate", units: [{ unitId: "scenario-19", split: "train", scores: [1, 1] }, { unitId: "scenario-13", split: "val", scores: [1, 1] }], verdict: "culled", gain: 0.1 }),
164
+ ];
165
+ writeFileSync(path.join(state, "ledger.campaign1-2026-09-21.jsonl"), `not json at all\n${good.join("")}{"v":9,"kind":"broken-schema"}\n`);
166
+ const out = readjudicateGeneration({ entry: entryFor(dir), configDir: cfg(t), genId: "g-epoch-cand", now: () => new Date("2026-09-22T00:00:00Z") });
167
+ assert.equal(out.verdict, "nominated", out.lines.join("\n"));
168
+ assert.ok(out.lines.some((l) => l.includes("not JSON")), "skip notice must surface");
169
+ });
170
+ // ------------------------------------------------------------------ historical anchor
171
+ // The incident replay: c17's real rows, judged first by the silently-downgraded engine
172
+ // (culled), then re-adjudicated offline to NOMINATED gain 0.2917 (start-work L141).
173
+ // A sandboxed copy of the real archives run through this command must reproduce that
174
+ // verdict with the same numbers — the engine re-judging its own history correctly.
175
+ // Skips (never fails) when the historian state files are absent (CI elsewhere).
176
+ test("anchor: real c17 archives replay to NOMINATED 0.2917 (copy, no mutation)", (t) => {
177
+ // located via env, never a hardcoded homedir (release hygiene); skipped wherever the
178
+ // historian repo is not wired — this anchor guards the OPERATOR's machine, CI has no history.
179
+ const hist = process.env["ABATHUR_HISTORIAN_REPO"];
180
+ if (hist === undefined || hist.length === 0) {
181
+ t.skip("ABATHUR_HISTORIAN_REPO unset — anchor needs the real archives");
182
+ return;
183
+ }
184
+ const real = path.join(hist, ".state", "abathur");
185
+ let names;
186
+ try {
187
+ names = readdirSync(real);
188
+ }
189
+ catch {
190
+ t.skip("historian .state/abathur not present on this machine");
191
+ return;
192
+ }
193
+ const archives = names.filter((n) => n.startsWith("ledger.") && n.endsWith(".jsonl"));
194
+ if (!archives.some((n) => n.includes("campaign17"))) {
195
+ t.skip("campaign17 archive not present");
196
+ return;
197
+ }
198
+ const { dir, state } = sandbox(t);
199
+ for (const n of archives)
200
+ copyFileSync(path.join(real, n), path.join(state, n));
201
+ writeFileSync(path.join(state, "ledger.jsonl"), ""); // copy of live would carry future rows — start empty
202
+ // spec: mirror the historian constitution's anchors exactly (stats/budget as registered)
203
+ const spec = entryFor(dir);
204
+ spec.spec.bench.stats = STATS;
205
+ const out = readjudicateGeneration({ entry: spec, configDir: cfg(t), genId: "g-20260925T150918Z-9969e4b6", now: () => new Date("2026-09-28T00:00:00Z") });
206
+ assert.equal(out.verdict, "nominated", out.lines.join("\n"));
207
+ const appended = lastRows(state).at(-1);
208
+ assert.ok(appended?.readjudication !== undefined);
209
+ assert.ok(Math.abs((appended.gain ?? 0) - 0.2916666666666667) < 1e-9, `gain ${String(appended.gain)} must match the offline replay 0.2917`);
210
+ assert.equal(appended.readjudication.replacedVerdict, "culled");
211
+ assert.equal(appended.readjudication.sourceRows.candidate.file, "ledger.campaign17-2026-09-25.jsonl");
212
+ });
@@ -130,7 +130,7 @@ async function sessionFixture(t) {
130
130
  // plant the canary in the VAL unit and commit it, so it rides into worktrees
131
131
  const sub = path.join(repo, "units", "sub.mjs");
132
132
  writeFileSync(sub, `${readFileSync(sub, "utf8")}\n// ${CANARY}\n`, "utf8");
133
- await gitIn(repo, "-c", "user.name=abathur", "-c", "user.email=abathur@harness.local", "commit", "-aqm", "plant val canary");
133
+ await gitIn(repo, "-c", "user.name=abathur", "-c", "user.email=agent@host.example", "commit", "-aqm", "plant val canary");
134
134
  const spec = loadGenomeSpecFile(path.join(repo, "genome.jsonc"));
135
135
  const env = {
136
136
  XDG_CACHE_HOME: path.join(root, "xdg-cache"),
@@ -27,6 +27,7 @@ const GUARDED_FILES = [
27
27
  .map((f) => path.join(SRC, "commands", f)),
28
28
  path.join(SRC, "core", "promote.ts"),
29
29
  path.join(SRC, "core", "tombstone.ts"),
30
+ path.join(SRC, "core", "retract.ts"),
30
31
  ];
31
32
  function collectHits(file) {
32
33
  const hits = [];
@@ -67,6 +67,12 @@ test("tombstone with env-literal repoPath: exported ⇒ blocked, no literal leak
67
67
  assert.equal(r.status, 1, `stdout: ${r.stdout}\nstderr: ${r.stderr}`);
68
68
  assertNoLiteralOrEnoent(r);
69
69
  });
70
+ test("retract with env-literal repoPath: exported ⇒ blocked naming the RESOLVED repo path", async (t) => {
71
+ const f = await fixture(t, "seam-gate-retract");
72
+ const r = cli(f, ["retract", f.label, "g-nonexistent", "--reason", "seam test"], { [LIT_VAR]: f.repo });
73
+ assert.equal(r.status, 1, `stdout: ${r.stdout}\nstderr: ${r.stderr}`);
74
+ assertNoLiteralOrEnoent(r);
75
+ });
70
76
  test("promote with env-literal repoPath: unset ⇒ clean exit 2 naming the variable", async (t) => {
71
77
  const f = await fixture(t, "seam-gate-promote-unset");
72
78
  const env = {};
@@ -283,8 +283,8 @@ function commitSurgery(fx, message, surgery) {
283
283
  exec("git", ["-C", fx.harness.repo, "reset", "--hard", fx.head]);
284
284
  surgery();
285
285
  const repo = fx.harness.repo;
286
- exec("git", ["-c", "user.name=abathur", "-c", "user.email=abathur@harness.local", "-C", repo, "add", "-A"]);
287
- exec("git", ["-c", "user.name=abathur", "-c", "user.email=abathur@harness.local", "-C", repo, "commit", "-m", message]);
286
+ exec("git", ["-c", "user.name=abathur", "-c", "user.email=agent@host.example", "-C", repo, "add", "-A"]);
287
+ exec("git", ["-c", "user.name=abathur", "-c", "user.email=agent@host.example", "-C", repo, "commit", "-m", message]);
288
288
  return headSha(repo);
289
289
  }
290
290
  function exec(bin, args) {
@@ -32,10 +32,14 @@ import { appendFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFile
32
32
  import os from "node:os";
33
33
  import path from "node:path";
34
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;
35
39
  const DEFAULTS = {
36
- bench: "/home/lab/workspace/harness/historian/.omo/evidence/judge-bench/judge-bench.mjs",
37
- rubrics: ["R1-semantic", "R4-duty-v2", "R5-flavor"].map(
38
- (r) => `/home/lab/workspace/harness/historian/.omo/evidence/judge-bench/rubrics/${r}.md`,
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`),
39
43
  ),
40
44
  model: "local-qwen/qwen3.8-flash-next",
41
45
  };
@@ -111,6 +115,7 @@ function foldMajority(row) {
111
115
  const sha256 = (text) => createHash("sha256").update(text).digest("hex");
112
116
 
113
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); }
114
119
  (async () => {
115
120
  try {
116
121
  const work = mkdtempSync(path.join(os.tmpdir(), "s17-judge-"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tachikomagundam/abathur",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Evolution harness: observe failures, mutate, re-bench, select — human-gated promotion, offline lineage bundles.",
5
5
  "author": "TachikomaGundam",
6
6
  "type": "module",
@@ -9,7 +9,7 @@ top-level command (`genome`, `run`, `status`, `bundle`, `graft`, `self-eval`,
9
9
  with `command` set to the first word and `extra` set to the remaining tokens,
10
10
  then report the CLI exit code (0 ok / 1 blocked decision / 2 cannot-answer)
11
11
  and the relevant lines of its output. If no request was given, call the tool
12
- with `command: "--help"` and summarize the command list. `promote` and
13
- `tombstone` cannot be called through the tool at all — the tool refuses them.
12
+ with `command: "--help"` and summarize the command list. `promote`,
13
+ `tombstone` and `retract` cannot be called through the tool at all — the tool refuses them.
14
14
  They are human gates that belong to a terminal: if the user asks for one,
15
- tell them to run `abathur promote …` / `abathur tombstone …` there.
15
+ tell them to run `abathur promote …` / `abathur tombstone …` / `abathur retract …` there.
package/plugin/abathur.ts CHANGED
@@ -1,4 +1,4 @@
1
- // abathur-opencode-plugin v0.2.6
1
+ // abathur-opencode-plugin v0.2.7
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
@@ -29,7 +29,7 @@ const TIMEOUT_MS = 120_000;
29
29
  const MAX_OUTPUT_BYTES = 64 * 1024;
30
30
 
31
31
  /** The seven tool-reachable top-level commands plus --help. Anything else is refused locally.
32
- * promote/tombstone are human gates and deliberately terminal-only (0.2.1): not reachable here. */
32
+ * promote/tombstone/retract are human gates and deliberately terminal-only (0.2.1/0.2.7): not reachable here. */
33
33
  const ALLOWED_COMMANDS: readonly string[] = [
34
34
  "genome",
35
35
  "run",
@@ -133,10 +133,10 @@ top-level command (\`genome\`, \`run\`, \`status\`, \`bundle\`, \`graft\`, \`sel
133
133
  with \`command\` set to the first word and \`extra\` set to the remaining tokens,
134
134
  then report the CLI exit code (0 ok / 1 blocked decision / 2 cannot-answer)
135
135
  and the relevant lines of its output. If no request was given, call the tool
136
- with \`command: "--help"\` and summarize the command list. \`promote\` and
137
- \`tombstone\` cannot be called through the tool at all — the tool refuses them.
136
+ with \`command: "--help"\` and summarize the command list. \`promote\`,
137
+ \`tombstone\` and \`retract\` cannot be called through the tool at all — the tool refuses them.
138
138
  They are human gates that belong to a terminal: if the user asks for one,
139
- tell them to run \`abathur promote …\` / \`abathur tombstone …\` there.
139
+ tell them to run \`abathur promote …\` / \`abathur tombstone …\` / \`abathur retract …\` there.
140
140
  `;
141
141
 
142
142
  export default {
@@ -168,7 +168,7 @@ export default {
168
168
  "(0 ok, 1 blocked decision, 2 cannot-answer). Honest privilege note: this " +
169
169
  "tool carries bash-equivalent privilege — `run` and `genome` legitimately " +
170
170
  "spawn mutator/engine binaries by design — so the allowlist limits typos " +
171
- "and UX, not capability. `promote` and `tombstone` are deliberately NOT " +
171
+ "and UX, not capability. `promote`, `tombstone` and `retract` are deliberately NOT " +
172
172
  "reachable here: they are human gates, run in a terminal.",
173
173
  args: {
174
174
  command: tool.schema