@tachikomagundam/abathur 0.2.5 → 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.
Files changed (45) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.js +4 -0
  3. package/dist/commands/genome.js +2 -2
  4. package/dist/commands/graft.js +1 -1
  5. package/dist/commands/readjudicate.js +28 -0
  6. package/dist/commands/retract.js +53 -0
  7. package/dist/commands/run.js +1 -1
  8. package/dist/commands/self-eval.js +1 -1
  9. package/dist/commands/tombstone.js +30 -16
  10. package/dist/core/evolve/run-loop.js +14 -0
  11. package/dist/core/evolve/run-rows.js +14 -0
  12. package/dist/core/evolve/score-bank.js +214 -0
  13. package/dist/core/graft-rebench.js +3 -0
  14. package/dist/core/ledger.js +8 -1
  15. package/dist/core/promote.js +63 -47
  16. package/dist/core/readjudicate.js +247 -0
  17. package/dist/core/retract.js +166 -0
  18. package/dist/core/stats-math.js +39 -0
  19. package/dist/core/stats.js +93 -19
  20. package/dist/core/worktree.js +1 -1
  21. package/dist/genomes/toy-smoke/init.mjs +1 -1
  22. package/dist/test/bundle.test.js +1 -1
  23. package/dist/test/fixture-loop.test.js +1 -1
  24. package/dist/test/fixtures-self.js +1 -1
  25. package/dist/test/fixtures-wt.js +1 -1
  26. package/dist/test/friction.test.js +3 -3
  27. package/dist/test/graft.test.js +1 -1
  28. package/dist/test/historian-grader-integrity.test.js +509 -2
  29. package/dist/test/promote.test.js +222 -5
  30. package/dist/test/readjudicate.test.js +212 -0
  31. package/dist/test/reflect.test.js +1 -1
  32. package/dist/test/repopath-seams.test.js +55 -0
  33. package/dist/test/score-bank.test.js +235 -0
  34. package/dist/test/seam-gates.test.js +84 -0
  35. package/dist/test/self-snapshot.test.js +2 -2
  36. package/dist/test/stats-acceptance.test.js +329 -0
  37. package/graders/historian/grader-core.d.mts +27 -0
  38. package/graders/historian/grader-core.mjs +311 -19
  39. package/graders/historian/grader.mjs +5 -5
  40. package/graders/historian/judge-poststage-19.mjs +212 -0
  41. package/graders/historian/run-scenario-19.sh +43 -0
  42. package/graders/historian/run-scenario.sh +16 -0
  43. package/package.json +1 -1
  44. package/plugin/abathur-command.md +3 -3
  45. package/plugin/abathur.ts +6 -6
@@ -11,13 +11,13 @@
11
11
  // real seals via todo-3 worktree APIs, commands driven through the real CLI.
12
12
  import assert from "node:assert/strict";
13
13
  import { spawnSync } from "node:child_process";
14
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
14
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, chmodSync } from "node:fs";
15
15
  import { mkdtemp } from "node:fs/promises";
16
16
  import * as os from "node:os";
17
17
  import path from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import test, {} from "node:test";
20
- import { Ledger } from "../core/ledger.js";
20
+ import { acquireGenomeLock, acquireLock, Ledger } from "../core/ledger.js";
21
21
  import { fingerprint, genId } from "../core/ids.js";
22
22
  import { newGeneration, openGenome, sealGeneration } from "../core/worktree.js";
23
23
  import { manifestPathFor, readManifestFile } from "../core/kernel.js";
@@ -315,6 +315,196 @@ test("tombstone fail-closed + prompt_injection hygiene", async (t) => {
315
315
  const tombPromoted = cli(f, "tombstone", "toy-smoke", promoted.genId, "--reason", "too late");
316
316
  assert.equal(tombPromoted.status, 1, "promoted gens cannot be tombstoned");
317
317
  });
