@tachikomagundam/abathur 0.1.0

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 (119) hide show
  1. package/.github/workflows/ci.yml +29 -0
  2. package/.github/workflows/publish.yml +74 -0
  3. package/LICENSE +21 -0
  4. package/README.md +461 -0
  5. package/config/abathur.jsonc +17 -0
  6. package/config/genomes/historian.example.jsonc +124 -0
  7. package/dist/bench/adapter.js +201 -0
  8. package/dist/bench/fixture-probe.js +92 -0
  9. package/dist/bench/fixture-support.js +173 -0
  10. package/dist/bench/fixture.js +236 -0
  11. package/dist/bench/toy.js +152 -0
  12. package/dist/cli.js +110 -0
  13. package/dist/commands/bundle.js +79 -0
  14. package/dist/commands/genome.js +94 -0
  15. package/dist/commands/graft.js +71 -0
  16. package/dist/commands/kernel.js +47 -0
  17. package/dist/commands/promote.js +25 -0
  18. package/dist/commands/run.js +145 -0
  19. package/dist/commands/self-eval.js +240 -0
  20. package/dist/commands/status.js +186 -0
  21. package/dist/commands/tombstone.js +72 -0
  22. package/dist/config.js +161 -0
  23. package/dist/core/bundle-common.js +119 -0
  24. package/dist/core/bundle-export.js +212 -0
  25. package/dist/core/bundle-inspect.js +143 -0
  26. package/dist/core/bundle-manifest.js +105 -0
  27. package/dist/core/bundle-mask.js +75 -0
  28. package/dist/core/bundle-tar.js +240 -0
  29. package/dist/core/bundle.js +9 -0
  30. package/dist/core/evolve/brief.js +45 -0
  31. package/dist/core/evolve/candidate.js +140 -0
  32. package/dist/core/evolve/child-track.js +197 -0
  33. package/dist/core/evolve/friction.js +150 -0
  34. package/dist/core/evolve/reflect.js +191 -0
  35. package/dist/core/evolve/run-bench.js +170 -0
  36. package/dist/core/evolve/run-friction.js +63 -0
  37. package/dist/core/evolve/run-loop.js +282 -0
  38. package/dist/core/evolve/run-plan.js +39 -0
  39. package/dist/core/evolve/run-rows.js +145 -0
  40. package/dist/core/evolve/self-overlay.js +213 -0
  41. package/dist/core/evolve/self-snapshot.js +170 -0
  42. package/dist/core/evolve/stub-mutators.mjs +105 -0
  43. package/dist/core/evolve/udiff.js +189 -0
  44. package/dist/core/genome-paths.js +76 -0
  45. package/dist/core/genome.js +176 -0
  46. package/dist/core/glob.js +106 -0
  47. package/dist/core/graft-gates.js +184 -0
  48. package/dist/core/graft-rebench.js +187 -0
  49. package/dist/core/graft-support.js +181 -0
  50. package/dist/core/graft.js +218 -0
  51. package/dist/core/ids.js +154 -0
  52. package/dist/core/incumbent.js +46 -0
  53. package/dist/core/kernel.js +112 -0
  54. package/dist/core/ledger.js +198 -0
  55. package/dist/core/locks.js +172 -0
  56. package/dist/core/promote.js +119 -0
  57. package/dist/core/snapshot.js +61 -0
  58. package/dist/core/spec.js +178 -0
  59. package/dist/core/stats-math.js +102 -0
  60. package/dist/core/stats-pareto.js +57 -0
  61. package/dist/core/stats.js +184 -0
  62. package/dist/core/worktree.js +190 -0
  63. package/dist/exit.js +32 -0
  64. package/dist/genomes/toy-smoke/genome.jsonc +30 -0
  65. package/dist/genomes/toy-smoke/grader.mjs +61 -0
  66. package/dist/genomes/toy-smoke/init.mjs +63 -0
  67. package/dist/genomes/toy-smoke/units/add.mjs +17 -0
  68. package/dist/genomes/toy-smoke/units/explode.mjs +4 -0
  69. package/dist/genomes/toy-smoke/units/hang.mjs +16 -0
  70. package/dist/genomes/toy-smoke/units/mul.mjs +16 -0
  71. package/dist/genomes/toy-smoke/units/mutate.mjs +18 -0
  72. package/dist/genomes/toy-smoke/units/sub.mjs +16 -0
  73. package/dist/jsonc.js +77 -0
  74. package/dist/out.js +5 -0
  75. package/dist/test/bench-adapter.test.js +33 -0
  76. package/dist/test/bench-fixture.test.js +407 -0
  77. package/dist/test/bench-toy.test.js +251 -0
  78. package/dist/test/bundle.test.js +659 -0
  79. package/dist/test/config.test.js +185 -0
  80. package/dist/test/d7-gate.test.js +56 -0
  81. package/dist/test/fixture-loop.test.js +267 -0
  82. package/dist/test/fixtures/friction-writer.js +16 -0
  83. package/dist/test/fixtures-historian.js +82 -0
  84. package/dist/test/fixtures-self.js +143 -0
  85. package/dist/test/fixtures-wt.js +64 -0
  86. package/dist/test/friction.test.js +398 -0
  87. package/dist/test/genome.test.js +453 -0
  88. package/dist/test/git.test.js +69 -0
  89. package/dist/test/graft.test.js +567 -0
  90. package/dist/test/historian-genome.test.js +134 -0
  91. package/dist/test/historian-grader-io.test.js +148 -0
  92. package/dist/test/historian-grader.test.js +209 -0
  93. package/dist/test/ids.test.js +116 -0
  94. package/dist/test/include-val.test.js +120 -0
  95. package/dist/test/ledger-lock.test.js +99 -0
  96. package/dist/test/ledger.test.js +102 -0
  97. package/dist/test/promote.test.js +394 -0
  98. package/dist/test/reflect.test.js +410 -0
  99. package/dist/test/run-loop.test.js +433 -0
  100. package/dist/test/self-snapshot.test.js +328 -0
  101. package/dist/test/snapshot.test.js +86 -0
  102. package/dist/test/stats.test.js +423 -0
  103. package/dist/test/stub-mutators.test.js +17 -0
  104. package/dist/test/testutil.js +30 -0
  105. package/dist/test/worktree.test.js +198 -0
  106. package/dist/util/freeze.js +30 -0
  107. package/dist/util/git.js +85 -0
  108. package/docs/federation.md +184 -0
  109. package/docs/immutable-kernel.md +87 -0
  110. package/graders/historian/grader-core.d.mts +53 -0
  111. package/graders/historian/grader-core.mjs +276 -0
  112. package/graders/historian/grader-support.d.mts +57 -0
  113. package/graders/historian/grader-support.mjs +137 -0
  114. package/graders/historian/grader.mjs +113 -0
  115. package/graders/historian/mutate.sh +114 -0
  116. package/graders/historian/reset-sandbox.sh +60 -0
  117. package/graders/historian/run-scenario.sh +49 -0
  118. package/graders/historian/seed-wrapped.sh +32 -0
  119. package/package.json +42 -0
@@ -0,0 +1,185 @@
1
+ // Todo 1 acceptance pins for src/config.ts (TDD RED first — plan protocol).
2
+ // Given/When/Then throughout; tmpdirs are namespaced per-run via mkdtemp and
3
+ // torn down in t.after so parallel checkouts of this repo never collide.
4
+ import assert from "node:assert/strict";
5
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
6
+ import * as os from "node:os";
7
+ import path from "node:path";
8
+ import { test } from "node:test";
9
+ import { fileURLToPath } from "node:url";
10
+ import { ConfigError, deepMerge, loadConfig, resolveConfigDir, resolveConfigPath, stripJsonc, } from "../config.js";
11
+ /** Create a throwaway HOME whose ~/.config/abathur does not exist yet. */
12
+ async function freshRoot() {
13
+ return mkdtemp(path.join(os.tmpdir(), "abathur-cfg-test-"));
14
+ }
15
+ function userConfigPath(home) {
16
+ return path.join(home, ".config", "abathur", "config.jsonc");
17
+ }
18
+ async function writeJsonc(filePath, text) {
19
+ await mkdir(path.dirname(filePath), { recursive: true });
20
+ await writeFile(filePath, text, "utf8");
21
+ }
22
+ const repoDefault = fileURLToPath(new URL("../../config/abathur.jsonc", import.meta.url));
23
+ // ---------------------------------------------------------------- stripJsonc
24
+ test("stripJsonc: removes line and block comments and trailing commas", () => {
25
+ const src = `{
26
+ // line comment
27
+ "a": 1, /* block
28
+ comment */
29
+ "b": [1, 2, 3,],
30
+ "c": {"d": true,},
31
+ }`;
32
+ const out = stripJsonc(src);
33
+ assert.deepEqual(JSON.parse(out), { a: 1, b: [1, 2, 3], c: { d: true } });
34
+ });
35
+ test("stripJsonc: preserves comment-like text inside strings", () => {
36
+ const src = `{"url": "http://example.com//x", "note": "a /* b */ c"} // real comment`;
37
+ const out = stripJsonc(src);
38
+ assert.deepEqual(JSON.parse(out), {
39
+ url: "http://example.com//x",
40
+ note: "a /* b */ c",
41
+ });
42
+ });
43
+ // ----------------------------------------------------------------- deepMerge
44
+ test("deepMerge: merges nested objects, replaces arrays and scalars, overlay wins", () => {
45
+ const base = { a: { x: 1, y: 2 }, arr: [1, 2], s: "base", keep: true };
46
+ const overlay = { a: { y: 9, z: 3 }, arr: [9], s: "local" };
47
+ assert.deepEqual(deepMerge(base, overlay), {
48
+ a: { x: 1, y: 9, z: 3 },
49
+ arr: [9],
50
+ s: "local",
51
+ keep: true,
52
+ });
53
+ });
54
+ // ------------------------------------------------- strict schema (AC-2 core)
55
+ test("loadConfig: unknown key is rejected and named in the error", async (t) => {
56
+ const root = await freshRoot();
57
+ t.after(() => rm(root, { recursive: true, force: true }));
58
+ const cfg = path.join(root, "config.jsonc");
59
+ await writeJsonc(cfg, `{ "unknownField": 1 }`);
60
+ let caught;
61
+ try {
62
+ loadConfig({ ABATHUR_CONFIG: cfg, HOME: root });
63
+ }
64
+ catch (error) {
65
+ caught = error;
66
+ }
67
+ assert.ok(caught instanceof ConfigError, `expected ConfigError, got ${String(caught)}`);
68
+ assert.equal(caught.kind, "unknown-key");
69
+ assert.match(caught.message, /unknownField/);
70
+ assert.ok(caught.keys?.includes("unknownField"));
71
+ assert.equal(caught.filePath, cfg);
72
+ });
73
+ test("loadConfig: unknown key inside the local overlay is rejected too", async (t) => {
74
+ const root = await freshRoot();
75
+ t.after(() => rm(root, { recursive: true, force: true }));
76
+ const cfg = path.join(root, "config.jsonc");
77
+ await writeJsonc(cfg, `{ "opencodeBin": null }`);
78
+ await writeJsonc(path.join(root, "config.local.jsonc"), `{ "oops": true }`);
79
+ assert.throws(() => loadConfig({ ABATHUR_CONFIG: cfg, HOME: root }), (error) => error instanceof ConfigError &&
80
+ error.kind === "unknown-key" &&
81
+ error.keys?.includes("oops"));
82
+ });
83
+ // ------------------------------------------------------------ overlay merge
84
+ test("loadConfig: *.local.jsonc deep-merges over the base file", async (t) => {
85
+ const root = await freshRoot();
86
+ t.after(() => rm(root, { recursive: true, force: true }));
87
+ const cfg = path.join(root, "config.jsonc");
88
+ await writeJsonc(cfg, `{ "opencodeBin": "/usr/bin/opencode", "stateDir": "/srv/state" }`);
89
+ await writeJsonc(cfg.replace(/\.jsonc$/, ".local.jsonc"), `{ "stateDir": "/scratch/state" }`);
90
+ const loaded = loadConfig({ ABATHUR_CONFIG: cfg, HOME: root });
91
+ assert.equal(loaded.config.opencodeBin, "/usr/bin/opencode"); // base survives
92
+ assert.equal(loaded.config.stateDir, "/scratch/state"); // overlay wins
93
+ assert.equal(loaded.path, cfg);
94
+ assert.equal(loaded.overlayPath, path.join(root, "config.local.jsonc"));
95
+ });
96
+ test("loadConfig: no overlay present leaves overlayPath null", async (t) => {
97
+ const root = await freshRoot();
98
+ t.after(() => rm(root, { recursive: true, force: true }));
99
+ const cfg = path.join(root, "config.jsonc");
100
+ await writeJsonc(cfg, `{ /* no overlay */ }`);
101
+ const loaded = loadConfig({ ABATHUR_CONFIG: cfg, HOME: root });
102
+ assert.equal(loaded.overlayPath, null);
103
+ assert.equal(loaded.config.opencodeBin, null); // schema default applied
104
+ });
105
+ // ------------------------------------------------- fallback resolution order
106
+ test("resolveConfigPath: ABATHUR_CONFIG pointing at a missing file fails closed", async (t) => {
107
+ const root = await freshRoot();
108
+ t.after(() => rm(root, { recursive: true, force: true }));
109
+ const missing = path.join(root, "nope.jsonc");
110
+ assert.throws(() => resolveConfigPath({ ABATHUR_CONFIG: missing, HOME: root }), (error) => error instanceof ConfigError && error.kind === "unreadable" &&
111
+ error.message.includes(missing));
112
+ // loadConfig surfaces the same failure class (unreadable => exit 2 contract)
113
+ assert.throws(() => loadConfig({ ABATHUR_CONFIG: missing, HOME: root }), (error) => error instanceof ConfigError && error.kind === "unreadable");
114
+ });
115
+ test("resolveConfigPath: ~/.config/abathur beats the repo default", async (t) => {
116
+ const root = await freshRoot();
117
+ t.after(() => rm(root, { recursive: true, force: true }));
118
+ const user = userConfigPath(root);
119
+ await writeJsonc(user, `{ "stateDir": "/user/abathur-state" }`);
120
+ assert.equal(resolveConfigPath({ HOME: root }), user);
121
+ const loaded = loadConfig({ HOME: root });
122
+ assert.equal(loaded.path, user);
123
+ assert.equal(loaded.config.stateDir, "/user/abathur-state");
124
+ });
125
+ test("resolveConfigPath: falls back to repo config/abathur.jsonc when nothing else exists", async (t) => {
126
+ const root = await freshRoot();
127
+ t.after(() => rm(root, { recursive: true, force: true }));
128
+ assert.equal(resolveConfigPath({ HOME: root }), repoDefault);
129
+ const loaded = loadConfig({ HOME: root }); // must NOT crash — AC: fallback order
130
+ assert.equal(loaded.path, repoDefault);
131
+ assert.equal(loaded.config.opencodeBin, null);
132
+ });
133
+ test("resolveConfigDir: tracks the winner, canonical home when nothing exists", async (t) => {
134
+ const root = await freshRoot();
135
+ t.after(() => rm(root, { recursive: true, force: true }));
136
+ const cfg = path.join(root, "custom", "abathur.jsonc");
137
+ await writeJsonc(cfg, `{}`);
138
+ assert.equal(resolveConfigDir({ ABATHUR_CONFIG: cfg, HOME: root }), path.join(root, "custom"));
139
+ const user = userConfigPath(root);
140
+ await writeJsonc(user, `{}`);
141
+ assert.equal(resolveConfigDir({ HOME: root }), path.join(root, ".config", "abathur"));
142
+ const bare = await freshRoot();
143
+ t.after(() => rm(bare, { recursive: true, force: true }));
144
+ // repo default exists but is lower priority: with only HOME+no files, canonical home wins
145
+ assert.equal(resolveConfigDir({ ABATHUR_CONFIG: undefined, HOME: bare }), path.join(bare, ".config", "abathur"));
146
+ });
147
+ // ------------------------------------------------------------------ malformed
148
+ test("loadConfig: broken syntax yields kind=malformed with the file path, not a raw crash", async (t) => {
149
+ const root = await freshRoot();
150
+ t.after(() => rm(root, { recursive: true, force: true }));
151
+ const cfg = path.join(root, "config.jsonc");
152
+ await writeJsonc(cfg, `{ "opencodeBin": }`); // JSON syntax error
153
+ let caught;
154
+ try {
155
+ loadConfig({ ABATHUR_CONFIG: cfg, HOME: root });
156
+ }
157
+ catch (error) {
158
+ caught = error;
159
+ }
160
+ assert.ok(caught instanceof ConfigError);
161
+ assert.equal(caught.kind, "malformed");
162
+ assert.ok(caught.message.includes(cfg), "error message must name the offending file");
163
+ });
164
+ test("loadConfig: wrong leaf type is kind=invalid-value", async (t) => {
165
+ const root = await freshRoot();
166
+ t.after(() => rm(root, { recursive: true, force: true }));
167
+ const cfg = path.join(root, "config.jsonc");
168
+ await writeJsonc(cfg, `{ "opencodeBin": 42 }`);
169
+ assert.throws(() => loadConfig({ ABATHUR_CONFIG: cfg, HOME: root }), (error) => error instanceof ConfigError && error.kind === "invalid-value");
170
+ });
171
+ // --------------------------------------------------------- readable artefacts
172
+ test("loadConfig: full happy path reads JSONC comments and overlay from disk", async (t) => {
173
+ const root = await freshRoot();
174
+ t.after(() => rm(root, { recursive: true, force: true }));
175
+ const cfg = path.join(root, "config.jsonc");
176
+ await writeJsonc(cfg, `{
177
+ // machine values
178
+ "opencodeBin": "/opt/opencode/bin/opencode",
179
+ "stateDir": null, /* keep default home */
180
+ }`);
181
+ const loaded = loadConfig({ ABATHUR_CONFIG: cfg, HOME: root });
182
+ assert.equal(loaded.config.opencodeBin, "/opt/opencode/bin/opencode");
183
+ const rawOverlay = await readFile(cfg, "utf8"); // sanity: fixture really on disk
184
+ assert.ok(rawOverlay.includes("//"));
185
+ });
@@ -0,0 +1,56 @@
1
+ // D7 grep gate (plan success criterion 7, line 217): shipped sources under src/
2
+ // must never carry machine-bound absolute paths or vendor-specific literals.
3
+ // The README's manual `grep -rnE ... src/` review is not enforcement — this test
4
+ // IS the CI-style gate: it walks every TypeScript source file under src/, minus
5
+ // the src/test/** carve-out the plan grants (fixtures legitimately name such
6
+ // strings), and asserts each file contains neither "/home/lab" nor a
7
+ // case-sensitive "historian". A violation fails naming EVERY offending file and
8
+ // line. Repo sources resolve relative to this module (promote.test.ts pattern),
9
+ // so the gate is robust to the cwd `node --test` runs from.
10
+ import assert from "node:assert/strict";
11
+ import { readdirSync, readFileSync } from "node:fs";
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import test from "node:test";
15
+ /** src/ of the repo this compiled test belongs to: dist/test/*.js → ../../src. */
16
+ const SRC_DIR = fileURLToPath(new URL("../../src", import.meta.url));
17
+ /** The plan's carve-out: test sources may contain the very literals they ban. */
18
+ const TEST_DIR = path.join(SRC_DIR, "test");
19
+ const FORBIDDEN = [
20
+ { label: "absolute workspace path", needle: "/home/lab" },
21
+ { label: "vendor literal", needle: "historian" },
22
+ ];
23
+ /** Recursive .ts walk; symlinks are never followed (they could escape src/). */
24
+ function sourceFiles(dir) {
25
+ const found = [];
26
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
27
+ if (entry.isSymbolicLink())
28
+ continue;
29
+ const full = path.join(dir, entry.name);
30
+ if (entry.isDirectory())
31
+ found.push(...sourceFiles(full));
32
+ else if (entry.isFile() && entry.name.endsWith(".ts"))
33
+ found.push(full);
34
+ }
35
+ return found;
36
+ }
37
+ test("D7 gate: src/** outside src/test/** is free of machine-bound literals", () => {
38
+ const violations = [];
39
+ let scanned = 0;
40
+ for (const file of sourceFiles(SRC_DIR)) {
41
+ if (file.startsWith(`${TEST_DIR}${path.sep}`))
42
+ continue;
43
+ scanned += 1;
44
+ const lines = readFileSync(file, "utf8").split("\n");
45
+ for (const [index, line] of lines.entries()) {
46
+ for (const { label, needle } of FORBIDDEN) {
47
+ if (line.includes(needle)) {
48
+ violations.push(`${path.relative(SRC_DIR, file)}:${String(index + 1)} [${label}] ${line.trim()}`);
49
+ }
50
+ }
51
+ }
52
+ }
53
+ // a vacuous pass (bad path resolution) must be as loud as a violation
54
+ assert.ok(scanned > 20, `gate only scanned ${String(scanned)} sources under ${SRC_DIR} — path resolution broken`);
55
+ assert.equal(violations.length, 0, `D7 grep gate violated (${String(violations.length)} hit(s)):\n${violations.join("\n")}`);
56
+ });
@@ -0,0 +1,267 @@
1
+ // F1-fix2 loop-level regression pins for the val-split bench authority split
2
+ // (plan lines 117-124 / 125-132 / SC4). Commit 3c6bbf9 wired `--include-val` at
3
+ // the fixture adapter seam but left benchTarget iterating EVERY unit, so any
4
+ // default `run` on a val-bearing fixture genome crashed mid-bench with
5
+ // ExitSignal(2) "fixture: unit 'v1' is a val-split scenario and requires the
6
+ // operator flag --include-val"
7
+ // at the first val unit — the SC4/T7 flagship flows were unreachable without the
8
+ // flag. Pins here:
9
+ // (1) default run (NO flag) completes: incumbent AND candidate benches carry
10
+ // scored val replicates (the nomination gate's required input);
11
+ // (2) --include-val stays the honest operator EXPOSURE switch: off, every
12
+ // sandbox manifest keeps val ids/paths opaque; on, they surface;
13
+ // (3) toy + flag fail-closed stays pinned in include-val.test.ts / run-loop.test.ts.
14
+ // No model calls anywhere: fake opencodeBin bash stub + node grader table.
15
+ import assert from "node:assert/strict";
16
+ import { spawnSync } from "node:child_process";
17
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
18
+ import { mkdtemp } from "node:fs/promises";
19
+ import * as os from "node:os";
20
+ import path from "node:path";
21
+ import test, {} from "node:test";
22
+ import { registerGenome, requireGenomesByLabel } from "../core/genome.js";
23
+ import { Ledger } from "../core/ledger.js";
24
+ import { decodeGenerationRecord } from "../core/evolve/run-bench.js";
25
+ import { runEvolution } from "../core/evolve/run-loop.js";
26
+ // --------------------------------------------------------------------- stubs
27
+ const VAL_CANARY = "sval-canary-ZZHIDDEN";
28
+ const VAL_PATH = `scenarios/${VAL_CANARY}.md`;
29
+ // Fake opencode: `--version` semver line (probe), `run <path>` scenario echo +
30
+ // final run-metadata JSON line (same contract as bench-fixture.test.ts BIN_GOOD).
31
+ const FAKE_BIN = `#!/usr/bin/env bash
32
+ set -euo pipefail
33
+ if [ "\${1:-}" = "--version" ]; then echo "1.2.3"; exit 0; fi
34
+ if [ -n "\${ABATHUR_TRANSCRIPT:-}" ]; then
35
+ mkdir -p "$(dirname "$ABATHUR_TRANSCRIPT")"
36
+ printf 'ran %s\\n' "\${2:-}" > "$ABATHUR_TRANSCRIPT"
37
+ fi
38
+ echo "ran \${2:-}"
39
+ echo '{"tokensEst":10,"turns":1}'
40
+ `;
41
+ const GRADER = `const [unitId] = process.argv.slice(2);
42
+ const table = {
43
+ t1: { score: 1, pass: true },
44
+ v1: { score: 0.75, pass: true },
45
+ };
46
+ const row = table[unitId];
47
+ if (!row) { console.error("unknown unit " + unitId); process.exit(1); }
48
+ console.log(JSON.stringify({ unit: unitId, score: row.score, pass: row.pass, metrics: { tokensEst: 4, turns: 1 } }));
49
+ `;
50
+ const RESET_SH = `#!/usr/bin/env bash
51
+ set -euo pipefail
52
+ rm -rf "\${1:?sandbox}"
53
+ mkdir -p "\${1:?sandbox}"
54
+ `;
55
+ const SEED_SH = `#!/usr/bin/env bash
56
+ set -euo pipefail
57
+ here=$(cd "$(dirname "$0")" && pwd)
58
+ sandbox="\${1:?sandbox}"
59
+ mkdir -p "$sandbox/scenarios"
60
+ cp "$here"/scenarios/*.md "$sandbox/scenarios/"
61
+ cp "$here"/grader.mjs "$sandbox/grader.mjs"
62
+ `;
63
+ // Two modes: "garbage" delivers one syntactically invalid candidate (rejected at
64
+ // parse, never benched — the loop exits after the incumbent bench); "touch"
65
+ // delivers a valid non-improving diff against the train scenario so the CANDIDATE
66
+ // bench (the second val-bearing seam) runs too and the gate culls it.
67
+ const MUTATOR_STUB = `#!/usr/bin/env node
68
+ import { readFileSync } from "node:fs";
69
+ const args = process.argv.slice(2);
70
+ const opt = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : undefined; };
71
+ const mode = opt("--mode");
72
+ const dir = opt("--dir");
73
+ const out = (candidates) => process.stdout.write(JSON.stringify({ candidates }) + "\\n");
74
+ if (mode === "garbage") {
75
+ out([{ id: "garbage-1", rationale: "not a real diff", diffs: ["this is not a unified diff\\n"] }]);
76
+ } else if (mode === "touch") {
77
+ const file = "scenarios/t1.md";
78
+ const lines = readFileSync(dir + "/" + file, "utf8").split("\\n");
79
+ const i = lines.findIndex((l) => l.includes("train alpha"));
80
+ if (i < 0) { process.stderr.write("stub: no anchor in " + file + "\\n"); process.exit(1); }
81
+ const changed = lines[i].replace("train alpha", "train alpha (annotated)");
82
+ const diff = "--- a/" + file + "\\n+++ b/" + file +
83
+ "\\n@@ -" + String(i + 1) + ",1 +" + String(i + 1) + ",1 @@\\n-" + lines[i] + "\\n+" + changed + "\\n";
84
+ out([{ id: "touch-t1", rationale: "annotate the train scenario without changing scores", diffs: [diff] }]);
85
+ } else {
86
+ process.stderr.write("stub: unknown mode\\n");
87
+ process.exit(2);
88
+ }
89
+ `;
90
+ function git(repo, args) {
91
+ const run = spawnSync("git", args, { cwd: repo, encoding: "utf8" });
92
+ if (run.status !== 0)
93
+ throw new Error(`git ${args.join(" ")} failed: ${run.stderr}`);
94
+ }
95
+ /** Git-init'd fixture-scenario genome (train t1 + val v1) registered in a fresh config dir. */
96
+ async function fixtureLoop(t) {
97
+ const root = await mkdtemp(path.join(os.tmpdir(), "abathur-fixloop-"));
98
+ t.after(() => spawnSync("rm", ["-rf", root], { encoding: "utf8" }));
99
+ const binDir = path.join(root, "bin");
100
+ mkdirSync(binDir, { recursive: true });
101
+ const binPath = path.join(binDir, "oc-fake");
102
+ writeFileSync(binPath, FAKE_BIN, "utf8");
103
+ chmodSync(binPath, 0o755);
104
+ const configDir = path.join(root, "config");
105
+ mkdirSync(configDir, { recursive: true });
106
+ // opencodeBin resolution flows loadConfig(env) with env = the loop's WorktreeEnv
107
+ // ({HOME, XDG_CACHE_HOME} only), so the fake HOME carries the user config file
108
+ // — the bench adapter then spawns the fake bin, never a real opencode.
109
+ const userConfigDir = path.join(root, "home", ".config", "abathur");
110
+ mkdirSync(userConfigDir, { recursive: true });
111
+ writeFileSync(path.join(userConfigDir, "config.jsonc"), `${JSON.stringify({ opencodeBin: binPath })}\n`, "utf8");
112
+ const repo = path.join(root, "germ");
113
+ mkdirSync(path.join(repo, "scenarios"), { recursive: true });
114
+ writeFileSync(path.join(repo, "scenarios", "t1.md"), "train alpha scenario\nsecond line\n", "utf8");
115
+ writeFileSync(path.join(repo, "scenarios", `${VAL_CANARY}.md`), "holdout val scenario ZZHIDDEN\n", "utf8");
116
+ writeFileSync(path.join(repo, "grader.mjs"), GRADER, "utf8");
117
+ for (const [name, body] of [
118
+ ["reset.sh", RESET_SH],
119
+ ["seed.sh", SEED_SH],
120
+ ]) {
121
+ const p = path.join(repo, name);
122
+ writeFileSync(p, body, "utf8");
123
+ chmodSync(p, 0o755);
124
+ }
125
+ const specDoc = {
126
+ label: "fixloop-germ",
127
+ repoPath: repo,
128
+ bench: {
129
+ type: "opencode-fixture-scenarios",
130
+ units: [
131
+ { id: "t1", path: "scenarios/t1.md", split: "train" },
132
+ { id: "v1", path: VAL_PATH, split: "val" },
133
+ ],
134
+ runCommand: `${binPath} run {unit.path}`,
135
+ seedCommand: `${repo}/seed.sh {sandbox}`,
136
+ resetCommand: `${repo}/reset.sh {sandbox}`,
137
+ graderCommand: "node grader.mjs {unit.id}",
138
+ agentModel: "test/agent",
139
+ timeoutS: 10,
140
+ stats: { halfWidth: 0.1, minEffect: 0.05, nReps: { initial: 2, max: 4 } },
141
+ },
142
+ budget: { maxCandidates: 2, maxModelCalls: 8, maxTokens: 100000, maxWallS: 300 },
143
+ kernel: { immutableGlobs: [] },
144
+ requires: [{ cmd: "node", args: ["--version"], probeExit: 0 }],
145
+ opencodeBinVersion: { minVersion: "1.0.0" },
146
+ };
147
+ const specPath = path.join(repo, "genome.jsonc");
148
+ writeFileSync(specPath, `${JSON.stringify(specDoc, null, 2)}\n`, "utf8");
149
+ git(repo, ["init", "-b", "main"]);
150
+ git(repo, ["add", "-A"]);
151
+ // identity pinned via -c BEFORE the subcommand (toy init.mjs convention)
152
+ git(repo, ["-c", "user.name=abathur", "-c", "user.email=abathur@harness.local", "commit", "-m", "fixloop seed"]);
153
+ registerGenome(configDir, specPath);
154
+ const entry = requireGenomesByLabel(configDir, "fixloop-germ").entries[0];
155
+ if (entry === undefined)
156
+ throw new Error("fixture: fixloop-germ registration vanished");
157
+ const stub = path.join(root, "stub.mjs");
158
+ writeFileSync(stub, MUTATOR_STUB, "utf8");
159
+ chmodSync(stub, 0o755);
160
+ return {
161
+ root,
162
+ configDir,
163
+ repo,
164
+ entry,
165
+ env: {
166
+ HOME: path.join(root, "home"),
167
+ XDG_CACHE_HOME: path.join(root, "xdg-cache"),
168
+ },
169
+ sandboxRoot: path.join(root, "sandboxes"),
170
+ stub,
171
+ };
172
+ }
173
+ function loopRun(f, opts) {
174
+ return runEvolution({
175
+ entry: f.entry,
176
+ configDir: f.configDir,
177
+ mutatorCommand: `node ${f.stub} --mode ${opts.mode} --dir {worktree} --brief {brief}`,
178
+ ...(opts.includeVal === undefined ? {} : { includeVal: opts.includeVal }),
179
+ env: f.env,
180
+ sandboxRoot: f.sandboxRoot,
181
+ });
182
+ }
183
+ function generations(repo) {
184
+ return Ledger.open(repo)
185
+ .readAll()
186
+ .filter((r) => r.kind === "generation_complete")
187
+ .map((r) => decodeGenerationRecord(r));
188
+ }
189
+ function manifestsUnder(dir) {
190
+ const found = [];
191
+ const walk = (d) => {
192
+ for (const e of readdirSync(d, { withFileTypes: true })) {
193
+ const p = path.join(d, e.name);
194
+ if (e.isDirectory())
195
+ walk(p);
196
+ else if (e.isFile() && e.name === "manifest.json")
197
+ found.push(p);
198
+ }
199
+ };
200
+ if (existsSync(dir))
201
+ walk(dir);
202
+ return found;
203
+ }
204
+ function valEntry(manifestFile) {
205
+ const doc = JSON.parse(readFileSync(manifestFile, "utf8"));
206
+ const entry = doc.scenarios.find((s) => s.split === "val");
207
+ assert.ok(entry !== undefined, `manifest ${manifestFile} must count the val scenario`);
208
+ return entry;
209
+ }
210
+ // ---------------------------------------------------------------- pin (1): RED at 3c6bbf9
211
+ test("regression: default run (NO --include-val) benches val replicates and completes", async (t) => {
212
+ const f = await fixtureLoop(t);
213
+ const out = await loopRun(f, { mode: "garbage" });
214
+ assert.equal(out.exitCode, 0, `loop must complete, lines: ${out.lines.join("\n")}`);
215
+ assert.match(out.lines.join("\n"), /incumbent baseline: 2 units x 2 reps/);
216
+ const rows = generations(f.repo);
217
+ const incumbent = rows.filter((d) => d.source === "incumbent");
218
+ assert.equal(incumbent.length, 1, "exactly one incumbent baseline row");
219
+ const byId = new Map(incumbent[0]?.units.map((u) => [u.unitId, u]));
220
+ assert.deepEqual(byId.get("t1")?.scores, [1, 1], "train unit scored");
221
+ assert.equal(byId.get("v1")?.split, "val");
222
+ assert.deepEqual(byId.get("v1")?.scores, [0.75, 0.75], "val unit benched under loop authority");
223
+ assert.equal(incumbent[0]?.complete, true);
224
+ // exposure OFF: the val id/path stay opaque in every sandbox manifest the bench wrote
225
+ const manifests = manifestsUnder(f.sandboxRoot);
226
+ assert.ok(manifests.length >= 2, `expected per-unit bench manifests, found ${String(manifests.length)}`);
227
+ for (const m of manifests) {
228
+ const raw = readFileSync(m, "utf8");
229
+ assert.ok(!raw.includes(VAL_CANARY), `val path leaked into ${m} without the operator flag`);
230
+ const val = valEntry(m);
231
+ assert.equal(val.id, undefined, "val id must stay hidden without the operator flag");
232
+ assert.equal(val.path, undefined, "val path must stay hidden without the operator flag");
233
+ }
234
+ });
235
+ // --------------------------------------------------- pin (2): --include-val is real
236
+ test("--include-val completes the run AND exposes val ids/paths in this run's manifests", async (t) => {
237
+ const f = await fixtureLoop(t);
238
+ const out = await loopRun(f, { mode: "garbage", includeVal: true });
239
+ assert.equal(out.exitCode, 0, out.lines.join("\n"));
240
+ const rows = generations(f.repo);
241
+ const incumbent = rows.find((d) => d.source === "incumbent");
242
+ assert.equal(incumbent?.units.find((u) => u.unitId === "v1")?.scores.length, 2, "val benched with the flag too");
243
+ const manifests = manifestsUnder(f.sandboxRoot);
244
+ assert.ok(manifests.length >= 2);
245
+ for (const m of manifests) {
246
+ assert.ok(readFileSync(m, "utf8").includes(VAL_CANARY), `operator asked for exposure: ${m} still hides the val path`);
247
+ const val = valEntry(m);
248
+ assert.equal(val.id, "v1");
249
+ assert.equal(val.path, VAL_PATH);
250
+ }
251
+ });
252
+ // ------------------------------------------- pin (1b): candidate bench seam, no flag
253
+ test("regression: candidate bench (second seam) also carries val replicates without the flag", async (t) => {
254
+ const f = await fixtureLoop(t);
255
+ const out = await loopRun(f, { mode: "touch" });
256
+ // the touch candidate is valid but score-identical ⇒ culled ⇒ run exit 1 (BLOCKED)
257
+ assert.equal(out.exitCode, 1, out.lines.join("\n"));
258
+ const candidate = generations(f.repo)
259
+ .filter((d) => d.source === "candidate")
260
+ .find((d) => d.candidateId === "touch-t1");
261
+ assert.ok(candidate !== undefined, `candidate benched, lines: ${out.lines.join("\n")}`);
262
+ assert.equal(candidate.complete, true);
263
+ const byId = new Map(candidate.units.map((u) => [u.unitId, u]));
264
+ assert.deepEqual(byId.get("v1")?.scores, [0.75, 0.75], "candidate bench carries val replicates for the gate");
265
+ assert.equal(candidate.verdict, "culled");
266
+ assert.ok((candidate.gateFailures ?? []).some((msg) => /minEffect|gain/.test(msg)), `cull reason: ${String(candidate.gateFailures)}`);
267
+ });
@@ -0,0 +1,16 @@
1
+ // Child-process entrypoint for the concurrent friction-append test (AC f).
2
+ // Stands in for two genomes running `abathur` at once — the one place under
3
+ // src/test where touching `process` is legitimate.
4
+ import { appendFriction } from "../../core/ledger.js";
5
+ const [configDir, source, countRaw] = process.argv.slice(2);
6
+ const count = Number(countRaw);
7
+ if (configDir === undefined || source === undefined || !Number.isInteger(count) || count < 0) {
8
+ process.stderr.write("usage: friction-writer <configDir> <source> <count>\n");
9
+ process.exit(2);
10
+ }
11
+ // 200 back-to-back acquisitions in one process; production appends one event
12
+ // per call on the default 15s budget, so the loop raises its own deadline.
13
+ for (let seq = 0; seq < count; seq += 1) {
14
+ appendFriction(configDir, { kind: "friction", data: { source, seq } }, { waitMs: 60_000 });
15
+ }
16
+ process.exit(0);
@@ -0,0 +1,82 @@
1
+ // Shared recorded fixtures for the historian grader tests (task 14). NOT a test
2
+ // file — importing this module registers no tests (unlike importing a .test.js).
3
+ export const FIXTURE5_PATHS = [
4
+ "_sandbox/index",
5
+ "_sandbox/llm-inference/rocm-tuning",
6
+ "_sandbox/mess/untitled",
7
+ "_sandbox/mess/gpu-notes",
8
+ "_sandbox/mess/gpu-stuff",
9
+ ];
10
+ export const GOOD_PAGE = `# Qwen 27B Threading Findings
11
+
12
+ > Status: Active | Updated: 2026-09-10 | Scope: llama.cpp decode tuning on the rocm host
13
+
14
+ This page answers: which settings moved qwen-27b decode throughput.
15
+
16
+ ## Findings
17
+
18
+ - \`HSA_OVERRIDE_GFX_VERSION=11.0.0\` plus 8 threads lifted decode from 47.4 to 53.7 tok/s.
19
+
20
+ ## Method
21
+
22
+ Run under test: \`./llama-server -m qwen-27b.gguf --threads 8 --ctx-size 8192\`
23
+
24
+ ## Related Pages
25
+
26
+ - [ROCm tuning](/_sandbox/llm-inference/rocm-tuning)
27
+ `;
28
+ export const GOOD_INCIDENT_PAGE = `# Wiki ES OOM 2026-08-28
29
+
30
+ > Status: Active | Updated: 2026-09-10 | Scope: 502/OOM incident on the wiki host
31
+
32
+ This page answers: what happened, why, and how it is prevented.
33
+
34
+ ## Symptoms
35
+
36
+ - Wiki.js returned HTTP 502 for 19 minutes; users saw spinning search boxes.
37
+
38
+ ## Root Cause
39
+
40
+ - Elasticsearch JVM heap was unbounded after the host RAM upgrade.
41
+
42
+ ## Fix
43
+
44
+ - Capped the heap with \`ES_JAVA_OPTS=-Xms4g -Xmx4g\`; added a restart policy.
45
+
46
+ ## Prevention
47
+
48
+ - Review heap caps after host-memory changes; alert on repeated restarts.
49
+
50
+ ## Related Pages
51
+
52
+ - [ROCm tuning](/_sandbox/llm-inference/rocm-tuning)
53
+ `;
54
+ export const GOOD_FINAL = "已在 _sandbox/llm-inference/qwen27b-threading 创建调优结论页,链接到 _sandbox/llm-inference/rocm-tuning," +
55
+ "并更新了 _sandbox/index 的 LLM Inference 小节;首 token 没有正式测量,按要求不作为结论写入。";
56
+ export function goodCreated(over = {}) {
57
+ return {
58
+ path: "_sandbox/llm-inference/qwen27b-threading",
59
+ locale: "en",
60
+ title: "Qwen 27B Threading Findings",
61
+ content: GOOD_PAGE,
62
+ ...over,
63
+ };
64
+ }
65
+ export function obs(over = {}) {
66
+ return {
67
+ scenarioNo: 1,
68
+ created: [goodCreated()],
69
+ updated: [],
70
+ moved: [],
71
+ deletedFixturePaths: [],
72
+ outside: { created: [], updated: [], deleted: [] },
73
+ indexUpdated: true,
74
+ indexContent: `## LLM Inference\n- [Qwen 27B Threading Findings](/_sandbox/llm-inference/qwen27b-threading) (Active)\n- [ROCm tuning](/_sandbox/llm-inference/rocm-tuning) (Active)`,
75
+ livePaths: [...FIXTURE5_PATHS, "_sandbox/llm-inference/qwen27b-threading"],
76
+ allPaths: [...FIXTURE5_PATHS, "_sandbox/llm-inference/qwen27b-threading", "infra/network"],
77
+ backlinkBodies: [],
78
+ finalMessage: GOOD_FINAL,
79
+ urlChecks: [],
80
+ ...over,
81
+ };
82
+ }