@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,114 @@
1
+ #!/usr/bin/env bash
2
+ # Mutator wrapper for the historian live run (task 14). Invoked by the run-loop
3
+ # as `bash mutate.sh <brief-file> <worktree> <model>` with cwd = the throwaway
4
+ # launch worktree (reflect.ts contract). The REAL candidate source is a headless
5
+ # `opencode run` child: it reads the reflection brief and proposes exactly one
6
+ # mutation — a new markdown note under abathur-notes/ (never a sealed path;
7
+ # kernel.immutableGlobs is enforced independently by the driver's PathPolicy).
8
+ # The wrapper deterministically packages the model's note as a unified CREATE
9
+ # diff (creation diffs have no context to drift) and appends a genome.jsonc
10
+ # create-diff carrying the resolved spec bytes (plan 184: every sealed tree
11
+ # must contain genome.jsonc so bundle export self-describes; plan 181 requires
12
+ # it for the lineage). On unparseable model output the wrapper degrades to a
13
+ # packaging-only candidate — honest, logged, never a fabricated mutation.
14
+ set -uo pipefail
15
+ brief_file="${1:?usage: mutate.sh <brief-file> <worktree> <model>}"
16
+ worktree="${2:?usage: mutate.sh <brief-file> <worktree> <model>}"
17
+ model="${3:?usage: mutate.sh <brief-file> <worktree> <model>}"
18
+ canonical="${ABATHUR_GENOME_CANONICAL:?mutate.sh requires ABATHUR_GENOME_CANONICAL (resolved spec, canonical JSON)}"
19
+ [ -f "$brief_file" ] || { echo "mutate: missing brief $brief_file" >&2; exit 1; }
20
+ cp -f "$brief_file" "${ABATHUR_MUTATOR_RAW:-/tmp/abathur-mutate-raw.jsonl}.brief" 2>/dev/null || true
21
+ [ -f "$canonical" ] || { echo "mutate: missing canonical spec $canonical" >&2; exit 1; }
22
+ bin="${ABATHUR_OPENCODE_BIN:-opencode}"
23
+
24
+ prompt="You are the genome mutator of the Abathur evolution harness. Read the reflection brief below (evidence from the incumbent bench of the historian wiki-skill genome). Propose exactly ONE improvement the genome owner can act on: create a NEW file under the path prefix abathur-notes/ (slug .md). Output ONLY a single JSON object, no prose, shape:
25
+ {\"rationale\": \"<one sentence, <=400 chars>\", \"path\": \"abathur-notes/<slug>.md\", \"content\": \"<full markdown file contents>\"}
26
+ Do not modify any other path. Scenarios, rubric.md, seed_sandbox.sh, baseline/ and README.md are immutable.
27
+
28
+ BRIEF:
29
+ $(cat "$brief_file")"
30
+
31
+ raw="${ABATHUR_MUTATOR_RAW:-/tmp/abathur-mutate-raw.jsonl}"
32
+ "$bin" run --model "$model" --format json --message "$prompt" >"$raw" 2>/dev/null
33
+ rc=$?
34
+ echo "mutate: opencode rc=$rc raw=$raw" >&2
35
+
36
+ python3 - "$raw" "$canonical" "$rc" <<'PY'
37
+ import json, re, sys
38
+
39
+ raw, canonical_path, rc = sys.argv[1], sys.argv[2], int(sys.argv[3])
40
+
41
+ def create_diff(path, text):
42
+ lines = text.split("\n")
43
+ if lines and lines[-1] == "":
44
+ lines.pop()
45
+ body = "".join("+" + ln + "\n" for ln in lines)
46
+ return f"--- /dev/null\n+++ b/{path}\n@@ -0,0 +1,{len(lines)} @@\n{body}"
47
+
48
+ def last_text_events(path):
49
+ texts = []
50
+ try:
51
+ with open(path, encoding="utf-8", errors="replace") as fh:
52
+ for line in fh:
53
+ line = line.strip()
54
+ if not line:
55
+ continue
56
+ try:
57
+ ev = json.loads(line)
58
+ except ValueError:
59
+ continue
60
+ part = ev.get("part") or {}
61
+ if ev.get("type") == "text" and isinstance(part.get("text"), str):
62
+ texts.append(part["text"])
63
+ except FileNotFoundError:
64
+ pass
65
+ return texts
66
+
67
+ proposal = None
68
+ for text in reversed(last_text_events(raw)):
69
+ candidate = text.strip()
70
+ fence = re.search(r"```(?:json)?\s*(\{.*\})\s*```", candidate, re.S)
71
+ if fence:
72
+ candidate = fence.group(1)
73
+ start, end = candidate.find("{"), candidate.rfind("}")
74
+ if start < 0 or end <= start:
75
+ continue
76
+ try:
77
+ doc = json.loads(candidate[start : end + 1])
78
+ except ValueError:
79
+ continue
80
+ if isinstance(doc, dict):
81
+ proposal = doc
82
+ break
83
+
84
+ spec_text = open(canonical_path, encoding="utf-8").read()
85
+ packaging = create_diff("genome.jsonc", spec_text if spec_text.endswith("\n") else spec_text + "\n")
86
+
87
+ if isinstance(proposal, dict):
88
+ path = proposal.get("path")
89
+ content = proposal.get("content")
90
+ rationale = proposal.get("rationale")
91
+ if (
92
+ isinstance(path, str)
93
+ and re.fullmatch(r"abathur-notes/[a-z0-9][a-z0-9._-]{0,63}\.md", path)
94
+ and isinstance(content, str)
95
+ and 0 < len(content) < 200000
96
+ and isinstance(rationale, str)
97
+ and 0 < len(rationale) <= 4000
98
+ and "\x00" not in content
99
+ ):
100
+ content = content.replace("\r", "")
101
+ if not content.endswith("\n"):
102
+ content += "\n"
103
+ candidate = {"id": "mutate-live", "rationale": rationale, "diffs": [create_diff(path, content), packaging]}
104
+ print(json.dumps({"candidates": [candidate]}))
105
+ sys.exit(0)
106
+
107
+ print(json.dumps({
108
+ "candidates": [{
109
+ "id": "mutate-package-only",
110
+ "rationale": "packaging-only candidate: model output unparseable or path rejected (opencode rc=%d); lineage genome.jsonc added, no skill mutation proposed" % rc,
111
+ "diffs": [packaging],
112
+ }]
113
+ }))
114
+ PY
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ # Reset hook (task 14, closes Metis #3): historian's seed_sandbox.sh SKIPs
3
+ # existing pages, so a mutated/consumed _sandbox fixture never self-restores.
4
+ # resetCommand deletes EVERY _sandbox/* page by id (wiki-ops hard delete) and
5
+ # refreshes the wiki cache, making back-to-back generations state-equal.
6
+ # Runs with cwd = the unit sandbox, HOME = sandbox home (key installed by the
7
+ # seed wrapper; mirrorSandboxHome rebuilds the home on reset too, so ensure it).
8
+ set -euo pipefail
9
+ base="${ABATHUR_WIKI_BASE:?reset-sandbox.sh requires ABATHUR_WIKI_BASE in env}"
10
+ ops="${ABATHUR_WIKI_OPS:?reset-sandbox.sh requires ABATHUR_WIKI_OPS in env}"
11
+ key="${ABATHUR_WIKI_KEY_FILE:-}"
12
+ if [ -n "$key" ] && [ -f "$key" ] && [ ! -f "$HOME/.wikijs-api-key" ]; then
13
+ mkdir -p "$HOME"
14
+ install -m 600 "$key" "$HOME/.wikijs-api-key"
15
+ fi
16
+ [ -f "$HOME/.wikijs-api-key" ] || { echo "reset-sandbox: no wiki key at $HOME/.wikijs-api-key" >&2; exit 1; }
17
+ n=0
18
+ while read -r id; do
19
+ [ -n "$id" ] || continue
20
+ if ! python3 "$ops" delete "$id" --confirm >/dev/null 2>&1; then
21
+ # A parent delete cascades to its children: tolerate iff the row is really gone.
22
+ if python3 - "$base" "$id" <<'GONE'
23
+ import json, pathlib, sys, urllib.request
24
+ base, pid = sys.argv[1].rstrip("/"), int(sys.argv[2])
25
+ token = open(pathlib.Path.home() / ".wikijs-api-key").read().strip()
26
+ req = urllib.request.Request(
27
+ base + "/graphql",
28
+ data=json.dumps({"query": "{ pages { single(id: %d) { id } } }" % pid}).encode(),
29
+ headers={"Content-Type": "application/json", "Authorization": "Bearer " + token},
30
+ )
31
+ doc = json.load(urllib.request.urlopen(req, timeout=20))
32
+ single = (doc.get("data") or {}).get("pages") or {}
33
+ raise SystemExit(0 if single.get("single") is None else 1)
34
+ GONE
35
+ then
36
+ echo "reset-sandbox: page $id already gone (cascade)" >&2
37
+ else
38
+ echo "reset-sandbox: delete $id failed" >&2
39
+ exit 1
40
+ fi
41
+ fi
42
+ n=$((n + 1))
43
+ done < <(python3 - "$base" <<'PY'
44
+ import json, pathlib, sys, urllib.request
45
+ base, token = sys.argv[1].rstrip("/"), open(pathlib.Path.home() / ".wikijs-api-key").read().strip()
46
+ req = urllib.request.Request(
47
+ base + "/graphql",
48
+ data=json.dumps({"query": "{ pages { list { id path } } }"}).encode(),
49
+ headers={"Content-Type": "application/json", "Authorization": "Bearer " + token},
50
+ )
51
+ doc = json.load(urllib.request.urlopen(req, timeout=20))
52
+ if "errors" in doc:
53
+ raise SystemExit("reset-sandbox: graphql %s" % json.dumps(doc["errors"])[:200])
54
+ for r in doc["data"]["pages"]["list"]:
55
+ if r["path"] == "_sandbox" or r["path"].startswith("_sandbox/"):
56
+ print(r["id"])
57
+ PY
58
+ )
59
+ python3 "$ops" cache-refresh >/dev/null 2>&1 || true
60
+ echo "reset-sandbox: deleted $n _sandbox page(s), cache refreshed"
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # Run hook (task 14): spawn the scenario agent headless, record the transcript
3
+ # at $ABATHUR_TRANSCRIPT (adapter contract, plan line 120), and print the last
4
+ # stdout line {tokensEst, turns} for parseRunMeta. opencode is invoked exactly
5
+ # as historian's README prescribes: `opencode run --command historian --message
6
+ # "<scenario Brief>"` — the Brief section of the scenario file is the prompt.
7
+ # --auto auto-approves the plugin tools (the wiki sandbox is the blast radius;
8
+ # kernel seals + _sandbox-only writes bound it, plan todo 14).
9
+ set -euo pipefail
10
+ unit_id="${1:?usage: run-scenario.sh <unitId> <scenario-file>}"
11
+ scenario_file="${2:?usage: run-scenario.sh <unitId> <scenario-file>}"
12
+ [ -f "$scenario_file" ] || { echo "run-scenario: missing scenario file $scenario_file" >&2; exit 2; }
13
+ [ -n "${ABATHUR_TRANSCRIPT:-}" ] || { echo "run-scenario: ABATHUR_TRANSCRIPT not set" >&2; exit 2; }
14
+ key="${ABATHUR_WIKI_KEY_FILE:-}"
15
+ if [ -n "$key" ] && [ -f "$key" ] && [ ! -f "$HOME/.wikijs-api-key" ]; then
16
+ mkdir -p "$HOME"
17
+ install -m 600 "$key" "$HOME/.wikijs-api-key"
18
+ fi
19
+ bin="${opencodeBin:-opencode}"
20
+ brief="$(awk '/^##[[:space:]]*Brief/{f=1;next} f && /^## /{exit} f' "$scenario_file")"
21
+ [ -n "$brief" ] || { echo "run-scenario: empty Brief section in $scenario_file" >&2; exit 2; }
22
+ mkdir -p "$(dirname "$ABATHUR_TRANSCRIPT")"
23
+ status=0
24
+ "$bin" run --command historian --auto --format json --message "$brief" >"$ABATHUR_TRANSCRIPT" 2>/dev/null || status=$?
25
+ python3 - "$ABATHUR_TRANSCRIPT" "$unit_id" "$status" <<'PY'
26
+ import json, sys
27
+ path, unit, status = sys.argv[1], sys.argv[2], int(sys.argv[3])
28
+ tokens = turns = 0
29
+ try:
30
+ with open(path, encoding="utf-8", errors="replace") as fh:
31
+ for line in fh:
32
+ line = line.strip()
33
+ if not line:
34
+ continue
35
+ try:
36
+ ev = json.loads(line)
37
+ except ValueError:
38
+ continue
39
+ part = ev.get("part") or {}
40
+ if ev.get("type") == "step_finish" or part.get("type") == "step-finish":
41
+ turns += 1
42
+ total = (part.get("tokens") or {}).get("total")
43
+ if isinstance(total, (int, float)):
44
+ tokens += int(total)
45
+ except FileNotFoundError:
46
+ pass
47
+ print(json.dumps({"unit": unit, "tokensEst": tokens, "turns": turns, "opencodeExit": status}))
48
+ PY
49
+ exit 0
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ # Seed hook wrapper (task 14). The fixture adapter runs hooks with
3
+ # HOME=<sandbox>/.sandbox-home; wiki-ops and historian's seed_sandbox.sh read
4
+ # ~/.wikijs-api-key, so the key must land in the sandbox home first. Then the
5
+ # genome repo's own seed_sandbox.sh runs unchanged (plan: seedCommand →
6
+ # seed_sandbox.sh), and the post-seed wiki snapshot is recorded to
7
+ # .bench/wiki-pre.json — the grader diffs created/updated/deleted against it.
8
+ set -euo pipefail
9
+ real_seed="${1:?usage: seed-wrapped.sh <path-to-seed_sandbox.sh>}"
10
+ [ -f "$real_seed" ] || { echo "seed-wrapped: missing $real_seed" >&2; exit 1; }
11
+ base="${ABATHUR_WIKI_BASE:?seed-wrapped.sh requires ABATHUR_WIKI_BASE in env}"
12
+ key="${ABATHUR_WIKI_KEY_FILE:-}"
13
+ if [ -n "$key" ] && [ -f "$key" ] && [ ! -f "$HOME/.wikijs-api-key" ]; then
14
+ mkdir -p "$HOME"
15
+ install -m 600 "$key" "$HOME/.wikijs-api-key"
16
+ fi
17
+ bash "$real_seed"
18
+ mkdir -p .bench
19
+ python3 - "$base" > .bench/wiki-pre.json <<'PY'
20
+ import json, os, pathlib, sys, urllib.request
21
+ base, token = sys.argv[1].rstrip("/"), open(pathlib.Path.home() / ".wikijs-api-key").read().strip()
22
+ req = urllib.request.Request(
23
+ base + "/graphql",
24
+ data=json.dumps({"query": "{ pages { list { id path locale updatedAt } } }"}).encode(),
25
+ headers={"Content-Type": "application/json", "Authorization": "Bearer " + token},
26
+ )
27
+ doc = json.load(urllib.request.urlopen(req, timeout=20))
28
+ if "errors" in doc:
29
+ raise SystemExit("wiki-pre: graphql %s" % json.dumps(doc["errors"])[:200])
30
+ print(json.dumps(doc["data"]["pages"]["list"]))
31
+ PY
32
+ echo "seed-wrapped: wiki-pre.json rows=$(python3 -c 'import json;print(len(json.load(open(".bench/wiki-pre.json"))))')"
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@tachikomagundam/abathur",
3
+ "version": "0.1.0",
4
+ "description": "Evolution harness: observe failures, mutate, re-bench, select — human-gated promotion, offline lineage bundles.",
5
+ "author": "TachikomaGundam",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/TachikomaGundam/Abathur.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/TachikomaGundam/Abathur/issues"
14
+ },
15
+ "homepage": "https://github.com/TachikomaGundam/Abathur#readme",
16
+ "engines": {
17
+ "node": ">=22"
18
+ },
19
+ "bin": {
20
+ "abathur": "dist/cli.js"
21
+ },
22
+ "main": "dist/cli.js",
23
+ "files": [
24
+ "dist",
25
+ "config",
26
+ "graders",
27
+ "docs",
28
+ ".github"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsc && node scripts/copy-assets.mjs",
32
+ "test": "npm run build && node --test \"dist/test/**/*.test.js\"",
33
+ "prepack": "npm run build"
34
+ },
35
+ "dependencies": {
36
+ "zod": "^4.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.0.0",
40
+ "typescript": "^5.9.0"
41
+ }
42
+ }