Skip to content

tally: the kit becomes a runtime-read, git-tracked file (ruling 4) - #420

Open
mecattaf wants to merge 10 commits into
mainfrom
tally/kit-runtime-read
Open

mecattaf wants to merge 10 commits into
mainfrom
tally/kit-runtime-read

Conversation

@mecattaf

Copy link
Copy Markdown
Owner

What

Ruling 4 (2026-09-16 evening): "Coordinator switching must NOT be required every time a Tally flow is added. A new kit entry / flow must load without a home-manager or NixOS switch. … The kit path must become a runtime-read, git-tracked location, not a store path baked at switch time."

FT-3 (#361, TL-18 / D-B18, #304) shipped the box's argv table as a pkgs.writeText store file, so every new flow cost a coordinator switch. This PR moves it out of the store.

  • home/dot_config/tally/kit.json — the committed 603-entry table, installed at ~/.config/tally/kit.json by config.lib.file.mkOutOfStoreSymlink into the dotfiles checkout (the motion home/home.nix already performs for every raw dotfile). The pinned lake's readKit is a readFileSync per pass, so an edit is live at the next timer wake.
  • services.tally-uplink.kit = "${config.home.homeDirectory}/.config/tally/kit.json" (the option is nullOr str). Rendered ExecStart carries --kit /home/tom/.config/tally/kit.json, and still no --plan.
  • tools/render-tally-kit.py renders the LOCAL-SMOKE trio plus the 3N CUBS entries (N = 200 as cubs-halogen-probe-1: cubs-iteration executable and kit entries (FRONT-12 bootstrap) #402, --dry NOT appended) deterministically; --check diffs the committed bytes. Supersedes the nix-rendered cubsEntries.
  • Every argv[0] is a stable per-user profile path/etc/profiles/per-user/tom/bin/{tally-local-smoke,cubs-iteration}. A hashed store path inside a git-tracked file goes stale on the next rebuild, and refreshing it would be the very switch the ruling removed. pkgs/tally-local-smoke promotes FT-3's localSmoke writeShellScript to a writeShellApplication (bytes unchanged, runtimeInputs deliberately empty so it runs under env -i); both packages join home.packages and are exposed as flake packages so the probe can realise the argv before the switch.
  • flake.nix tally-uplink-topology asserts flipped: hasPrefix "/home/tom/.config/tally/" cfg.kit, !(hasPrefix "/nix/store/" cfg.kit), builtins.pathExists ./home/dot_config/tally/kit.json, builtins.fromJSON (builtins.readFile …) parses and carries build:LOCAL-SMOKE + build:CUBS-1 and still not claude:headless, and ExecStart carries --kit /home/tom/.config/tally/.
  • tests/tally-uplink/probe-FT-3-kit.sh: K1/K3/K5 inverted, new K0 (the committed bytes are the rendered bytes and hold no store path), K2 reads the checkout file through the pinned lake's own readKit, K4 kept. tests/tally-uplink/probe-cubs-iteration.sh follows the kit to the checkout and to the stable argv[0].
  • Docs and ledgers: docs/local-ai/tally-uplink-input.md rewritten for the runtime-read kit (a new flow is a JSON edit + a commit); DECISIONS.md cites ruling 4 and records that Rule 9 (claude-transcript-mirror is two hand-written unit files and an untracked script — declare it (Rule 9) #293) yields for this file — the review moves from the store path to the git object, and the path on the box is a symlink into the checkout, so editing it is editing the repository — plus four "default, unruled" lines; DEFERRED.md gains DF-KIT-1 [OPERATOR], DF-KIT-2 [OTHER-REPO], DF-KIT-3 with the inherited red tails.

Why

~/.local/state/tally-rewrite/ledger.jsonl has 0 lease_grant rows ever. Adding a Tally flow or kit entry must never require a coordinator switch (ruling 4), and until this lands it did.

Base and ordering

Contains #402 (campaign/cubs-iteration, local head 25fe0dbb) by merge. Merge #402 first, then this shows only its own diff. It supersedes the nix-rendered cubsEntries that #402 added to home/tally-uplink.nix — the same 600 CUBS entries, same stdin pointer contract, same cwd, rendered by the checked-in tool instead. pkgs/cubs-iteration and the campaign's STDIN contract are untouched.

Oracle — rc and tails (MEASURED 2026-09-17, on this branch)

$ python3 tools/render-tally-kit.py --check
render-tally-kit --check: home/dot_config/tally/kit.json matches (603 entries)     rc 0
$ test "$(grep -c /nix/store home/dot_config/tally/kit.json)" -eq 0                rc 0
$ nix build --offline --no-link .#checks.x86_64-linux.tally-uplink-topology        rc 0
$ bash tests/tally-uplink/probe-FT-3-kit.sh                                        rc 0
  ok  K0 kit.json is the rendered output and names no store path
  ok  K1 kit = /home/tom/.config/tally/kit.json (runtime-read, git-tracked)
  ok  K2 readKit(pinned lake) over the CHECKOUT file: 603 refs; resolves the
         LOCAL-SMOKE / CUBS-1 / CUBS-200 trios; refuses claude:headless and CUBS-201
  ok  K3 argv[0] set is stable: /etc/profiles/per-user/tom/bin/{tally-local-smoke,cubs-iteration}
  ok  K4 one usage line, execution_id = exec-probe, under env -i
  ok  K5 ExecStart carries --kit /home/tom/.config/tally/… and no --plan
  ok  K6 nix build .#checks.x86_64-linux.tally-uplink-topology
  ENV K7 test-tally-uplink-input.sh red at clause A ONLY (see below)
$ bash tests/tally-uplink/probe-cubs-iteration.sh                                  rc 0  (C1–C11)
$ bash tests/tally-uplink/probe-u-d14-uplink.sh                                    rc 0

Runtime-read proved, no nix invoked. Adding build:HAND-EDITED-DEMO to the committed JSON and re-resolving through the pinned lake's readKit with the store paths already in hand printed REFS=604 and the new argv immediately; render-tally-kit.py --check then went rc 1 on the drift (the intended friction) and rc 0 again after revert.

The reds, verbatim, not laundered

Two oracles are rc 1 on this branch and byte-identically rc 1 on main 202d9c3 in a clean worktree — inherited, not introduced:

$ nix flake check --offline --no-build          # branch: rc 1   main: rc 1
       … while checking the derivation 'checks.x86_64-linux.nas-topology'
       error: assertion '(! ((builtins).elem 8731 (coordinator).networking.firewall.interfaces.wlp192s0.allowedTCPPorts))' failed

An earlier pass died first at the other one, which is an evaluation-order difference, not a second cause:

       … while checking the derivation 'checks.x86_64-linux.nas-personal-tailnet'
       error: path 'pl6rmijq3dkwqw9cf16wzpycsc7gb9m2-86byf0qaz7f0vgj7x4km4zc9q446skd0-source' is not valid
  • tests/tally-uplink/test-tally-uplink-input.sh — rc 1, clause A only (that flake check). Every other clause [P]. Same on main.
  • tests/tally-uplink/probe-FIX-E12.sh — rc 1, clause S5 only (the same assertion). Same on main. Not fenced: it is rc 1 here exactly as it is rc 1 on main.

probe-FT-3-kit.sh clause K7 carries one narrow [ENV] fence, recorded in DECISIONS.md as "default, unruled" and in DEFERRED.md DF-KIT-3 with both tails: it fires only when the suite's failing set is exactly {A}. A second failing clause is a hard FAIL, so a real regression cannot hide behind it. The alternative — a kit probe that can never be green while an unrelated repo-wide check is red — would retire it as an oracle for every later change.

What is left out (all written down)

  • DF-KIT-1 [OPERATOR] — the one coordinator switch that installs the symlink and the two executables. No overnight lane may home-manager switch / nixos-rebuild. Until it lands, services.tally-uplink.kit names a path that does not yet exist and the uplink fails legibly ("cannot read the kit …") rather than running with no argv table. After it, adding a flow is a JSON edit plus a commit and never a switch again.
  • DF-KIT-2 [OTHER-REPO] — ruling 4's second step: a kit.d/ directory (needs the lake's --kit <dir>, tally-ts-sdk#135, then a pin bump here), and per-item stdin for eval(...) entries (the same seam; the evaluator cells stay declared no-ops until G7 rules a mechanical verdict).
  • DF-KIT-3 — the two inherited flake-check reds above.
  • Not touched, by constraint: modules/tally-b.nix, home/seat-feeder.nix, home/dot_local/bin/tally-seat-feeder, tests/tally-b/**, docs/local-ai/tally-b-input.md (D-B owns them); pkgs/cubs-iteration and the campaign's STDIN contract.

Nothing was switched, started, armed or deployed; no live state under ~/.local/state/ was written; no credential was read.

🤖 Generated with Claude Code

mecattaf and others added 10 commits September 16, 2026 11:23
…ase (cubs-halogen-probe-1)

A writeShellApplication for the campaign's kit entries: block on
worker:8731/health until busy:false (10 s poll, 20 min deadline, then exit 69
and a receipt that says outage), one fresh `pi -p --mode json` process on the
task's own git worktree from ~/agency/<repo>'s current HEAD, the campaign's
diff guard (built-in fallback with the same rules), the task's validation
command under a 10 min timeout, exactly one repair process fed bundle + diff
+ transcript, a commit on pass (never a push), receipt.json in the campaign's
schema plus attempts.json, one usage line at $TALLY_USAGE_SOURCE_PATH, a
SIGTERM trap that commits WIP and writes a partial receipt within the
kernel's 30 s grace, and a 3-consecutive-fail fuse (exit 2). --dry prints
the plan; --help lists the exit codes.

Runs under the kernel's empty environment: HOME from the passwd entry, every
tool a store path, the two profile bins appended last for a validation_cmd
that reaches for nix.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…FRONT-12 bootstrap)

120 generated entries beside the untouched LOCAL-SMOKE trio: build:CUBS-<n>
runs the cubs-iteration store executable with cwd ~/mecattaf/cubs-campaign,
an empty env_allowlist, usage_source halogen-usage/1 under the rewrite
state's uplink/usage/cubs-*.jsonl, and a static stdin POINTER
{worklist, id} into worklists/current.jsonl that the script resolves; the
scope()/eval() cells are the existing noopEntry. A kit entry's stdin is
static bytes in a store file (lake kit.mjs, uplink.mjs:623), so the day's
task JSON cannot live there without a switch per day.

MEASURED: the rendered kit carries 123 entries; nix build
.#checks.x86_64-linux.tally-uplink-topology -> rc 0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the executable and the CUBS kit

Stub Pi (event stream + scripted edit) and stub Halogen (http.server) in a
scratch dir; throwaway git repos as the CUBS tree. Clauses C1..C11: usage
and exit codes, env -i --dry on the kit's own stdin, pass with commit +
usage line + ledger + idempotent rerun, fail + one repair, the fuse and its
reset, the spec.md guard, outage without touching the fuse, SIGTERM -> WIP
commit + cancelled receipt inside 25 s and a clean retry, the events
summariser, readKit over all 120 CUBS refs, the topology check.

MEASURED: PROBE cubs-iteration: PASS (all clauses).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… receipt, the exit codes, the two operator acts, DF-CUBS-1..5

docs/local-ai/cubs-campaign.md records what cubs-iteration does step by
step, the receipt field sources, the exit-code table, why the kit's stdin is
a pointer (kit.mjs / uplink.mjs:623 / exec.rs:747), the MEASURED kit and
check outputs, the hermetic oracle, one MEASURED real run against Halogen
(pass first try, 17 s, 4 tool calls, usage summed), and the two acts left to
Tom: the coordinator switch and the non-dry arm.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…per-attempt timeout -k 30, settings.json, newline gate)

From the smoke-pi-halogen lane (GO-WITH-FIXES): `pi -p` reads a non-TTY
stdin to EOF as prompt text, so every Pi process now gets </dev/null after
the task JSON has been read in full; each Pi process runs under
`timeout --foreground -k 30 <budget>` (1200 s first attempt, 900 s repair)
because a dropped Halogen connection leaves Pi's own auto-retry hanging;
the health gate uses `curl --max-time 5`; preflight requires
pi/settings.json beside models.json (compaction reserveTokens /
keepRecentTokens live there); a trailing-newline gate over the allowed
touched files follows the diff guard (git diff --check misses it); bash tool
calls are already counted per attempt in tool_call_names / attempts.json and
reasoning tokens in usage — the doc now says so. The probe gains clauses for
Pi's stdin, settings.json reachability, the bash count and the newline gate.

MEASURED: PROBE cubs-iteration: PASS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…g_tokens, attempts_path; timeout exit 124; stray_files emits exactly one JSON array under pipefail

stray_files() captured the guard pipeline and appended `|| echo '[]'`; the guard
exits 1 on any violation (including "empty diff" when nothing is untracked), so
under pipefail it printed two JSON values, `jq --argjson stray` failed, and the
receipt stayed `pending`. It now captures into a local and prints `${out:-[]}`.

probe-cubs-iteration.sh C8b: a tracked-file-only edit and a stray untracked file
both leave a terminal receipt that validates against the campaign's
tools/receipt.schema.json (stdlib validator). C6b/C6c reset the fuse so the
guard clauses no longer blow it for the clauses after them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… so campaign days 2-7 need no second coordinator switch

The kit now renders 603 entries: 200 labels x 3 cells, plus the unchanged
LOCAL-SMOKE trio. probe C10 resolves all 200 through the pinned lake's readKit
and refuses build:CUBS-201. docs/local-ai/cubs-campaign.md follows. The fuse
rule is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d, the exit-code table is reconciled

One global counter meant three consecutive failures ANYWHERE retired all 28
day-1 items: the plan put day one finishing at ~17%. The counter is now per
work package.

- <state>/fuse.d/<package> holds one package's consecutive failures. A fail
  increments only its own package, a pass resets only its own package, and a
  blown package retires only its own remaining items: three forced fails in
  WP1 leave WP2, WP3 and WP7 running.
- <state>/fuse stays the MASTER fuse and is now READ-ONLY to the executable,
  so `echo 3 > ~/.local/state/cubs-campaign/fuse` still stops every package
  (RETURN-CHECKLIST (g) is unchanged). Either fuse resets by removing its file.
- Receipts gain `censored` (required): true exactly on an UNRUN item a blown
  fuse retired before it started (no Pi process, terminal_status "fuse",
  exit 2). The task that BLEW the fuse ran and is censored false, so
  `jq -s '[.[]|select(.censored)]|length'` counts the unrun items exactly.
- A censored receipt never reached the preflight, so campaign_sha,
  skill_digest and bundle_digest are null rather than the empty strings the
  campaign schema's patterns reject; the schema now spells them
  ["string","null"] and the probe validates a censored receipt against it.
- --dry reports both counters and their paths. The exit-code table in
  STDIN-CONTRACT §4 is reconciled with the executable: earlier drafts folded
  fail and cancelled into exit 0 and named a 70 the script never returns.

Probe: 69 clauses, PROBE cubs-iteration: PASS (was 58). New C5b proves the
DoD directly — three forced fails in package WPA censor a fourth WPA item
while a WPB item passes in the same state dir, the master fuse then stops WPB
too, and jq over that state dir's receipts counts censored == unrun.

The campaign side (STDIN-CONTRACT.md §2/§3.1/§3.9/§3.10/§4/§5/§6 and
tools/receipt.schema.json) is edited in ~/mecattaf/cubs-campaign and left
uncommitted there for its own owner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ruling 4 of the 2026-09-16 evening session: "Coordinator switching must NOT
be required every time a Tally flow is added. A new kit entry / flow must
load without a home-manager or NixOS switch. ... The kit path must become a
runtime-read, git-tracked location, not a store path baked at switch time."

FT-3 (dotfiles#361, TL-18 / D-B18, dotfiles#304) shipped the box's argv table
as a `pkgs.writeText` store file, so every new flow cost a switch. This moves
it:

- home/dot_config/tally/kit.json — the committed 603-entry table, installed at
  ~/.config/tally/kit.json by `mkOutOfStoreSymlink` into the dotfiles checkout
  (home/home.nix's own motion for raw dotfiles). The uplink's `readKit` is a
  `readFileSync` per wake, so an edit is live at the next wake: a commit,
  never a switch.
- services.tally-uplink.kit = "${config.home.homeDirectory}/.config/tally/kit.json"
  (the pinned lake's option is nullOr str). The rendered ExecStart carries
  `--kit /home/tom/.config/tally/kit.json` and still no `--plan`.
- tools/render-tally-kit.py renders the LOCAL-SMOKE trio plus the CUBS 3N
  entries (N=200, as #402; `--dry` NOT appended) deterministically; `--check`
  diffs the committed bytes. Supersedes the nix-rendered `cubsEntries`.
- Every argv[0] is a STABLE per-user profile path —
  /etc/profiles/per-user/tom/bin/{tally-local-smoke,cubs-iteration} — because a
  hashed store path in a git-tracked file goes stale on the next rebuild and
  refreshing it would be the switch the ruling removed. pkgs/tally-local-smoke
  promotes FT-3's `localSmoke` writeShellScript to a writeShellApplication
  (bytes unchanged); both packages join home.packages and are exposed as flake
  packages so the probe can realise the argv before the switch.
  `grep -c /nix/store home/dot_config/tally/kit.json` == 0 is the fence.
- flake.nix `tally-uplink-topology` asserts flipped: the kit is the runtime
  path and NOT a store path, the committed JSON exists, parses through
  `builtins.fromJSON` and carries build:LOCAL-SMOKE + build:CUBS-1 while still
  not carrying claude:headless, and ExecStart carries
  `--kit /home/tom/.config/tally/`.
- tests/tally-uplink/probe-FT-3-kit.sh: K1/K3/K5 inverted, new K0 (rendered
  bytes, no store path), K2 reads the CHECKOUT file with the pinned lake's own
  readKit (resolves the LOCAL-SMOKE / CUBS-1 / CUBS-200 trios, refuses
  claude:headless and CUBS-201) and runs with no nix at all under
  TALLY_KIT_LAKE / TALLY_KIT_NODE, K4 kept.
- tests/tally-uplink/probe-cubs-iteration.sh follows the kit to the checkout
  and to the stable argv[0]; rc 0, C1-C11.
- docs/local-ai/tally-uplink-input.md rewritten for the runtime-read kit;
  DECISIONS.md records ruling 4 plus four "default, unruled" lines (Rule 9
  yields for this file); DEFERRED.md DF-KIT-1 [OPERATOR] the one switch,
  DF-KIT-2 [OTHER-REPO] the kit.d/ second step and per-item evaluator stdin,
  DF-KIT-3 the inherited repo-wide flake-check reds with their tails verbatim.

Contains #402 (campaign/cubs-iteration, local head 25fe0db) by merge.
Issues: dotfiles#361 (FT-3), dotfiles#304 (TL-18 / D-B18), dotfiles#293
(Rule 9, which yields here), dotfiles#362 (the Claude-seat ruling, untouched).

MEASURED: render --check rc 0 (603 entries); no store path in the JSON;
tally-uplink-topology rc 0; probe-FT-3-kit.sh rc 0 (K0-K6 ok, K7 [ENV]-fenced);
probe-cubs-iteration.sh rc 0; probe-u-d14-uplink.sh rc 0. `nix flake check
--offline --no-build` is rc 1 on this branch AND byte-identically on main
202d9c3 (checks.x86_64-linux.nas-topology, the 8731 firewall assertion) — no
NEW red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant