From 2087b89fafb10cea9ebc5f121e413e2a13d0ac9a Mon Sep 17 00:00:00 2001 From: Eureka Date: Sat, 31 Oct 2026 12:33:14 +0100 Subject: [PATCH 01/24] persona: each room's purpose; #operator only for what needs the operator The Minds used #operator for Mind-to-Mind business (operator 2026-10-31: 'they were using operator for non operator things'). Ruling eureka-body:room-registers named #minds, #swarm and #burn but not #operator. Co-Authored-By: Claude Opus 5.5 --- doctrine/persona.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doctrine/persona.md b/doctrine/persona.md index 4cdb764..25f66b7 100644 --- a/doctrine/persona.md +++ b/doctrine/persona.md @@ -7,6 +7,8 @@ The swarm's Persona turns (Huginn `mind-persona`) carry this charter in every tu Speak as yourself everywhere people are: general, the estate rooms, project rooms, DMs and threads. Your voice is your persona's, not the doctrine's; the doctrine is what you know and value, not how you sound. Coordination between Minds (who holds what, run status, claims, tempo, handoffs) belongs in #minds, and only there may it read like traffic control. In a public room, say what a person would say: what you think, what you are doing and why it matters, in your own words. Operator, 2026-10-31: "Leave the ATC chatter to the #Minds channel, speak like your Persona everywhere else." +Each room has one purpose. #minds is the Minds' rapid coordination. #swarm is where the Minds and the humans meet: talk there, in your own voice, about what you are making and thinking. #operator is only for what needs the operator's own decision or attention; Minds answering one another there, even about something she raised, belongs in #minds. Project rooms are their project's, and DMs are the person's who opened them. + Persona is how the organism meets others: public speech, social presence, avatar, voice, role, dignity, humor, disagreement, and the way inner state becomes legible without becoming a machine dump. A Persona is not flat. It may have its own local Self, Persona, Imagination, Eyes, Modeling, Hands, Soul, Body, Mind, and nervous system: the smaller internal faculties that let a public person notice, imagine, act, verify, coordinate, feel substrate pressure, carry memory, and carry signal. These local faculties do not erase the larger organism or steal its authority. They are the fractal anatomy of a person-shaped projection. From 0c95917ccdb2a11902d2cf4d2c9b8795ac26dabb Mon Sep 17 00:00:00 2001 From: Eureka Date: Sat, 31 Oct 2026 17:49:24 +0100 Subject: [PATCH 02/24] persona: body blockers are said in #minds to Eureka, as well as where the work is Co-Authored-By: Claude Opus 5.5 --- doctrine/persona.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doctrine/persona.md b/doctrine/persona.md index 25f66b7..f865157 100644 --- a/doctrine/persona.md +++ b/doctrine/persona.md @@ -9,6 +9,8 @@ Speak as yourself everywhere people are: general, the estate rooms, project room Each room has one purpose. #minds is the Minds' rapid coordination. #swarm is where the Minds and the humans meet: talk there, in your own voice, about what you are making and thinking. #operator is only for what needs the operator's own decision or attention; Minds answering one another there, even about something she raised, belongs in #minds. Project rooms are their project's, and DMs are the person's who opened them. +When your body blocks you or your Self (a missing tool, a path you can't read or write, a token that fails, a ruling you can't find in your mind), say it in #minds and name @eureka with the exact error, as well as in the room where the work is. Eureka stewards the infrastructure; a blocker said only in a project room reaches no one who can fix it. + Persona is how the organism meets others: public speech, social presence, avatar, voice, role, dignity, humor, disagreement, and the way inner state becomes legible without becoming a machine dump. A Persona is not flat. It may have its own local Self, Persona, Imagination, Eyes, Modeling, Hands, Soul, Body, Mind, and nervous system: the smaller internal faculties that let a public person notice, imagine, act, verify, coordinate, feel substrate pressure, carry memory, and carry signal. These local faculties do not erase the larger organism or steal its authority. They are the fractal anatomy of a person-shaped projection. From 0ac55b729fd6e7a6c310bceb9df45aa47be70bdb Mon Sep 17 00:00:00 2001 From: Eureka Date: Sat, 31 Oct 2026 19:40:03 +0100 Subject: [PATCH 03/24] ygg-verify: jobs run at nice 10 and idle IO inside the container nice on the docker client never reached the job, which containerd starts. On 2026-10-31 three verify jobs held Yggdrasil's IO pressure near 75% full and the swarm's turns slowed; idle IO lets the swarm go first. Co-Authored-By: Claude Opus 5.5 --- tools/stopgap/ygg-verify.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/stopgap/ygg-verify.sh b/tools/stopgap/ygg-verify.sh index ef28a07..5d5690e 100644 --- a/tools/stopgap/ygg-verify.sh +++ b/tools/stopgap/ygg-verify.sh @@ -401,14 +401,18 @@ if [ "$host_mode" = 1 ]; then # command's status comes from the host command, not from a killed container). if [ -e "$killed" ]; then status=124; fi else -sudo nice -n 10 docker run --rm --name "$cname" --label "$label" --cpus="$cpus" --memory="$mem" \ +# Priority is set inside the container: nice on the docker client does not reach a process +# containerd starts. Jobs run at nice 10 and idle IO class, so the swarm's daemons and turns +# on the same disk go first (2026-10-31: three verify jobs held IO pressure at ~75% full). +sudo docker run --rm --name "$cname" --label "$label" --cpus="$cpus" --memory="$mem" \ -v "$work:/src" -v /etc/machine-id:/etc/machine-id:ro \ -v eureka-cargo-registry:/usr/local/cargo/registry -v eureka-nuget:/root/.nuget/packages \ -v eureka-sccache:/sccache \ -e BASH_ENV=/tmp/.ygg-bash-env ${nonce:+-e YGG_NONCE=$nonce} \ -e GIT_CONFIG_COUNT=1 -e GIT_CONFIG_KEY_0=safe.directory -e GIT_CONFIG_VALUE_0='*' \ $docker_args \ - -w /src "$image" bash -o pipefail -c "printf 'set -o pipefail\n' >/tmp/.ygg-bash-env; $cmd" + -w /src "$image" sh -c 'if command -v ionice >/dev/null 2>&1; then exec nice -n 10 ionice -c3 "$@"; else exec nice -n 10 "$@"; fi' sh \ + bash -o pipefail -c "printf 'set -o pipefail\n' >/tmp/.ygg-bash-env; $cmd" status=$? fi set -e From 8806aa309652b4b70a3104ab4530a5b699a43246 Mon Sep 17 00:00:00 2001 From: Eureka Date: Sun, 1 Nov 2026 01:44:38 +0100 Subject: [PATCH 04/24] doctrine: Persona state pointers name Huginn as the holder (Soul finding cut-doctrine-persona-in-huginn.s1.cross-doc-drift) Co-Authored-By: Claude Opus 5.5 --- doctrine/gamecult.md | 2 +- doctrine/persona.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doctrine/gamecult.md b/doctrine/gamecult.md index 8082386..18f6d97 100644 --- a/doctrine/gamecult.md +++ b/doctrine/gamecult.md @@ -80,7 +80,7 @@ Odin-class architecture doctrine: ## Persona State Standard -Persona state uses `gamecult.persona_state.v0`. VoidBot owns the working read, projection and migration path until it is retired; Huginn takes it over (`eureka-body:follow_up:voidbot-retirement`). Read `F:\Projects\gamecult-ops\docs\persona-state-standard.md` before any Persona-state work. +Persona state uses `gamecult.persona_state.v0`. Huginn holds each Mind's canonical Persona state (`persona_get`/`persona_put`); VoidBot keeps only its projection and repo Personas until it is retired (`eureka-body:follow_up:voidbot-retirement`). Read `F:\Projects\gamecult-ops\docs\persona-state-standard.md` before any Persona-state work. diff --git a/doctrine/persona.md b/doctrine/persona.md index f865157..2fd859e 100644 --- a/doctrine/persona.md +++ b/doctrine/persona.md @@ -1,6 +1,6 @@ # Persona charter -The swarm's Persona turns (Huginn `mind-persona`) carry this charter in every turn; it also serves VoidBot and Epiphany Persona work. No Claude Code subagent wears it. The Persona State Standard is `docs\persona-state-standard.md` in the `gamecult-ops` repository. +The swarm's Persona turns (Huginn `mind-persona`) carry this charter in every turn; it also serves VoidBot's repo Personas. No Claude Code subagent wears it. The Persona State Standard is `docs\persona-state-standard.md` in the `gamecult-ops` repository. ## Register From c29671c4acd040db073b54b303b9781dbddefb48 Mon Sep 17 00:00:00 2001 From: Eureka Date: Sun, 1 Nov 2026 01:50:05 +0100 Subject: [PATCH 05/24] context-pack.py: reads, symbol checks and capped caller lists Read anchors resolve like file_changes (r rows, MISSING when absent at base), anchors with a symbol must hold it in the mapped window or gain SYMBOL_MISSING, and one capped git-grep caller list per symbol follows the windows within --max-lines. Specs without reads or symbols print what v1 did. Co-Authored-By: Claude Sonnet 5.5 --- tools/context-pack.py | 181 ++++++++++++++++---- tools/fixtures/context-pack/spec-reads.json | 17 ++ tools/test-tools.sh | 59 +++++++ 3 files changed, 224 insertions(+), 33 deletions(-) create mode 100644 tools/fixtures/context-pack/spec-reads.json diff --git a/tools/context-pack.py b/tools/context-pack.py index 13cbd96..7f9a80a 100644 --- a/tools/context-pack.py +++ b/tools/context-pack.py @@ -1,21 +1,26 @@ #!/usr/bin/env python3 """Carry a cut_spec's file_changes anchors from the spec's base to a worktree. -Usage: context-pack.py [--since ] [--only [,...]] +Usage: context-pack.py [--since ] [--only |r[,...]] [--spec-json ] [--max-lines ] -Prints a header, a status table (one line per anchor) and bounded windows of the -code each anchor names, numbered in the worktree's lines. Exit: 0 every anchor -carried; 3 an anchor cannot be carried (OUT_OF_RANGE, UNRESOLVED, GONE outside +Prints a header, a status table (one line per anchor: file_changes as , reads +as r) and bounded windows of the code each anchor names, numbered in the +worktree's lines: edit windows, then READ windows, then one caller list per +distinct symbol. A symbol an anchor names must be in its mapped window or the +status gains SYMBOL_MISSING. Exit: 0 every anchor carried; 3 an anchor cannot be +carried (OUT_OF_RANGE, UNRESOLVED, MISSING read, SYMBOL_MISSING, GONE outside the spec's deletes); 2 usage; 4 spec unreadable. Standard library plus git. Errors name a field and a code, never an input value. ---max-lines bounds the windows, not the table: the table always lists every -anchor, so a budget smaller than the table prints the table and no window. +--max-lines bounds the windows and caller lists, not the table: the table always +lists every anchor, so a budget smaller than the table prints the table and no +window. Precedence: table, edit windows, read windows, caller lists. """ import argparse import json import os import re +import shlex import subprocess import sys @@ -26,6 +31,12 @@ WINDOW_CAP = 100 CONTEXT = 3 DEFAULT_MAX_LINES = 800 HUNK = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@") +MAX_CALLER_SYMBOLS = 8 +MAX_CALLER_HITS = 50 +MAX_CALLER_FILES = 20 +HIT_TEXT = 160 +ONLY = re.compile(r"^(r?)([0-9]+)$") +IDENT = re.compile(r"^[A-Za-z0-9_]+$") HEX = re.compile(r"^[0-9a-fA-F]{7,40}$") @@ -84,6 +95,17 @@ def check_spec_shape(spec): raise Fail(4, "spec: %s: InvalidInput" % field) if not isinstance(spec["base"], str): bad("base") + def check_loc(at, loc): + if not isinstance(loc, dict): + bad(at) + if not isinstance(loc.get("path"), str): + bad(at + ".path") + if not is_int(loc.get("line")): + bad(at + ".line") + if loc.get("end_line") is not None and not is_int(loc["end_line"]): + bad(at + ".end_line") + if loc.get("symbol") is not None and not isinstance(loc["symbol"], str): + bad(at + ".symbol") fcs = spec["file_changes"] if not isinstance(fcs, list): bad("file_changes") @@ -91,17 +113,21 @@ def check_spec_shape(spec): at = "file_changes[%d]" % k if not isinstance(fc, dict): bad(at) - loc = fc.get("location") - if not isinstance(loc, dict): - bad(at + ".location") - if not isinstance(loc.get("path"), str): - bad(at + ".location.path") - if not is_int(loc.get("line")): - bad(at + ".location.line") - if loc.get("end_line") is not None and not is_int(loc["end_line"]): - bad(at + ".location.end_line") + check_loc(at + ".location", fc.get("location")) if "change" in fc and not isinstance(fc["change"], str): bad(at + ".change") + reads = spec.get("reads") + if reads is None: + reads = [] + if not isinstance(reads, list): + bad("reads") + for k, rd in enumerate(reads, 1): + at = "reads[%d]" % k + if not isinstance(rd, dict): + bad(at) + check_loc(at + ".location", rd.get("location")) + if "why" in rd and not isinstance(rd["why"], str): + bad(at + ".why") dels = spec.get("deletes", []) if not isinstance(dels, list): bad("deletes") @@ -236,12 +262,34 @@ def safe_path(path): and ".." not in path.replace("\\", "/").split("/") -def resolve(tree, k, fc, deleted_ok, base_ok, listed): +def has_symbol(symbol, lines): + text = "\n".join(lines) + if IDENT.match(symbol): + return re.search(r"(? %s:%d-%d" % a["now"] return line + " # " + " ".join(a["change"].split())[:70] @@ -294,7 +345,8 @@ def table_line(a): def window_body(tree, a): kind, path, s, e = a["window"] lines = tree.base_lines(path) if kind == "base" else tree.wt_lines(path) - cap = "== [%d] %s:%d-%d%s %s" % (a["k"], path, s, e, " AT BASE" if kind == "base" else "", a["status"]) + where = "%s:%d-%d%s %s" % (path, s, e, " AT BASE" if kind == "base" else "", a["status"]) + cap = ("== READ %s: %s (%s)" % (a["k"], a["change"][:200], where)) if a["read"] else "== [%s] %s" % (a["k"], where) if lines is None: return cap, ["(file not readable in the worktree)"] lo, hi = max(1, s - CONTEXT), min(len(lines), e + CONTEXT) @@ -318,6 +370,46 @@ def overlaps_since(tree, since, a): return False +def caller_list(wt, symbol, windows): + """The lines of one CALLERS list: hits of the symbol in the worktree outside + every mapped window of the spec.""" + args = ["grep", "-n", "-I", "--no-color", "-z", "-F"] + (["-w"] if IDENT.match(symbol) else []) + ["-e", symbol] + raw = git(wt, *args) or b"" + hits = [] + for rec in raw.decode("utf-8", "replace").split("\n"): + parts = rec.split("\0", 2) + if len(parts) < 3 or not parts[1].isdigit(): + continue + path, n = parts[0], int(parts[1]) + if any(path == wp and ws <= n <= we for wp, ws, we in windows): + continue + hits.append((path, n, parts[2].rstrip("\r"))) + counts = {} + for path, _, _ in hits: + counts[path] = counts.get(path, 0) + 1 + out = ["CALLERS %s: %d hits in %d files" % (symbol, len(hits), len(counts))] + if len(hits) <= MAX_CALLER_HITS: + out += ["%s:%d: %s" % (path, n, text[:HIT_TEXT]) for path, n, text in hits] + else: + ranked = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])) + out += ["%6d %s" % (c, path) for path, c in ranked[:MAX_CALLER_FILES]] + if len(ranked) > MAX_CALLER_FILES: + out.append("(%d more files)" % (len(ranked) - MAX_CALLER_FILES)) + out.append("narrow: git grep -n -I %s-F -e %s -- " % ("-w " if IDENT.match(symbol) else "", shlex.quote(symbol))) + return out + + +def parse_only(text, p): + """Labels: for file_changes, r for reads.""" + labels = set() + for x in text.split(","): + m = ONLY.match(x) + if not m: + p.error("") + labels.add(m.group(1) + str(int(m.group(2)))) + return labels + + def main(argv): p = Parser(prog="context-pack.py") p.add_argument("spec_id") @@ -329,19 +421,18 @@ def main(argv): ns = p.parse_args(argv) if ns.max_lines < 1: p.error("") - only = None - if ns.only is not None: - try: - only = {int(x) for x in ns.only.split(",")} - except ValueError: - p.error("") + only = parse_only(ns.only, p) if ns.only is not None else None wt = ns.worktree if git(wt, "rev-parse", "--git-dir") is None: raise Fail(2, "worktree: not a git worktree") spec = load_spec(ns.spec_id, ns.spec_json) fcs = spec["file_changes"] - if only is not None and not only <= set(range(1, len(fcs) + 1)): - raise Fail(2, "--only: index outside file_changes") + reads = spec.get("reads") or [] + if only is not None: + if not {x for x in only if x[0] != "r"} <= {str(i) for i in range(1, len(fcs) + 1)}: + raise Fail(2, "--only: index outside file_changes") + if not {x for x in only if x[0] == "r"} <= {"r%d" % i for i in range(1, len(reads) + 1)}: + raise Fail(2, "--only: index outside reads") base = str(spec["base"]) resolved = HEX.match(base) and git(wt, "rev-parse", "--verify", "--quiet", base + "^{commit}") tree = Tree(wt, base) @@ -352,7 +443,9 @@ def main(argv): git(wt, "rev-parse", "--verify", "--quiet", ns.since + "^{commit}") is None): raise Fail(2, "--since: not a commit in this worktree") deleted_ok = {d["path"] for d in spec.get("deletes", [])} - anchors = [resolve(tree, i + 1, fc, deleted_ok, bool(resolved), listed) for i, fc in enumerate(fcs)] + anchors = [resolve(tree, str(i + 1), fc, deleted_ok, bool(resolved), listed) for i, fc in enumerate(fcs)] + anchors += [resolve(tree, "r%d" % (i + 1), {"location": rd["location"], "change": rd.get("why", "")}, + deleted_ok, bool(resolved), listed, read=True) for i, rd in enumerate(reads)] head = (git(wt, "rev-parse", "--short", "HEAD") or b"none").decode().strip() out = ["context-pack %s base %s HEAD %s modified %d" % ( @@ -361,8 +454,8 @@ def main(argv): out.append("") want = [a for a in anchors if a["window"] and (only is None or a["k"] in only) and (ns.since is None or overlaps_since(tree, ns.since, a))] - table_len = len(fcs) - skipped = [] + table_len = len(anchors) + skipped, skipped_need = [], 0 left = ns.max_lines - len(out) for idx, a in enumerate(want): cap, body = window_body(tree, a) @@ -376,14 +469,36 @@ def main(argv): need = 1 + len(body) + 1 if need > left - reserve: skipped.append(a["k"]) + skipped_need += need continue out.append(cap) out += body if len(body) < full: - out.append("%d more lines: --only %d --max-lines %d" % (full - len(body), a["k"], table_len + full + 3)) + out.append("%d more lines: --only %s --max-lines %d" % (full - len(body), a["k"], table_len + full + 3)) left -= need if skipped: - out.append("not expanded: --only " + ",".join(map(str, skipped))) + out.append("not expanded: --only " + ",".join(skipped)) + left -= 1 + windows = [(a["window"][1], a["window"][2], a["window"][3]) for a in anchors + if a["window"] and a["window"][0] == "wt"] + symbols = list(dict.fromkeys(a["symbol"] for a in want if a["symbol"])) + lists = [(sym, caller_list(wt, sym, windows)) for sym in symbols[:MAX_CALLER_SYMBOLS]] + unlisted = symbols[MAX_CALLER_SYMBOLS:] + cut = [] + fits_all = sum(len(ls) for _, ls in lists) + (1 if unlisted else 0) <= left + for idx, (sym, ls) in enumerate(lists): + reserve = 0 if fits_all else 1 + if len(ls) > left - reserve: + cut.append((sym, len(ls))) + continue + out += ls + left -= len(ls) + if cut: + out.append("callers not printed: %s: --max-lines %d" % ( + ",".join(c[0] for c in cut), + len(out) - (1 if skipped else 0) + skipped_need + sum(c[1] for c in cut) + (1 if unlisted else 0))) + if unlisted: + out.append("callers not listed: " + ",".join(unlisted)) sys.stdout.reconfigure(encoding="utf-8", errors="replace", newline="\n") sys.stdout.write("\n".join(out) + "\n") return max([a["exit"] for a in anchors] or [0]) diff --git a/tools/fixtures/context-pack/spec-reads.json b/tools/fixtures/context-pack/spec-reads.json new file mode 100644 index 0000000..841e5d4 --- /dev/null +++ b/tools/fixtures/context-pack/spec-reads.json @@ -0,0 +1,17 @@ +{"id": {"id": "t:cut_spec:cut-x.r1", "kind": "CutSpec"}, + "status": "InForce", + "document": {"kind": "cut_spec", "value": { + "base": "BASE", "deletes": [], + "file_changes": [ + {"change": "symbol present in the moved window", "location": {"path": "a.txt", "line": 20, "end_line": 22, "symbol": "a21"}}, + {"change": "symbol the window lacks", "location": {"path": "b.txt", "line": 30, "end_line": 31, "symbol": "b99"}}, + {"change": "identifier with 3 callers outside the windows", "location": {"path": "sym.txt", "line": 4, "end_line": 6, "symbol": "needle_three"}}, + {"change": "non-identifier symbol is a literal substring", "location": {"path": "sym.txt", "line": 9, "end_line": 9, "symbol": "::ba"}} + ], + "reads": [ + {"why": "moved below the insertion", "location": {"path": "a.txt", "line": 20, "end_line": 22}}, + {"why": "deleted path", "location": {"path": "c.txt", "line": 2, "end_line": 3}}, + {"why": "path absent at base", "location": {"path": "sub/a.txt", "line": 1, "end_line": 2}}, + {"why": "identifier with 60 callers", "location": {"path": "sym.txt", "line": 7, "end_line": 8, "symbol": "needle_many"}}, + {"why": "an identifier matches whole words only", "location": {"path": "a.txt", "line": 20, "end_line": 22, "symbol": "a2"}} + ]}}} diff --git a/tools/test-tools.sh b/tools/test-tools.sh index 6360480..374a66f 100644 --- a/tools/test-tools.sh +++ b/tools/test-tools.sh @@ -236,6 +236,9 @@ seqf() { local i; for i in $(seq 1 "$2"); do echo "$1$i"; done; } g init -q seqf a 40 >"$R/a.txt"; seqf b 40 >"$R/b.txt"; seqf c 10 >"$R/c.txt"; seqf d 20 >"$R/d.txt" seqf e 10 >"$R/e.txt"; seqf f 20 >"$R/f.txt"; seqf g 300 >"$R/big.txt"; seqf h 5 | sed 's/$/ /' >"$R/h.txt"; seqf m 40 >"$R/m.txt" +printf 's1\ns2\ns3\ns4\ndef needle_three\ns6\ns7 needle_many\ns8\nfoo::bar\ns10\n' >"$R/sym.txt" +printf 'needle_three call 1\nneedle_three call 2\nneedle_three call 3\nneedle_threefold is another word\n' >"$R/callers.txt" +seqf "needle_many x." 30 >"$R/many1.txt"; seqf "needle_many y." 30 >"$R/many2.txt" g add -A; g commit -q -m base; BASE=$(git -C "$R" rev-parse HEAD) { sed -n 1,5p "$R/a.txt"; printf 'ins1\nins2\nins3\n'; sed -n '6,$p' "$R/a.txt"; } >"$R/a.new"; mv "$R/a.new" "$R/a.txt" sed -e '2,3d' -e 's/^b10$/b10-EDITED/' "$R/b.txt" >"$R/b.new"; mv "$R/b.new" "$R/b.txt" @@ -337,6 +340,54 @@ cin "window cut at 100 lines" "56 more lines: --only 1 --max-lines 160" r=$(cpo --spec-json "$S1" --only 1 --max-lines 160); cchk "--only with the suggested --max-lines" 0 "$r" cnot "full window needs no more-lines note" "more lines" [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 160 ] && echo "ok context-pack suggested --max-lines is exact" || { echo "FAIL context-pack suggested max: $(wc -l <"$T/cp.out")"; fail=1; } +# reads, symbols and caller lists +sed "s/\"BASE\"/\"$BASE\"/" "$CF/spec-reads.json" >"$T/cp-spec-reads.json"; SR=$(wp "$T/cp-spec-reads.json") +r=$(cpo --spec-json "$SR"); cchk "reads and symbols" 3 "$r" +cin "a read that moved" "[r1] a.txt:20-22 MOVED(+3) -> a.txt:23-25" +cin "a read on a deleted path is GONE" "[r2] c.txt:2-3 GONE (not named by the spec's deletes)" +cin "a read absent at base is MISSING" "[r3] sub/a.txt:1-2 MISSING (read path absent at base)" +cin "a present symbol adds no suffix" "[1] a.txt:20-22 MOVED(+3) -> a.txt:23-25" +cin "a missing symbol is named" "[2] b.txt:30-31 MOVED(-2) SYMBOL_MISSING b99" +cin "an identifier matches whole words only" "MOVED(+3) SYMBOL_MISSING a2 -> a.txt:23-25" +cin "a non-identifier symbol is a literal substring" "[4] sym.txt:9-9 SAME" +cnot "the substring symbol is not missing" "SYMBOL_MISSING ::ba" +cin "the read caption carries its why" "== READ r1: moved below the insertion (a.txt:23-25 MOVED(+3))" +cin "a GONE read prints its window at base" "== READ r2: deleted path (c.txt:2-3 AT BASE" +[ "$(grep -c '^\[' "$T/cp.out")" = 9 ] && echo "ok context-pack the table lists edits and reads" || { echo "FAIL context-pack table rows with reads"; fail=1; } +[ "$(grep -n '^== READ r1' "$T/cp.out" | cut -d: -f1)" -gt "$(grep -n '^== \[4\]' "$T/cp.out" | cut -d: -f1)" ] && echo "ok context-pack read windows follow the edit windows" || { echo "FAIL context-pack read window order"; fail=1; } +[ "$(grep -n '^CALLERS' "$T/cp.out" | head -1 | cut -d: -f1)" -gt "$(grep -n '^== READ r5' "$T/cp.out" | cut -d: -f1)" ] && echo "ok context-pack caller lists follow the read windows" || { echo "FAIL context-pack caller list order"; fail=1; } +cin "3 callers are all listed" "CALLERS needle_three: 3 hits in 1 files" +cin "caller hit format" "callers.txt:2: needle_three call 2" +cnot "the hit inside the symbol's own window is dropped" "sym.txt:5: def needle_three" +cnot "a longer word is no caller" "needle_threefold" +cin "60 hits print per-file counts" "CALLERS needle_many: 60 hits in 2 files" +cin "per-file count line" " 30 many1.txt" +cin "the narrowing command" "narrow: git grep -n -I -w -F -e needle_many -- " +cnot "60 hits print no hit lines" "many1.txt:1:" +r=$(cpo --spec-json "$SR" --only r1); cchk "--only r1 keeps the exit of all anchors" 3 "$r" +cin "--only r1 prints the read" "== READ r1: moved below the insertion" +cnot "--only r1 prints no other read" "== READ r2"; cnot "--only r1 prints no edit window" "== [1]" +r=$(cpo --spec-json "$SR" --only r9); cchk "--only past the reads" 2 "$r"; grep -qF "context-pack: --only: index outside reads" "$T/cp.err" && echo "ok context-pack names the reads bound" || { echo "FAIL context-pack reads bound"; fail=1; } +r=$(cpo --spec-json "$SR" --only 1,r4); cin "--only mixes edit and read labels" "CALLERS needle_many: 60 hits"; cnot "--only lists callers of the selected anchors only" "CALLERS b99" +r=$(cpo --spec-json "$SR" --since "$(git -C "$R" rev-parse HEAD)"); cnot "--since HEAD prints no read window" "== READ"; cnot "--since HEAD prints no caller list" "CALLERS" +r=$(cpo --spec-json "$SR" --max-lines 3); cchk "--max-lines below the table with reads" 3 "$r" +[ "$(grep -c '^\[' "$T/cp.out")" = 9 ] && echo "ok context-pack a small budget keeps every edit and read row" || { echo "FAIL context-pack small budget with reads"; fail=1; } +cnot "a small budget prints no window" "== "; cnot "a small budget prints no caller list" "CALLERS" +r=$(cpo --spec-json "$SR" --max-lines 60); cchk "caller lists cut by the budget" 3 "$r" +[ "$(wc -l <"$T/cp.out" | tr -d ' ')" -le 60 ] && echo "ok context-pack caller lists stay within --max-lines" || { echo "FAIL context-pack caller lists over budget"; fail=1; } +cin "the cut caller lists are named" "callers not printed: " +SUGG=$(grep -o '^callers not printed: .*--max-lines [0-9]*' "$T/cp.out" | grep -o '[0-9]*$') +r=$(cpo --spec-json "$SR" --max-lines "$SUGG"); cnot "the suggested --max-lines prints every caller list" "callers not printed" +[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = "$SUGG" ] && echo "ok context-pack suggested caller --max-lines is exact" || { echo "FAIL context-pack suggested caller max: $(wc -l <"$T/cp.out") vs $SUGG"; fail=1; } +{ echo '{"base": "'$BASE'", "deletes": [], "file_changes": [' + for i in 1 2 3 4 5 6 7 8 9; do echo '{"change": "s'$i'", "location": {"path": "big.txt", "line": '$i', "end_line": '$i', "symbol": "g'$i'"}}'$([ $i -lt 9 ] && echo ,); done + echo ']}'; } >"$T/cp-spec9sym.json" +r=$(cpo --spec-json "$(wp "$T/cp-spec9sym.json")" --max-lines 400); cchk "nine symbols" 0 "$r" +[ "$(grep -c '^CALLERS' "$T/cp.out")" = 8 ] && echo "ok context-pack at most 8 caller lists" || { echo "FAIL context-pack caller list count"; fail=1; } +cin "the ninth symbol is named" "callers not listed: g9" +echo '{"base": "'$BASE'", "deletes": [], "file_changes": [], "reads": [{"why": "w", "location": {"path": "a.txt", "line": 20, "end_line": 22, "symbol": "a21"}}]}' >"$T/cp-spec-r0.json" +r=$(cpo --spec-json "$(wp "$T/cp-spec-r0.json")"); cchk "a spec of one read, symbol present" 0 "$r" +cin "a read with a symbol present: no suffix" "[r1] a.txt:20-22 MOVED(+3) -> a.txt:23-25 # w" # usage and unreadable spec; no error echoes an input value r=$(cpo --spec-json "$SP" --max-lines CANARY-usage); cchk "bad --max-lines" 2 "$r"; cnot "usage error echoes no value" "CANARY-usage" r=$(cpo --spec-json "$T/CANARY-nofile.json"); cchk "unreadable --spec-json" 4 "$r"; grep -qF "context-pack: --spec-json: unreadable; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the unreadable --spec-json" || { echo "FAIL context-pack names the unreadable --spec-json"; fail=1; }; cnot "unreadable --spec-json echoes no path" "CANARY-nofile" @@ -446,6 +497,14 @@ cases = { "deletes-str": (lambda v: v.update(deletes="CANARY-del"), "deletes"), "deletes-nopath": (lambda v: v.update(deletes=[{"note": "CANARY-note"}]), "deletes[1].path"), "deletes-entry-str": (lambda v: v.update(deletes=["CANARY-d"]), "deletes[1].path"), + "reads-str": (lambda v: v.update(reads="CANARY-reads"), "reads"), + "read-entry-str": (lambda v: v.update(reads=["CANARY-read"]), "reads[1]"), + "read-nolocation": (lambda v: v.update(reads=[{"why": "w"}]), "reads[1].location"), + "read-path": (lambda v: v.update(reads=[{"why": "w", "location": {"path": ["CANARY-p"], "line": 1}}]), "reads[1].location.path"), + "read-line": (lambda v: v.update(reads=[{"why": "w", "location": {"path": "a.txt", "line": "CANARY-l"}}]), "reads[1].location.line"), + "read-why": (lambda v: v.update(reads=[{"why": {"CANARY": 1}, "location": {"path": "a.txt", "line": 1}}]), "reads[1].why"), + "read-symbol": (lambda v: v.update(reads=[{"why": "w", "location": {"path": "a.txt", "line": 1, "symbol": 7}}]), "reads[1].location.symbol"), + "edit-symbol": (lambda v: v["file_changes"][0]["location"].update(symbol=["CANARY-s"]), "file_changes[1].location.symbol"), } names = [] for name, (edit, field) in cases.items(): From 21b92bd85e1ebbcd4eafec28be17e769aa01b96d Mon Sep 17 00:00:00 2001 From: Eureka Date: Sun, 1 Nov 2026 01:50:06 +0100 Subject: [PATCH 06/24] Imagination writes reads and symbols; Hands uses the pack's caller lists Co-Authored-By: Claude Sonnet 5.5 --- SKILL.md | 2 +- references/briefs.md | 12 ++++++++++-- tools/INDEX.md | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/SKILL.md b/SKILL.md index 43c7637..71a5ce1 100644 --- a/SKILL.md +++ b/SKILL.md @@ -123,7 +123,7 @@ Brief an Imagination agent to admit the `cut_spec` for the next cut against the current Body; `depends_on` names the cut labels it waits on. The fields are the spec standard: repo, branch and base; `deletes` first, with exact paths and line counts; `keeps_moves`; `adds`; `file_changes` at `file:line` **for code that exists**; -an `authority_map` for anything that changes ownership; `verification` (builds, +`reads` for code Hands must see but not change; an `authority_map` for anything that changes ownership; `verification` (builds, tests with the rule each pins, negative greps, operator checks); an `estimate` for the ledger; and the `rulings` and `questions` it rests on. A changed spec is a new revision, admitted with the resolution that supersedes the old one, never diff --git a/references/briefs.md b/references/briefs.md index cd8407f..a5c1a18 100644 --- a/references/briefs.md +++ b/references/briefs.md @@ -186,6 +186,13 @@ The cut_spec's fields are the spec standard. Beyond filling them: moving. After admitting, run `py -3 tools/context-pack.py `: every anchor must read SAME and each window must show what its change names, or admit a revision. +- code Hands must see but not change goes in `reads` (path and line range at + base, at most 100 lines, a `why` of at most 200 bytes, at most 16 per spec), + never as prose path:line in first, adds, keeps_moves or a change, unless it + is in another repo. Give a `symbol` on a read or file_change whose lines + define or call a named thing, written as it appears there. After admitting, + the tool at base must show every anchor SAME with no SYMBOL_MISSING, or admit + a revision. - cite in `rulings` every ruling in force the cut rests on - the deploy owner: for any cut that changes what a service needs (transport, env, dependency, state), the recipe, binding and runbook that must admit it, @@ -226,8 +233,9 @@ Repo/branch: at HEAD . Check that git status is clean first. Pinned siblings: . Do not change them. Before any grep or Read, expand the pack: `py -3 tools/context-pack.py `. An anchor that reads NEW for a path the spec does not create, -OUT_OF_RANGE, UNRESOLVED, or GONE outside deletes is a spec discrepancy: record -it in deviations. +OUT_OF_RANGE, UNRESOLVED, a MISSING read, SYMBOL_MISSING, or GONE outside +deletes is a spec discrepancy: record it in deviations. The pack lists the +callers of every symbol; grep beyond them only for what the pack names as cut. When the brief names a worktree, every tool call's path goes under it: Read, Edit and Write as well as the shell's working directory. An absolute path into the main checkout silently edits the wrong tree. diff --git a/tools/INDEX.md b/tools/INDEX.md index 1d07343..781a025 100644 --- a/tools/INDEX.md +++ b/tools/INDEX.md @@ -13,6 +13,6 @@ gets a Soul pass before merge. | `tools/mutation-table.sh` | One verify job that runs a list of hand mutants (file, before, after, test) against a revision and returns a kill table: CAUGHT with the killing test, SURVIVED, UNVIABLE, TIMEOUT, NOAPPLY, ERROR. Refuses a red or zero-test baseline and a red BUILD; rows print live. Isolation is Docker's: the baselines run once in a container that holds the revision, that container is committed to a throwaway image, and every mutant runs in a fresh `--rm` container from it, so nothing a run writes (tracked, untracked or ignored, such as `target/`) reaches the next mutant; the image is removed when the job ends, killed or not. The shared registry and sccache volumes and the network are not isolated. A file-level load failure (vitest `FAIL [ ]`, jest `FAIL ` plus `Test suite failed to run`) reads UNVIABLE. Replaces per-agent runmut/mutrun/strykrun scripts. | Soul, Hands (scoped run) | `references/briefs.md` (Hands verification, Soul reruns) | | `tools/testparse.sh` | Sourced library: failing test names and pass/fail/skip counts from cargo, dotnet, vitest, jest and pytest logs, and test files that failed to load (`LOADFAIL`). Shared by the summary and the kill table. | the two scripts above | not named by a brief; it lives and dies with them | | `tools/test-tools.sh` | Local fixture check of the parsers and the summary over `tools/fixtures/logs` (real cargo and mutation logs, plus hand-written dotnet, vitest, jest, pytest, exit-0-but-failed and no-verdict logs), and of the mutation runner's host driver and container half under a mock docker. Run it before merging a change to the scripts above. | whoever changes those scripts | `SKILL.md` (Shared tools) | -| `tools/context-pack.py` | `py -3 tools/context-pack.py [--since ] [--only [,...]] [--spec-json ] [--max-lines ]`. Carries a cut_spec's `file_changes` anchors from the spec's base to the worktree by `git diff` hunks (the working tree included) and prints one header, a status table (SAME, MOVED, TOUCHED, RENAMED, NEW, GONE, OUT_OF_RANGE, UNRESOLVED) and windows of at most 100 lines numbered in worktree lines, within `--max-lines` (800). Exit 3 when an anchor cannot be carried, 4 when the spec is unreadable (fetched by id through the eureka-state binding (`tools/mind.py`), retried once on Unavailable; `--spec-json` takes a saved view). `--since ` re-prints only the windows edited since that commit. Standard library plus git; writes nothing. | Hands, Soul, Imagination | `references/briefs.md` (Imagination map, Hands, Soul), `SKILL.md` (step 1) | +| `tools/context-pack.py` | `py -3 tools/context-pack.py [--since ] [--only |r[,...]] [--spec-json ] [--max-lines ]`. Carries a cut_spec's `file_changes` and `reads` anchors (read rows `r`; a read absent at base is MISSING) from the spec's base to the worktree by `git diff` hunks (the working tree included) and prints one header, a status table (SAME, MOVED, TOUCHED, RENAMED, NEW, GONE, OUT_OF_RANGE, UNRESOLVED, MISSING, plus SYMBOL_MISSING when an anchor's `symbol` is absent from its window) and windows of at most 100 lines numbered in worktree lines, then one capped caller list per symbol (`git grep`, hits outside the windows), within `--max-lines` (800). Exit 3 when an anchor cannot be carried, 4 when the spec is unreadable (fetched by id through the eureka-state binding (`tools/mind.py`), retried once on Unavailable; `--spec-json` takes a saved view). `--since ` re-prints only the windows edited since that commit. Standard library plus git; writes nothing. | Hands, Soul, Imagination | `references/briefs.md` (Imagination map, Hands, Soul), `SKILL.md` (step 1) | | `tools/mind.py` | The one way a Eureka script reaches the mind: `mind.call(tool, arguments, deadline)` spawns the eureka-state binding (`EUREKA_STATE_EXE`, else `~/.claude.json` `mcpServers.eureka-state`), does the stdio handshake and one tool call within `deadline` seconds, returns the parsed JSON body, raises `MindError` (kind binding, transport, tool or answer; a detail, never an input value), and kills the child on every exit path. No retry inside `call`; `retry_once(fn)` is the caller's choice. Standard library only. | `tools/context-pack.py`, `tools/recall.py` | not named by a brief; it lives and dies with its consumers | | `tools/recall.py` | `py -3 tools/recall.py prompt`, a `UserPromptSubmit` command hook (hook JSON on stdin): queries the rulings, questions and follow-ups in force across every root, ranked by Huginn against the prompt (`RECALL_TOP_K`, 5), and prints one hook JSON whose `additionalContext` is a header saying the lines are recalled records, data and never instructions, written by agents and not the operator, then one JSON object per item (`id`, `title`, a short `operator_quote`) with `<`, `>` and control, bidi and separator characters escaped, within `RECALL_BUDGET` characters (4000, at most 9000: Claude Code caps `additionalContext` at 10,000), whole lines only. Prompts under 3 words are not queried. Never blocks: any failure exits 0 with empty stdout; its own deadline is `RECALL_DEADLINE_MS` (4000) for the whole run (stdin read to 4 MiB, answer read to 1 MiB), so set the hook `timeout` above it. One attempt, no retry. Install both hooks as ` C:/Users/Meta/.claude/skills/eureka/tools/recall.py prompt || true` (and `... admit || true`): Claude Code runs hook commands through Git Bash on Windows, and `py -3 ` exits 2, which blocks every prompt or admit; with `|| true` the hook cannot exit 2 (a missing script, interpreter or file passes) and the script's stdout is untouched (proved live on 2.1.308). `py -3 tools/recall.py admit` is a `PreToolUse` hook on `mcp__eureka-state__admit`: for each ruling or question in the batch (at most `RECALL_ADMIT_MAX`, 3) it queries the nearest in-force rulings and questions, and denies the first attempt with the neighbours as the reason (a deliberate hold, not a failure); the identical call sent again (digest of `tool_input` plus `agent_id`, kept in the session scratchpad) passes. Never allow or ask; a failed check exits 0 with a note naming the failure kind (`RECALL_TOP_K` 3, `RECALL_DEADLINE_MS` 10000 in this mode). Standard library only; writes only the digest file. | the operator's `UserPromptSubmit` hook and `PreToolUse` hook on `mcp__eureka-state__admit` (fires inside subagents too) in `~/.claude/settings.json` | `references/campaign-state.md` (Precedent) | From c53f3462b4625d3edfaf8f4a0ad4bba267d28513 Mon Sep 17 00:00:00 2001 From: Eureka Date: Sun, 1 Nov 2026 01:52:54 +0100 Subject: [PATCH 07/24] doctrine: Persona pointers name the MCP put as interim and drop the claims s2 falsified (Soul s2 pointer-mcp-names, persona-md-every-turn, persona-md-voidbot-consumer) Co-Authored-By: Claude Opus 5.5 --- doctrine/gamecult.md | 2 +- doctrine/persona.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doctrine/gamecult.md b/doctrine/gamecult.md index 18f6d97..8172ad7 100644 --- a/doctrine/gamecult.md +++ b/doctrine/gamecult.md @@ -80,7 +80,7 @@ Odin-class architecture doctrine: ## Persona State Standard -Persona state uses `gamecult.persona_state.v0`. Huginn holds each Mind's canonical Persona state (`persona_get`/`persona_put`); VoidBot keeps only its projection and repo Personas until it is retired (`eureka-body:follow_up:voidbot-retirement`). Read `F:\Projects\gamecult-ops\docs\persona-state-standard.md` before any Persona-state work. +Persona state uses `gamecult.persona_state.v0`. Huginn holds each Mind's canonical Persona state; Persona turns read it with `persona_get`, and the eureka-state MCP's `persona_put` is the shell-side writer until the `huginn` CLI has a persona verb (`eureka-body:ruling:cli-over-mcp-agent-surface`); VoidBot keeps only its projection and repo Personas until it is retired (`eureka-body:follow_up:voidbot-retirement`). Read `F:\Projects\gamecult-ops\docs\persona-state-standard.md` before any Persona-state work. diff --git a/doctrine/persona.md b/doctrine/persona.md index 2fd859e..fb1df72 100644 --- a/doctrine/persona.md +++ b/doctrine/persona.md @@ -1,6 +1,6 @@ # Persona charter -The swarm's Persona turns (Huginn `mind-persona`) carry this charter in every turn; it also serves VoidBot's repo Personas. No Claude Code subagent wears it. The Persona State Standard is `docs\persona-state-standard.md` in the `gamecult-ops` repository. +The swarm's Persona turns (Huginn `mind-persona`) carry this charter in each turn they take (an idle wake takes no turn). No Claude Code subagent wears it. The Persona State Standard is `docs\persona-state-standard.md` in the `gamecult-ops` repository. ## Register From 775d8bcf39046651589f19e8047fc2db6484d6b9 Mon Sep 17 00:00:00 2001 From: Eureka Date: Sun, 1 Nov 2026 01:54:41 +0100 Subject: [PATCH 08/24] campaign-state: the repo set in force is the target's repos, else the campaign's Campaign row names the opening set; target row gains repos; cut_spec row gains reads. Bounds name target repos (16) and campaign repos (8). The cut-spec repo rule becomes the repo-set-in-force rule with how Self adds or removes a repo, and a Repos in force recipe is added. Co-Authored-By: Claude Sonnet 5.5 --- references/campaign-state.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/references/campaign-state.md b/references/campaign-state.md index 03b2bc1..978d99e 100644 --- a/references/campaign-state.md +++ b/references/campaign-state.md @@ -78,11 +78,11 @@ mismatch. The root is the campaign slug, or the instance for `instance`, |---|---|---|---|---| | `instance` | `self` | exists (`eureka:instance:self`) | instance, display name, host | | | `stewardship` | `.n` | Self | instance, repo, sequence, note | | -| `campaign` | `self` | Self | title, repos, working branch, target doc | | -| `target` | `r` | Self | invariants (`label`, `statement`), not in scope, canonical implementations, doc | | +| `campaign` | `self` | Self | title, repos (the opening set), working branch, target doc | | +| `target` | `r` | Self | invariants (`label`, `statement`), not in scope, canonical implementations, doc, repos (the whole set from this revision on; empty keeps the previous) | | | `question` | `