318
+ // -------------------------------------------------------------------- retract
319
+ test("AC(r1): retract undoes a fresh promote — ref deleted, manifest cleared, ledger append-only", async (t) => {
320
+ const f = await gateFixture(t);
321
+ const gen = await handSeal(f, "retract-happy", CLEAN_MUTATION, "nominated");
322
+ assert.equal(cli(f, "promote", "toy-smoke", gen.genId).status, 0);
323
+ assert.equal(incumbentSha(f), gen.commitSha);
324
+ const manifestFile = manifestPathFor(f.configDir, f.entry.fingerprint);
325
+ assert.ok(existsSync(manifestFile), "manifest exists after promote");
326
+ const before = readFileSync(path.join(f.repo, ".state", "abathur", "ledger.jsonl"), "utf8");
327
+ const run = cli(f, "retract", "toy-smoke", gen.genId, "--reason", "zero-gain change, human-ordered rollback");
328
+ assert.equal(run.status, 0, `retract: ${run.stderr}`);
329
+ assert.equal(incumbentSha(f), null, "incumbent ref deleted");
330
+ assert.ok(!existsSync(manifestFile), "kernel manifest cleared");
331
+ const retractRows = rowsOfKind(f, "retract");
332
+ assert.equal(retractRows.length, 1);
333
+ const data = retractRows[0]?.data;
334
+ assert.equal(data.genId, gen.genId);
335
+ assert.equal(data.retractedTo, gen.commitSha);
336
+ assert.equal(data.reason, "zero-gain change, human-ordered rollback");
337
+ assert.equal(rowsOfKind(f, "promote").length, 1, "promote row remains (history never rewritten)");
338
+ const after = readFileSync(path.join(f.repo, ".state", "abathur", "ledger.jsonl"), "utf8");
339
+ assert.ok(after.startsWith(before), "ledger stays append-only");
340
+ const cat = spawnSync("git", ["-C", f.repo, "cat-file", "-e", `${gen.commitSha}^{commit}`], { encoding: "utf8" });
341
+ assert.equal(cat.status, 0, "candidate commit preserved (retract ≠ delete)");
342
+ const st = cli(f, "status", "toy-smoke");
343
+ assert.equal(st.status, 0);
344
+ assert.match(st.stdout, /incumbent: none/);
345
+ assert.match(st.stdout, /ref is missing/, "status honestly warns about the retracted target");
346
+ });
347
+ test("retract fail-closed: missing reason, unknown gen, never-promoted, double retract", async (t) => {
348
+ const f = await gateFixture(t);
349
+ const gen = await handSeal(f, "retract-guards", CLEAN_MUTATION, "nominated");
350
+ assert.equal(cli(f, "retract", "toy-smoke", gen.genId).status, 2, "--reason is required");
351
+ assert.equal(cli(f, "retract", "toy-smoke", "g-nope", "--reason", "x").status, 1);
352
+ const neverPromoted = cli(f, "retract", "toy-smoke", gen.genId, "--reason", "not promoted yet");
353
+ assert.equal(neverPromoted.status, 1);
354
+ assert.match(neverPromoted.stderr + neverPromoted.stdout, /no promote ledger row/);
355
+ assert.equal(cli(f, "promote", "toy-smoke", gen.genId).status, 0);
356
+ assert.equal(cli(f, "retract", "toy-smoke", gen.genId, "--reason", "undo").status, 0);
357
+ const twice = cli(f, "retract", "toy-smoke", gen.genId, "--reason", "again");
358
+ assert.equal(twice.status, 1);
359
+ assert.match(twice.stderr + twice.stdout, /already retracted/);
360
+ });
361
+ test("retract LIFO + descendant guard: live descendant blocks, released descendant frees", async (t) => {
362
+ const f = await gateFixture(t);
363
+ const genA = await handSeal(f, "retract-A", { file: "units/mul.mjs", append: "\n// A\n" }, "nominated");
364
+ assert.equal(cli(f, "promote", "toy-smoke", genA.genId).status, 0);
365
+ // handSeal forks from the repo HEAD, so fast-forward main onto the new
366
+ // incumbent first — the child must be a descendant of A or its own promote
367
+ // would hit the not-an-ancestor stale_state guard.
368
+ git(f, "merge", "--ff-only", "-q", genA.commitSha);
369
+ const genChild = await handSeal(f, "retract-child", { file: "units/add.mjs", append: "\n// child\n" }, "nominated");
370
+ const blockedByChild = cli(f, "retract", "toy-smoke", genA.genId, "--reason", "too late");
371
+ assert.equal(blockedByChild.status, 1);
372
+ assert.match(blockedByChild.stderr + blockedByChild.stdout, /orphan/);
373
+ assert.equal(incumbentSha(f), genA.commitSha, "refusal leaves the ref where it was");
374
+ assert.equal(cli(f, "promote", "toy-smoke", genChild.genId).status, 0);
375
+ const lifo = cli(f, "retract", "toy-smoke", genA.genId, "--reason", "out of order");
376
+ assert.equal(lifo.status, 1);
377
+ assert.match(lifo.stderr + lifo.stdout, /LIFO/);
378
+ assert.equal(cli(f, "retract", "toy-smoke", genChild.genId, "--reason", "child first").status, 0);
379
+ assert.equal(incumbentSha(f), null);
380
+ // child retracted ⇒ its lineage released ⇒ A retracts without a tombstone
381
+ assert.equal(cli(f, "retract", "toy-smoke", genA.genId, "--reason", "now clear").status, 0);
382
+ assert.equal(incumbentSha(f), null);
383
+ });
384
+ test("retract ref-absent: sole live promote records refState=absent; two live promotes refuse", async (t) => {
385
+ const f = await gateFixture(t);
386
+ const genA = await handSeal(f, "absent-A", CLEAN_MUTATION, "nominated");
387
+ assert.equal(cli(f, "promote", "toy-smoke", genA.genId).status, 0);
388
+ // documented manual-rollback shape: ref deleted out-of-band, sole live promote
389
+ git(f, "update-ref", "-d", `refs/heads/${INCUMBENT_BRANCH}`);
390
+ const ok = cli(f, "retract", "toy-smoke", genA.genId, "--reason", "manual rollback already done");
391
+ assert.equal(ok.status, 0, `retract: ${ok.stderr}`);
392
+ const row = rowsOfKind(f, "retract")[0]?.data;
393
+ assert.equal(row.refState, "absent", "row honestly marks the ref was already gone");
394
+ });
395
+ test("retract ref-absent with two live promotes refuses (no laundering of divergence)", async (t) => {
396
+ const f = await gateFixture(t);
397
+ const genA = await handSeal(f, "absent2-A", { file: "units/mul.mjs", append: "\n// A\n" }, "nominated");
398
+ assert.equal(cli(f, "promote", "toy-smoke", genA.genId).status, 0);
399
+ git(f, "merge", "--ff-only", "-q", genA.commitSha);
400
+ const genB = await handSeal(f, "absent2-B", { file: "units/add.mjs", append: "\n// B\n" }, "nominated");
401
+ assert.equal(cli(f, "promote", "toy-smoke", genB.genId).status, 0);
402
+ git(f, "update-ref", "-d", `refs/heads/${INCUMBENT_BRANCH}`); // out-of-band divergence
403
+ const run = cli(f, "retract", "toy-smoke", genB.genId, "--reason", "try to launder");
404
+ assert.equal(run.status, 1);
405
+ assert.match(run.stderr + run.stdout, /cannot verify what the retract undoes/);
406
+ assert.equal(rowsOfKind(f, "retract").length, 0, "no row written on refusal");
407
+ });
408
+ test("retract refuses while a LINKED worktree has the incumbent branch checked out", async (t) => {
409
+ const f = await gateFixture(t);
410
+ const gen = await handSeal(f, "retract-wt", CLEAN_MUTATION, "nominated");
411
+ assert.equal(cli(f, "promote", "toy-smoke", gen.genId).status, 0);
412
+ const wt2 = path.join(f.root, "wt2");
413
+ git(f, "worktree", "add", "-q", wt2, INCUMBENT_BRANCH);
414
+ const run = cli(f, "retract", "toy-smoke", gen.genId, "--reason", "x");
415
+ assert.equal(run.status, 1);
416
+ assert.match(run.stderr + run.stdout, /checked out/);
417
+ assert.equal(incumbentSha(f), gen.commitSha, "ref untouched while a linked worktree holds the branch");
418
+ });
419
+ test("retract echo hygiene: control bytes never reach stdout or the ledger file", async (t) => {
420
+ const f = await gateFixture(t);
421
+ const gen = await handSeal(f, "retract-evil", CLEAN_MUTATION, "nominated");
422
+ assert.equal(cli(f, "promote", "toy-smoke", gen.genId).status, 0);
423
+ const evil = cli(f, "retract", "toy-smoke", gen.genId, "--reason", "evil\n\u001b[31mred\u001b[0m reason");
424
+ assert.equal(evil.status, 0, `evil reason: ${evil.stderr}`);
425
+ assert.ok(!evil.stdout.includes("\u001b"), "raw ANSI never reaches stdout");
426
+ const raw = readFileSync(path.join(f.repo, ".state", "abathur", "ledger.jsonl"), "utf8");
427
+ assert.ok(!raw.includes("\u001b"), "no raw ESC byte in ledger text");
428
+ const stored = rowsOfKind(f, "retract")[0]?.data;
429
+ assert.equal(stored.reason, "evil\n\u001b[31mred\u001b[0m reason", "ledger stores verbatim (JSON-escaped on disk)");
430
+ });
431
+ test("gate locks: a live genome-lock holder blocks promote with a named pid (exit 2)", async (t) => {
432
+ const f = await gateFixture(t);
433
+ const gen = await handSeal(f, "lock-holder", CLEAN_MUTATION, "nominated");
434
+ const lease = acquireGenomeLock({ ledger: Ledger.open(f.repo), configDir: f.configDir, genomeFp: f.entry.fingerprint });
435
+ t.after(() => lease.release());
436
+ const run = cli(f, "promote", "toy-smoke", gen.genId);
437
+ assert.equal(run.status, 2, `promote while locked: ${run.stderr}`);
438
+ assert.match(run.stderr + run.stdout, /held by live pid/);
439
+ assert.equal(incumbentSha(f), null, "no ref movement while locked out");
440
+ });
441
+ test("gate locks: the repo-scoped gate lock contends across config homes", async (t) => {
442
+ const f = await gateFixture(t);
443
+ const gen = await handSeal(f, "repo-lock", CLEAN_MUTATION, "nominated");
444
+ assert.equal(cli(f, "promote", "toy-smoke", gen.genId).status, 0);
445
+ // a second config home registering the SAME repo (different genome lock, same gate lock)
446
+ const configDir2 = path.join(f.root, "config2");
447
+ mkdirSync(configDir2, { recursive: true });
448
+ writeFileSync(path.join(configDir2, "config.jsonc"), '{ "opencodeBin": null }\n', "utf8");
449
+ const doc = JSON.parse(readFileSync(path.join(f.repo, "genome.jsonc"), "utf8"));
450
+ doc["label"] = "toy-smoke";
451
+ const spec2 = path.join(f.root, "private2.jsonc");
452
+ writeFileSync(spec2, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
453
+ registerGenome(configDir2, spec2);
454
+ const repoLease = acquireLock({ configDir: path.join(f.repo, ".state", "abathur"), key: "gate", label: "gate lock" });
455
+ t.after(() => repoLease.release());
456
+ const run = spawnSync(process.execPath, [CLI, "retract", "toy-smoke", gen.genId, "--reason", "x"], {
457
+ env: { ...process.env, ABATHUR_CONFIG: path.join(configDir2, "config.jsonc"), HOME: path.join(f.root, "home") },
458
+ encoding: "utf8",
459
+ cwd: f.root,
460
+ });
461
+ assert.equal(run.status, 2, `retract via second config while gate-locked: ${run.stderr}`);
462
+ assert.match(run.stderr + run.stdout, /gate lock 'gate' is held by live pid/);
463
+ assert.equal(incumbentSha(f), gen.commitSha, "ref untouched while gate-locked");
464
+ assert.equal(rowsOfKind(f, "retract").length, 0, "no row appended by the locked-out process");
465
+ });
466
+ test("retract refuses when the incumbent ref moved out-of-band after the promote", async (t) => {
467
+ const f = await gateFixture(t);
468
+ const gen = await handSeal(f, "diverge", CLEAN_MUTATION, "nominated");
469
+ assert.equal(cli(f, "promote", "toy-smoke", gen.genId).status, 0);
470
+ git(f, "update-ref", `refs/heads/${INCUMBENT_BRANCH}`, gen.parent); // rewind out-of-band
471
+ const run = cli(f, "retract", "toy-smoke", gen.genId, "--reason", "x");
472
+ assert.equal(run.status, 1);
473
+ assert.match(run.stderr + run.stdout, /state diverged/);
474
+ assert.equal(incumbentSha(f), gen.parent, "the moved ref is NOT deleted");
475
+ });
476
+ test("promote after retract refuses — a retracted gen stays dead (no re-promote lockup)", async (t) => {
477
+ const f = await gateFixture(t);
478
+ const gen = await handSeal(f, "re-promote", CLEAN_MUTATION, "nominated");
479
+ assert.equal(cli(f, "promote", "toy-smoke", gen.genId).status, 0);
480
+ assert.equal(cli(f, "retract", "toy-smoke", gen.genId, "--reason", "undo").status, 0);
481
+ const again = cli(f, "promote", "toy-smoke", gen.genId);
482
+ assert.equal(again.status, 1);
483
+ assert.match(again.stderr + again.stdout, /already promoted/);
484
+ });
485
+ test("retract: a PRUNABLE worktree (deleted directory) does not block the rollback valve", async (t) => {
486
+ const f = await gateFixture(t);
487
+ const gen = await handSeal(f, "retract-prunable", CLEAN_MUTATION, "nominated");
488
+ assert.equal(cli(f, "promote", "toy-smoke", gen.genId).status, 0);
489
+ const wt2 = path.join(f.root, "wt-stale");
490
+ git(f, "worktree", "add", "-q", wt2, INCUMBENT_BRANCH);
491
+ rmSync(wt2, { recursive: true, force: true }); // directory gone, metadata stale ⇒ porcelain marks it prunable
492
+ const list = spawnSync("git", ["-C", f.repo, "worktree", "list", "--porcelain"], { encoding: "utf8" });
493
+ assert.match(list.stdout, /prunable/, "fixture sanity: git itself marks the stale worktree prunable");
494
+ const run = cli(f, "retract", "toy-smoke", gen.genId, "--reason", "stale worktree must not block");
495
+ assert.equal(run.status, 0, `retract: ${run.stderr}`);
496
+ assert.equal(incumbentSha(f), null);
497
+ });
498
+ test("retract refuses while the incumbent branch is checked out", async (t) => {
499
+ const f = await gateFixture(t);
500
+ const gen = await handSeal(f, "retract-checkout", CLEAN_MUTATION, "nominated");
501
+ assert.equal(cli(f, "promote", "toy-smoke", gen.genId).status, 0);
502
+ git(f, "checkout", "-q", INCUMBENT_BRANCH);
503
+ const run = cli(f, "retract", "toy-smoke", gen.genId, "--reason", "x");
504
+ assert.equal(run.status, 1);
505
+ assert.match(run.stderr + run.stdout, /checked out/i);
506
+ assert.equal(incumbentSha(f), gen.commitSha, "ref untouched while checked out");
507
+ });
318
508
  // -------------------------------------------------------------------- status
319
509
  test("AC(d): status renders incumbent, generations, quarantine depth, budget; never fabricates", async (t) => {
320
510
  const f = await gateFixture(t);
@@ -368,7 +558,7 @@ test("genome rm: ledgerless genomes unregister (manifest stays), ledger history
368
558
  assert.equal(empty.status, 0, "no ledger rows ⇒ still removable");
369
559
  });
370
560
  // -------------------------------------------------------------- structural
371
- test("structural: promote core is CLI-only; no auto-promote import path; no --force anywhere", () => {
561
+ test("structural: promote/retract cores are CLI-only; no auto-gate import path; no --force anywhere", () => {
372
562
  const importers = [];
373
563
  const scan = (dir) => {
374
564
  for (const ent of readdirSync(dir, { withFileTypes: true })) {
@@ -386,9 +576,36 @@ test("structural: promote core is CLI-only; no auto-promote import path; no --fo
386
576
  scan(path.join(REPO_ROOT, "src"));
387
577
  const relative = importers.map((p) => path.relative(REPO_ROOT, p).split(path.sep).join("/")).sort();
388
578
  assert.deepEqual(relative, ["src/commands/promote.ts"], "core/promote imported ONLY by commands/promote");
579
+ const retractImporters = [];
580
+ const scanRetract = (dir) => {
581
+ for (const ent of readdirSync(dir, { withFileTypes: true })) {
582
+ const p = path.join(dir, ent.name);
583
+ if (ent.isDirectory()) {
584
+ if (ent.name !== "node_modules")
585
+ scanRetract(p);
586
+ }
587
+ else if (ent.name.endsWith(".ts")) {
588
+ const src = readFileSync(p, "utf8");
589
+ if (/(?:from\s+|import\(\s*)["'](?:[^"']*\/)?core\/retract\.js["']|from\s+["']\.\/retract\.js["']/.test(src))
590
+ retractImporters.push(p);
591
+ }
592
+ }
593
+ };
594
+ scanRetract(path.join(REPO_ROOT, "src"));
595
+ const relRetract = retractImporters.map((p) => path.relative(REPO_ROOT, p).split(path.sep).join("/")).sort();
596
+ assert.deepEqual(relRetract, ["src/commands/retract.ts"], "core/retract imported ONLY by commands/retract");
389
597
  for (const rel of ["src/core/evolve/run-loop.ts", "src/core/evolve/reflect.ts", "src/core/evolve/candidate.ts"]) {
390
598
  assert.doesNotMatch(readFileSync(path.join(REPO_ROOT, rel), "utf8"), /from\s+"[^"]*promote\.js"/, `${rel} must not import promote`);
599
+ assert.doesNotMatch(readFileSync(path.join(REPO_ROOT, rel), "utf8"), /from\s+"[^"]*retract\.js"/, `${rel} must not import retract`);
600
+ }
601
+ for (const gateFile of ["src/core/promote.ts", "src/core/retract.ts"]) {
602
+ assert.doesNotMatch(readFileSync(path.join(REPO_ROOT, gateFile), "utf8"), /--force/, `no force flag in ${gateFile}`);
603
+ }
604
+ // the plugin tool allowlist must never grow a human gate (terminal-only class)
605
+ const pluginSrc = readFileSync(path.join(REPO_ROOT, "plugin", "abathur.ts"), "utf8");
606
+ const allowBlock = pluginSrc.match(/const ALLOWED_COMMANDS[\s\S]*?;/)?.[0] ?? "";
607
+ assert.ok(allowBlock.length > 0, "ALLOWED_COMMANDS block must be found — a vacuous match tests nothing");
608
+ for (const gate of ['"promote"', '"tombstone"', '"retract"']) {
609
+ assert.ok(!allowBlock.includes(gate), `${gate} must NOT be plugin-reachable`);
391
610
  }
392
- const gate = readFileSync(path.join(REPO_ROOT, "src/core/promote.ts"), "utf8");
393
- assert.doesNotMatch(gate, /--force/, "no force flag in the gate");
394
611
  });
@@ -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"),
@@ -0,0 +1,55 @@
1
+ // Tripwire (postmortem 863e6c2): the env-literal repoPath defect class was
2
+ // caused by per-call-site resolution being a CONVENTION — every new FS
3
+ // consumer had to remember to call effectiveRepoPath, and promote/tombstone
4
+ // forgot for 18 days. This test makes the convention mechanical: any line
5
+ // on the command/human-gate surface that touches `spec.repoPath` must either
6
+ // resolve it through effectiveRepoPath(...) on the same line, or carry an
7
+ // inline waiver comment `raw-repoPath: <reason>` documenting WHY the raw
8
+ // stored form is correct there (display strings, self-mode literal
9
+ // detection, post-swap pipeline paths). A new command that forgets now
10
+ // fails CI instead of failing a human gate 18 days later.
11
+ //
12
+ // Scope note: engine-internal pipeline modules (src/core/evolve/**,
13
+ // src/core/graft*.ts, src/core/bundle-*.ts, src/bench/**) operate on
14
+ // candidate-worktree-swapped absolute paths by design (run-loop.ts swaps
15
+ // spec.repoPath before invoking them) and are out of scope here; the ledger
16
+ // FS seam additionally resolves unconditionally in ledgerPath().
17
+ import assert from "node:assert/strict";
18
+ import { readdirSync, readFileSync, statSync } from "node:fs";
19
+ import path from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ import { test } from "node:test";
22
+ // dist/test/*.js -> repo src/ (tests run from the built tree but guard SOURCE).
23
+ const SRC = fileURLToPath(new URL("../../src/", import.meta.url));
24
+ const GUARDED_FILES = [
25
+ ...readdirSync(path.join(SRC, "commands"))
26
+ .filter((f) => f.endsWith(".ts"))
27
+ .map((f) => path.join(SRC, "commands", f)),
28
+ path.join(SRC, "core", "promote.ts"),
29
+ path.join(SRC, "core", "tombstone.ts"),
30
+ path.join(SRC, "core", "retract.ts"),
31
+ ];
32
+ function collectHits(file) {
33
+ const hits = [];
34
+ const lines = readFileSync(file, "utf8").split("\n");
35
+ lines.forEach((line, i) => {
36
+ if (!line.includes("spec.repoPath"))
37
+ return;
38
+ if (line.includes("effectiveRepoPath("))
39
+ return;
40
+ if (line.includes("raw-repoPath:"))
41
+ return;
42
+ hits.push(`${path.relative(SRC, file)}:${String(i + 1)}: ${line.trim()}`);
43
+ });
44
+ return hits;
45
+ }
46
+ test("command/gate surface: every spec.repoPath use resolves via effectiveRepoPath or waives in writing", () => {
47
+ const violations = [];
48
+ for (const file of GUARDED_FILES) {
49
+ if (!statSync(file, { throwIfNoEntry: false }))
50
+ continue;
51
+ violations.push(...collectHits(file));
52
+ }
53
+ assert.deepEqual(violations, [], "raw spec.repoPath uses without effectiveRepoPath() or a `raw-repoPath:` waiver:\n" +
54
+ violations.join("\n"));
55
+ });