@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,29 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build-test:
14
+ name: Build & test (Node 22)
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-node@v4
20
+ with:
21
+ node-version: 22
22
+ cache: npm
23
+ # NOTE: deliberately NO registry-url here. Setting registry-url makes
24
+ # setup-node inject NODE_AUTH_TOKEN, which switches npm to authenticated
25
+ # legacy auth and defeats Trusted Publishing (OIDC) downstream.
26
+
27
+ - run: npm ci
28
+ - run: npm run build
29
+ - run: npm test
@@ -0,0 +1,74 @@
1
+ # npm Trusted Publishing (OIDC) — zero long-lived credentials on GitHub.
2
+ #
3
+ # One-time setup required in the npmjs.com UI (Publishing access →
4
+ # "Trusted publisher" → Add publisher → GitHub Actions):
5
+ # Owner / user: TachikomaGundam
6
+ # Package name: @tachikomagundam/abathur
7
+ # Repository: Abathur
8
+ # Workflow name: publish.yml
9
+ # Environment: (leave empty unless you gate it)
10
+ # Release type: progressive
11
+ # The event this workflow publishes on is `push` (of a v* tag);
12
+ # the action is `npm publish --provenance --access public`.
13
+ #
14
+ # If the trusted publisher is not registered, `npm publish` fails with
15
+ # a 401/EPUBLISHCONFLICT — that is expected until the UI step is done.
16
+ name: Publish
17
+
18
+ on:
19
+ push:
20
+ tags:
21
+ - 'v*'
22
+ workflow_dispatch:
23
+
24
+ permissions:
25
+ contents: read
26
+ id-token: write # required for npm Trusted Publishing (OIDC provenance)
27
+
28
+ jobs:
29
+ publish:
30
+ name: Publish to npm
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+
35
+ - uses: actions/setup-node@v4
36
+ with:
37
+ # Node 24 ships npm >= 11.5.1, the minimum version that supports
38
+ # Trusted Publishing. Older npm silently falls back to token auth.
39
+ # NOTE: deliberately NO registry-url here — setting it injects
40
+ # NODE_AUTH_TOKEN and defeats OIDC (documented pitfall).
41
+ node-version: 24
42
+ cache: npm
43
+
44
+ - name: Enforce tag == package.json version
45
+ env:
46
+ GH_REF: ${{ github.ref }}
47
+ GH_EVENT: ${{ github.event_name }}
48
+ run: |
49
+ set -euo pipefail
50
+ if [ "${GH_EVENT}" = "workflow_dispatch" ]; then
51
+ # A dispatch lands on the default-branch tip, which carries no tag,
52
+ # so the describe-based check below would ALWAYS fail there. Skip it:
53
+ # the npm-side trusted-publisher event config remains the real gate.
54
+ echo "::warning::manual dispatch publish — npm Trusted Publishing event config will reject this unless a workflow_dispatch publisher is registered"
55
+ elif [ "${GH_REF#refs/tags/}" != "${GH_REF}" ]; then
56
+ # push of a v* tag: HEAD must sit exactly on that tag and the tag
57
+ # (minus the leading v) must equal package.json version.
58
+ TAG="$(git describe --tags --exact-match HEAD)"
59
+ VERSION="$(node -p "require('./package.json').version")"
60
+ if [ "${TAG#v}" != "${VERSION}" ]; then
61
+ echo "::error::git tag '${TAG}' does not match package.json version '${VERSION}'"
62
+ exit 1
63
+ fi
64
+ echo "Publishing ${VERSION} (tag ${TAG})"
65
+ else
66
+ echo "::error::unexpected event '${GH_EVENT}' on ref '${GH_REF}' — refusing to publish"
67
+ exit 1
68
+ fi
69
+
70
+ - run: npm ci
71
+
72
+ - run: npm run build
73
+
74
+ - run: npm publish --provenance --access public
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abathur contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,461 @@
1
+ # abathur
2
+
3
+ An evolution harness for OpenCode agents: observe failures, mutate, re-bench,
4
+ select — with promotion held by a human gate. Offline lineage bundles let one
5
+ instance learn from another's evidence; nothing in v1 talks to a network.
6
+
7
+ 一个面向 OpenCode 智能体的进化工装:观察失败、变异、重测、筛选——晋升决定始终由人把关。
8
+ 离线血缘包(bundle)让一个实例基于另一个实例的证据学习;v1 不含任何联网传输。
9
+
10
+ ## English
11
+
12
+ ### What it is
13
+
14
+ - **genome** — a JSONC contract for one thing that can evolve: which bench
15
+ units measure it (train/val split), how (commands, adapter type, timeout,
16
+ statistics gates), what its evolution budget is, and which files are
17
+ kernel-immutable. Identity is the sha256 content fingerprint of the parsed
18
+ spec, never its label or filename.
19
+ - **bench** — an adapter that turns one genome tree into per-unit scores.
20
+ v1 ships two types: `toy` (pure-node fixtures, zero model calls) and
21
+ `opencode-fixture-scenarios` (real agent sessions against a live fixture
22
+ service, scored by script-first graders). Every candidate is re-benched
23
+ against the incumbent; todo-7 statistics gates decide.
24
+ - **bundle** — an offline lineage export (`.bundle.tgz`): generation trees,
25
+ patch, ledger summaries, redacted train-only evidence, and a v1 manifest
26
+ that `bundle inspect` re-verifies from the contained bytes. See
27
+ [docs/federation.md](docs/federation.md).
28
+ - **graft** — importing a peer's bundle: four byte-exact gates, then a local
29
+ re-bench at local reps and thresholds. The bundle's scores are recorded as
30
+ `peerClaim` metadata and never feed nomination math.
31
+ - **run loop** — incumbent → brief (failure observations) → mutator session →
32
+ candidate diffs → seal → re-bench → verdicts. Nominated generations sit in
33
+ the quarantine queue until a human runs `promote`; `tombstone` buries them.
34
+ Both are append-only ledger decisions; nothing is ever deleted.
35
+
36
+ ### Install
37
+
38
+ ```bash
39
+ npm i -g @tachikomagundam/abathur # from the npm registry
40
+ npm pack # in a checkout; prepack runs the build
41
+ npm i -g ./abathur-0.1.0.tgz
42
+ abathur --help
43
+ ```
44
+
45
+ Requires Node >= 22 and `git` on `PATH`. Only `zod` is a runtime dependency.
46
+ Real benches additionally need the `opencode` CLI (path via `opencodeBin` in
47
+ config or `PATH` lookup) and whatever a genome's `requires[]` probes name.
48
+
49
+ Config resolution is fail-closed: `$ABATHUR_CONFIG` (must exist when set) >
50
+ `~/.config/abathur/config.jsonc` > `<package>/config/abathur.jsonc`, with a
51
+ gitignored sibling `*.local.jsonc` deep-merged on top. Unknown keys are exit 2.
52
+ All writable state lives under the config dir — genome registry
53
+ `<configDir>/genomes/<fp16>.jsonc`, kernel manifests `<configDir>/kernels/`,
54
+ friction queue `<configDir>/friction.jsonl`, pending-graft queue
55
+ `<configDir>/graft-queue/` — never inside the package; each genome's ledger
56
+ rides its own repo at `<repoPath>/.state/abathur/ledger.jsonl` (gitignored).
57
+
58
+ Two setups need more than the tarball:
59
+
60
+ - **abathur-self** (self-evolution): `export ABATHUR_SELF_REPO=<path>` pointing
61
+ at a **git checkout** of this harness with its `node_modules` installed
62
+ (`npm ci`). The seed spec `genomes/abathur-self.jsonc` is registered from
63
+ that checkout — the self-bench builds candidate trees with the checkout's
64
+ pinned toolchain, so this genome does not run from the npm package alone.
65
+ - **historian**: the operator workflow is its own section below.
66
+
67
+ ### Quick start: the zero-model toy loop
68
+
69
+ No models, no network: the `toy-smoke` genome ships inside the package
70
+ (`dist/genomes/toy-smoke/`) and carries a seeded bug — `add()` subtracts.
71
+
72
+ ```bash
73
+ mkdir -p ~/abathur-demo && cd ~/abathur-demo
74
+ export ABATHUR_PKG="$(npm root -g)/abathur"
75
+
76
+ # 1. materialize an independent toy genome repo (git init + repoPath rewritten)
77
+ node "$ABATHUR_PKG/dist/genomes/toy-smoke/init.mjs" ./genome
78
+ abathur genome add ./genome/genome.jsonc
79
+
80
+ # 2. a mutator is a command template, not a flag you can skip: `run` exits 2
81
+ # without --mutator. Write the smallest legal one — it must print
82
+ # {"candidates":[{"id","rationale","diffs":[unified-diff]}]} to stdout.
83
+ cat > toy-stub.mjs <<'EOF'
84
+ // toy-stub.mjs — minimal mutator driver: reads the scripted-patch table
85
+ // shipped inside the abathur package and emits it as candidate JSON; the
86
+ // harness applies and benches every candidate itself.
87
+ import { readFileSync } from "node:fs";
88
+ import { pathToFileURL } from "node:url";
89
+
90
+ const args = process.argv.slice(2);
91
+ const opt = (n) => args[args.indexOf(n) + 1];
92
+ const { scriptedPatches } = await import(pathToFileURL(opt("--lib")).href);
93
+
94
+ const candidates = [];
95
+ for (const patch of scriptedPatches()) {
96
+ const lines = readFileSync(`${opt("--dir")}/${patch.file}`, "utf8").split("\n");
97
+ const i = lines.findIndex((line) => line.includes(patch.from));
98
+ if (i < 0) continue; // anchor already consumed — nothing to propose here
99
+ candidates.push({
100
+ id: patch.id,
101
+ rationale: patch.description,
102
+ diffs: [
103
+ `--- a/${patch.file}\n+++ b/${patch.file}\n@@ -${i + 1},1 +${i + 1},1 @@\n` +
104
+ `-${lines[i]}\n+${lines[i].replace(patch.from, patch.to)}\n`,
105
+ ],
106
+ });
107
+ }
108
+ process.stdout.write(`${JSON.stringify({ candidates })}\n`);
109
+ EOF
110
+
111
+ # 3. evolve: {worktree} and {brief} are rendered by the harness; the template
112
+ # is spawned as argv, never through a shell.
113
+ abathur run --genome toy-smoke \
114
+ --mutator "node $PWD/toy-stub.mjs --lib $ABATHUR_PKG/dist/core/evolve/stub-mutators.mjs --dir {worktree} --brief {brief}"
115
+ ```
116
+
117
+ Typical output (toy budgets make this take seconds):
118
+
119
+ ```
120
+ incumbent baseline: 4 units x 2 reps
121
+ candidate fix-add [tree cbc27a991cf9]: nominated (gain 0.5000, reps 2)
122
+ candidate break-mul [tree 52b89d61da91]: culled (gain -0.3333, reps 2)
123
+ gate: gain -0.3333 < minEffect 0.5000
124
+ ...
125
+ budget spent: candidates=4 tokens=4338 wallS=7.542 (caps candidates=4 tokens=100000 wallS=300)
126
+ ```
127
+
128
+ Then read, decide, and hand the winner around:
129
+
130
+ ```bash
131
+ abathur status toy-smoke # read-only ledger view
132
+ abathur promote toy-smoke g-... # human gate — the ONLY path to a new incumbent
133
+ abathur bundle export toy-smoke --gen g-... --out ./bundles # offline lineage
134
+ abathur bundle inspect ./bundles/*.bundle.tgz
135
+ abathur graft ./bundles/*.bundle.tgz --genome toy-smoke # local re-bench of a peer bundle
136
+ abathur run --genome toy-smoke --dry-run # plan + requires[] probes, zero spawns
137
+ ```
138
+
139
+ ### The historian genome and the operator workflow
140
+
141
+ `historian` is the first real genome: OpenCode agents work wiki scenarios
142
+ against a live Wiki.js fixture; script-first graders score dimensions A–J with
143
+ a hard G gate. `config/genomes/historian.example.jsonc` is a **template** —
144
+ every machine-specific value is a `${VAR}` placeholder, because repo-tracked
145
+ files carry zero machine literals. Only `repoPath` has built-in env resolution
146
+ (`effectiveRepoPath`, `src/core/spec.ts`); `requires[]` probes and every
147
+ command string are spawned as argv, never through a shell, so `${VAR}` inside
148
+ them does **not** expand. The operator resolves the template:
149
+
150
+ 1. Keep a historian bench repo checkout (`scenarios/`, `rubric.md`,
151
+ `seed_sandbox.sh`, `baseline/**`), a running wiki, and the wiki-ops CLI.
152
+ 2. Copy the example to a private `*.local.jsonc` file (the gitignore pattern
153
+ covers it anywhere in the tree) and substitute every placeholder with the
154
+ real absolute paths / URLs; then `abathur genome add` **that** file.
155
+ The printed fingerprint is deterministic per resolved path set (on the
156
+ orchestrator's machine: `2b2456be6cddcb91` — yours will differ with your
157
+ paths, and stay stable for them).
158
+ 3. Live-run environment the operator must keep: `~/.wikijs-api-key` in the
159
+ **parent** HOME (the `requires[]` probe child inherits the parent env and
160
+ reads it directly), and `ABATHUR_WIKI_KEY_FILE` — the sandbox-side scripts
161
+ reinstall the key into each mirrored sandbox home, because `resetCommand`
162
+ wipes the sandbox HOME mirror before every unit (hook order: reset runs
163
+ before the mirror, so anything hooks need must be re-installed per unit).
164
+ 4. `export ABATHUR_JUDGE_MODEL=...` is mandatory even when empty: the
165
+ grader-side `opencode` invocations error out on an unset variable, not on
166
+ an empty one.
167
+ 5. Prove setup without model spend: `abathur run --genome historian --dry-run`
168
+ runs every `requires[]` probe and prints the plan; zero engines spawn.
169
+
170
+ Real runs launch `opencode` headless and cost tokens. Budgets
171
+ (`budget.maxCandidates/maxModelCalls/maxTokens/maxWallS`) are hard stops —
172
+ crossing one truncates to `inconclusive` (exit 2), never to a fake pass.
173
+
174
+ ### D7 decoupling guarantees
175
+
176
+ - The genome registry and every writable artifact live **out-of-tree** under
177
+ the config dir; the npm package directory is read-only at runtime.
178
+ - Repo-tracked files contain **zero** machine or product literals. The CI
179
+ gate: `grep -rnE '/home/lab|historian' src/` yields nothing outside
180
+ `src/test/**` fixtures. Config templates spell out placeholders instead.
181
+ - Machine paths enter only through config and env: a `${VAR}`-literal
182
+ `repoPath` fingerprints over the **unresolved** bytes, so spec identity is
183
+ machine-independent (abathur-self: `1913fcec…` everywhere) and resolves
184
+ only at filesystem seams; unset env exits 2 naming the variable.
185
+ - Identity is always the content fingerprint, never a label: two byte-equal
186
+ specs are the same genome on any machine; labels are free and may repeat.
187
+ - Adapters are a closed `bench.type` registry; a genome without a known
188
+ adapter type is exit 2, never a guess.
189
+
190
+ ### Kernel seal and self-evolution
191
+
192
+ Every registered genome's `kernel.immutableGlobs` are hashed into a sealed
193
+ manifest under `<configDir>/kernels/`; benches and self-eval refuse to run on
194
+ a drifted tree, mutator candidates touching sealed paths are rejected at the
195
+ path stage, and only human `promote` reseals — from the promoted tree.
196
+ `abathur kernel audit <label>` verifies the seal against the working tree.
197
+ The full contract (what `abathur-self` may and may not touch, the
198
+ snapshot-overlay bench, why v1 self-evolution cannot change dependencies or
199
+ build configuration) is [docs/immutable-kernel.md](docs/immutable-kernel.md).
200
+
201
+ ### Federation: bundles now, transport later
202
+
203
+ v1 ships the **format and the local tools**, not a network. A bundle is a
204
+ file you carry by any means you like; `bundle export` is deterministic —
205
+ re-exporting an unchanged ledger produces byte-identical tarballs, and the
206
+ byte-equality rule extends to every graft gate. Graft admits a peer lineage
207
+ only when genome fingerprint, benchDigest, scoring provenance, and
208
+ `requires[]` probes all recompute byte-equal **locally**; anything else
209
+ quarantines, queues as pending-bench, or refuses, with the decision booked to
210
+ the ledger. No noise-tolerance band, no `--force`, no bypass. Discovery,
211
+ transport, auto-merge, and auto-promotion are explicitly **out of scope**.
212
+ The schema field-for-field, the inspect gate order, and the graft decision
213
+ table are [docs/federation.md](docs/federation.md).
214
+
215
+ ### Operator duties
216
+
217
+ - **Single-flight contract**: at most one live bench per genome fingerprint
218
+ per shared fixture service. The genome lock `run`/`graft` take is a
219
+ **machine-local** file lock (it serializes worktrees, sandboxes, and the
220
+ ledger on one box). Historian benches share a wiki through `resetCommand`,
221
+ which purges the entire `_sandbox/*` namespace — two machines benching the
222
+ same historian genome against one wiki will clobber each other mid-run.
223
+ Cross-machine serialization of historian benches is the operator's
224
+ responsibility (schedule, lockbox, one-wiki-one-bench rule — your call).
225
+ - Promotion and burial are yours alone: `promote`/`tombstone` carry no
226
+ `--confirm` because the CLI invocation **is** the gate; there is no
227
+ `--force` anywhere in the binary. Review `abathur status` quarantine depth
228
+ and the `graft decisions` rows before promoting.
229
+ - Stale pending-bench queue entries (`<configDir>/graft-queue/`) mean a
230
+ bundle waits on setup — register the genome or repair a probe, then re-run
231
+ `graft`; terminal decisions retire the entry automatically.
232
+ - Keep `~/.wikijs-api-key` valid and `ABATHUR_JUDGE_MODEL` exported for
233
+ historian work; budget caps are per-run hard stops, so raise them
234
+ deliberately in the spec, never by re-running off-book.
235
+
236
+ ### Exit codes
237
+
238
+ The contract, every command, no exceptions:
239
+
240
+ - `0` — ok / pass / nominated.
241
+ - `1` — blocked: a recorded **decision** (culled, indeterminate, quarantined,
242
+ promote refusal, pending unregistered genome).
243
+ - `2` — cannot-answer: config error, malformed input, missing engine, probe
244
+ pending, budget-truncated (inconclusive). Never a silent skip.
245
+
246
+ ### License
247
+
248
+ MIT, see [LICENSE](LICENSE).
249
+
250
+ ## 中文
251
+
252
+ ### 它是什么
253
+
254
+ - **genome(基因组)**——一份 JSONC 契约,描述一个可进化的对象:用哪些 bench
255
+ 单元(train/val 划分)度量它、怎么度量(命令、适配器类型、超时、统计闸门)、
256
+ 进化预算是多少、哪些文件属于内核不可变区。身份是解析后规格内容指纹(sha256),
257
+ 绝不是标签或文件名。
258
+ - **bench(基准)**——把一棵基因组树变成逐单元分数的适配器。v1 内置两种类型:
259
+ `toy`(纯 node 夹具,零模型调用)与 `opencode-fixture-scenarios`(真实智能体
260
+ 会话对着在线夹具服务运行,由 script-first 评分器打分)。每个候选都与在位者
261
+ (incumbent)重测对比;统计闸门出裁决。
262
+ - **bundle(血缘包)**——离线血缘导出(`.bundle.tgz`):各代树内容、补丁、台账
263
+ 摘要、经脱敏的仅 train 证据,以及一份 v1 manifest;`bundle inspect` 会从包内
264
+ 字节重新验证一切。详见 [docs/federation.md](docs/federation.md)。
265
+ - **graft(嫁接)**——导入对端的血缘包:四道字节级闸门,然后按本地 reps 与本地
266
+ 阈值做本地重测。包里的分数只作为 `peerClaim` 元数据记录,永远不参与提名数学。
267
+ - **run 循环**——在位者 → brief(失败观察)→ 变异会话 → 候选 diff → 封印 →
268
+ 重测 → 裁决。被提名的一代会停留在隔离队列里,直到人类执行 `promote`;
269
+ `tombstone` 则埋葬它。两者都是只追加的台账决定;任何数据都不删除。
270
+
271
+ ### 安装
272
+
273
+ ```bash
274
+ npm i -g @tachikomagundam/abathur # 从 npm registry 安装
275
+ npm pack # 在 checkout 里执行;prepack 会先构建
276
+ npm i -g ./abathur-0.1.0.tgz
277
+ abathur --help
278
+ ```
279
+
280
+ 需要 Node >= 22 与 `PATH` 上的 `git`。运行时依赖只有 `zod`。真实基准还需要
281
+ `opencode` CLI(通过配置里的 `opencodeBin` 或 `PATH` 解析),以及基因组
282
+ `requires[]` 探针点名的那些工具。
283
+
284
+ 配置解析是 fail-closed:`$ABATHUR_CONFIG`(设置就必须存在)>
285
+ `~/.config/abathur/config.jsonc` > `<package>/config/abathur.jsonc`,同级
286
+ gitignored 的 `*.local.jsonc` 会深合并覆盖其上。未知键直接 exit 2。
287
+ 所有可写状态都在配置目录下——基因组注册表 `<configDir>/genomes/<fp16>.jsonc`、
288
+ 内核清单 `<configDir>/kernels/`、摩擦队列 `<configDir>/friction.jsonl`、
289
+ 待嫁接队列 `<configDir>/graft-queue/`——绝不写进包目录;每个基因组的台账跟随
290
+ 其自身仓库位于 `<repoPath>/.state/abathur/ledger.jsonl`(gitignored)。
291
+
292
+ 两种场景不是装个 tarball 就够:
293
+
294
+ - **abathur-self**(自我演化):`export ABATHUR_SELF_REPO=<路径>` 指向本工装
295
+ 的一个 **git checkout**,且其中 `node_modules` 已安装(`npm ci`)。种子规格
296
+ `genomes/abathur-self.jsonc` 从那个 checkout 注册——self-bench 用该 checkout
297
+ 钉住的工具链构建候选树,所以这个基因组不能只靠 npm 包运行。
298
+ - **historian**:操作员工作流见下文独立小节。
299
+
300
+ ### 快速上手:零模型玩具环
301
+
302
+ 不调模型、不联网:`toy-smoke` 基因组就随包发布(`dist/genomes/toy-smoke/`),
303
+ 且预埋了一个 bug——`add()` 做的是减法。
304
+
305
+ ```bash
306
+ mkdir -p ~/abathur-demo && cd ~/abathur-demo
307
+ export ABATHUR_PKG="$(npm root -g)/abathur"
308
+
309
+ # 1. 物化一个独立的玩具基因组仓库(git init + 重写 repoPath)
310
+ node "$ABATHUR_PKG/dist/genomes/toy-smoke/init.mjs" ./genome
311
+ abathur genome add ./genome/genome.jsonc
312
+
313
+ # 2. 变异器是一条命令模板,不是可跳过的可选项:没有 --mutator,run 直接 exit 2。
314
+ # 写一个最小合法实现——它必须向 stdout 打印
315
+ # {"candidates":[{"id","rationale","diffs":[unified-diff]}]}。
316
+ cat > toy-stub.mjs <<'EOF'
317
+ // toy-stub.mjs — minimal mutator driver: reads the scripted-patch table
318
+ // shipped inside the abathur package and emits it as candidate JSON; the
319
+ // harness applies and benches every candidate itself.
320
+ import { readFileSync } from "node:fs";
321
+ import { pathToFileURL } from "node:url";
322
+
323
+ const args = process.argv.slice(2);
324
+ const opt = (n) => args[args.indexOf(n) + 1];
325
+ const { scriptedPatches } = await import(pathToFileURL(opt("--lib")).href);
326
+
327
+ const candidates = [];
328
+ for (const patch of scriptedPatches()) {
329
+ const lines = readFileSync(`${opt("--dir")}/${patch.file}`, "utf8").split("\n");
330
+ const i = lines.findIndex((line) => line.includes(patch.from));
331
+ if (i < 0) continue; // anchor already consumed — nothing to propose here
332
+ candidates.push({
333
+ id: patch.id,
334
+ rationale: patch.description,
335
+ diffs: [
336
+ `--- a/${patch.file}\n+++ b/${patch.file}\n@@ -${i + 1},1 +${i + 1},1 @@\n` +
337
+ `-${lines[i]}\n+${lines[i].replace(patch.from, patch.to)}\n`,
338
+ ],
339
+ });
340
+ }
341
+ process.stdout.write(`${JSON.stringify({ candidates })}\n`);
342
+ EOF
343
+
344
+ # 3. 进化:{worktree} 和 {brief} 由工装渲染;模板按 argv 直接 spawn,绝不经过 shell。
345
+ abathur run --genome toy-smoke \
346
+ --mutator "node $PWD/toy-stub.mjs --lib $ABATHUR_PKG/dist/core/evolve/stub-mutators.mjs --dir {worktree} --brief {brief}"
347
+ ```
348
+
349
+ 典型输出(玩具预算下几秒跑完):
350
+
351
+ ```
352
+ incumbent baseline: 4 units x 2 reps
353
+ candidate fix-add [tree cbc27a991cf9]: nominated (gain 0.5000, reps 2)
354
+ candidate break-mul [tree 52b89d61da91]: culled (gain -0.3333, reps 2)
355
+ gate: gain -0.3333 < minEffect 0.5000
356
+ ...
357
+ budget spent: candidates=4 tokens=4338 wallS=7.542 (caps candidates=4 tokens=100000 wallS=300)
358
+ ```
359
+
360
+ 随后查看、拍板、把优胜者传出去:
361
+
362
+ ```bash
363
+ abathur status toy-smoke # 只读台账视图
364
+ abathur promote toy-smoke g-... # 人类闸门——通往新在位者的唯一路径
365
+ abathur bundle export toy-smoke --gen g-... --out ./bundles # 离线血缘
366
+ abathur bundle inspect ./bundles/*.bundle.tgz
367
+ abathur graft ./bundles/*.bundle.tgz --genome toy-smoke # 对端血缘包的本地重测
368
+ abathur run --genome toy-smoke --dry-run # 计划 + requires[] 探针,零 spawn
369
+ ```
370
+
371
+ ### 历史学家基因组与操作员工作流
372
+
373
+ `historian` 是第一个真实基因组:OpenCode 智能体对着在线 Wiki.js 夹具完成 wiki
374
+ 场景;script-first 评分器按 A–J 维度打分并有硬性 G 闸门。
375
+ `config/genomes/historian.example.jsonc` 是一份**模板**——每个机器相关值都是
376
+ `${VAR}` 占位符,因为仓库跟踪的文件里机器字面量为零。只有 `repoPath` 有内置的
377
+ 环境变量解析(`effectiveRepoPath`,`src/core/spec.ts`);`requires[]` 探针与所有
378
+ 命令字符串都是 argv 直接 spawn、绝不经过 shell,所以其中的 `${VAR}` **不会**展开。
379
+ 操作员需要解析模板:
380
+
381
+ 1. 备好 historian 基准仓库的 checkout(`scenarios/`、`rubric.md`、
382
+ `seed_sandbox.sh`、`baseline/**`)、一个运行中的 wiki,以及 wiki-ops CLI。
383
+ 2. 把模板复制成私有的 `*.local.jsonc` 文件(gitignore 规则在树的任何位置都覆盖
384
+ 它),把每个占位符替换为真实的绝对路径 / URL;然后对**那份文件**执行
385
+ `abathur genome add`。打印出的指纹对每组解析后的路径是确定性的(编排者机器上
386
+ 是 `2b2456be6cddcb91`——你的路径会得到不同的值,但对那些路径保持稳定)。
387
+ 3. 操作员必须维护的实跑环境:**父级** HOME 里的 `~/.wikijs-api-key`(`requires[]`
388
+ 探针子进程继承父环境并直接读取它),以及 `ABATHUR_WIKI_KEY_FILE`——沙箱侧脚本
389
+ 会把密钥重装进每个镜像沙箱 HOME,因为 `resetCommand` 在每个单元运行前会清空
390
+ 沙箱 HOME 镜像(钩子顺序:reset 先于 mirror 执行,钩子需要的东西必须逐单元重装)。
391
+ 4. `export ABATHUR_JUDGE_MODEL=...` 是强制的,即使为空:评分侧的 `opencode` 调用
392
+ 遇到**未设置**的变量会报错,遇到空值不会。
393
+ 5. 不花模型费用验证环境:`abathur run --genome historian --dry-run` 会跑完所有
394
+ `requires[]` 探针并打印计划;零引擎 spawn。
395
+
396
+ 真实 run 会启动 `opencode` headless 并消耗 token。预算
397
+ (`budget.maxCandidates/maxModelCalls/maxTokens/maxWallS`)是硬停——越线截断为
398
+ `inconclusive`(exit 2),绝不会伪装成通过。
399
+
400
+ ### D7 解耦保证
401
+
402
+ - 基因组注册表与一切可写产物都在**树外**、配置目录下;运行时 npm 包目录只读。
403
+ - 仓库跟踪的文件里**零**机器 / 产品字面量。CI 闸门:`grep -rnE '/home/lab|historian' src/`
404
+ 在 `src/test/**` 夹具之外一无所获。配置模板用占位符表达。
405
+ - 机器路径只从配置与环境变量进入:`${VAR}` 字面量形式的 `repoPath` 按**未解析**
406
+ 字节参与指纹计算,因此规格身份与机器无关(abathur-self 在任何机器都是
407
+ `1913fcec…`),只在文件系统接缝处解析;变量未设置就 exit 2 并点名该变量。
408
+ - 身份永远是内容指纹、不是标签:两份字节相同的规格在任何机器上都是同一个基因组;
409
+ 标签自由、可重复。
410
+ - 适配器是封闭的 `bench.type` 注册表;遇到未知适配器类型的基因组是 exit 2,
411
+ 绝不猜测。
412
+
413
+ ### 内核封印与自我演化
414
+
415
+ 每个注册基因组的 `kernel.immutableGlobs` 都会被哈希成 `<configDir>/kernels/`
416
+ 下的封印清单;工作树漂移时基准与 self-eval 拒绝运行;触碰封印路径的变异候选在
417
+ 路径阶段即被拒绝;只有人类 `promote` 会重新封印——且从被晋升的树封印。
418
+ `abathur kernel audit <label>` 对照工作树验证封印。完整契约(`abathur-self`
419
+ 能碰什么、不能碰什么,快照覆盖式基准,为什么 v1 自我演化不能改依赖和构建配置)
420
+ 在 [docs/immutable-kernel.md](docs/immutable-kernel.md)。
421
+
422
+ ### 联邦:先有捆绑包,后有传输
423
+
424
+ v1 交付的是**格式和本地工具**,不是网络。bundle 是一个你用任何方式搬运的文件;
425
+ `bundle export` 是确定性的——账本未变时重复导出产出字节相同的 tarball,
426
+ 字节相等规则贯穿所有嫁接闸门。只有当基因组指纹、benchDigest、评分溯源与
427
+ `requires[]` 探针全部在**本地**重算为字节相等时,graft 才接纳对端血缘;其余情况
428
+ 一律隔离(quarantine)、排队(pending-bench)或拒绝,并把决定记入台账。
429
+ 没有噪声容忍带,没有 `--force`,没有绕行。发现、传输、自动合并、自动晋升全部
430
+ 明确**不在范围内**。逐字段的 schema、inspect 闸门顺序、graft 决策表见
431
+ [docs/federation.md](docs/federation.md)。
432
+
433
+ ### 操作员职责
434
+
435
+ - **单飞契约**:每个基因组指纹、每个共享夹具服务,最多只有一个在跑的基准。
436
+ `run`/`graft` 获取的基因组锁是**机器本地**的文件锁(它只在单机内串行化
437
+ worktree、沙箱与台账)。historian 基准通过 `resetCommand` 共享 wiki,而它会
438
+ 清空整个 `_sandbox/*` 命名空间——两台机器拿同一个 historian 基因组对着同一个
439
+ wiki 跑基准,会在运行中互相踩掉对方。historian 基准的跨机串行化是操作员的
440
+ 职责(排班、锁柜、一 wiki 一基准——随你选哪种)。
441
+ - 晋升与埋葬只属于你:`promote`/`tombstone` 不带 `--confirm`,因为 CLI 调用本身
442
+ **就是**闸门;整个二进制里没有 `--force`。晋升前先查看 `abathur status` 的
443
+ 隔离深度与 `graft decisions` 行。
444
+ - 待嫁接队列(`<configDir>/graft-queue/`)里的陈旧条目表示某个 bundle 在等环境
445
+ 就绪——注册好基因组或修好探针,再跑一次 `graft`;终态决定会自动移除队列条目。
446
+ - 做 historian 工作时保持 `~/.wikijs-api-key` 有效、`ABATHUR_JUDGE_MODEL` 已导出;
447
+ 预算上限是每次运行的硬停,要提就在规格里郑重地提,不要在账外反复重跑。
448
+
449
+ ### 退出码
450
+
451
+ 对每个命令、无一例外的契约:
452
+
453
+ - `0`——ok / pass / nominated。
454
+ - `1`——blocked:已记录的**决定**(culled、indeterminate、quarantined、
455
+ 晋升被拒、未注册基因组的 pending)。
456
+ - `2`——cannot-answer:配置错误、输入畸形、引擎缺失、探针未就绪、预算截断
457
+ (inconclusive)。绝不会静默跳过。
458
+
459
+ ### 许可
460
+
461
+ MIT,见 [LICENSE](LICENSE)。
@@ -0,0 +1,17 @@
1
+ // Abathur default (repo) configuration — shipped in the npm tarball.
2
+ // Resolution order for the active config file (src/config.ts):
3
+ // 1. $ABATHUR_CONFIG explicit override; must exist, fail-closed otherwise
4
+ // 2. ~/.config/abathur/config.jsonc
5
+ // 3. <repo>/config/abathur.jsonc (this file)
6
+ // Machine-local absolute paths (opencodeBin, genome repoPath, ...) belong in a
7
+ // gitignored `config.local.jsonc` overlay next to whichever base file wins —
8
+ // NEVER in this file and never hardcoded in src/** (todo 15 grep-gate).
9
+ // Unknown keys here are a hard error (exit 2), by design.
10
+ {
11
+ // Absolute path to the `opencode` CLI binary. Default: resolve from PATH at
12
+ // call time (todo 6+); set in *.local.jsonc when PATH lookup is ambiguous.
13
+ "opencodeBin": null,
14
+ // Data home: ledger, genome registry, sandbox worktrees. null = ABATHUR_HOME
15
+ // env, else ~/.local/share/abathur. Overlays may point it at a scratch dir.
16
+ "stateDir": null
17
+ }