a73x

4f370210

chore: untrack the working files an add -A swept in

a73x   2026-08-30 20:19

Commit message
chore: untrack the working files an add -A swept in

.task8-report.md, RETRO.md, try.sh and the module-collapse plan were
untracked before this branch and are untracked again. Still on disk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSUFuYHqU9wr4J5NC8EkWV

.task8-report.md
Old New
@@ -1,145 +0,0 @@
1 # Task 8 — evidence-only silence
2
3 Branch `shape-first`, on top of 5f608210.
4
5 **The rule delivered.** A SILENT host never takes its pending panes. They ride
6 it out indefinitely wearing `unreachable` on their bars. Only EVIDENCE moves a
7 pane: a host's own reachable list either binds it (`bindTile` → `.connecting`)
8 or, by not naming it, vanishes it. The 2 s settle no longer collapses anything —
9 it gates the picker's auto-open on an empty wall and nothing else.
10
11 ## Changed, per file
12
13 ### `src/tui/wallview.zig`
14 - `State` gained `@"unreachable"` (spelled with `@""` — `unreachable` is a Zig
15 keyword; the bare field did not parse, which is the build break that was
16 handed over). `word()` returns `"unreachable"`, byte-identical to the picker
17 row's word, so a pane and its row read the same.
18 - `EndKey.waiting` removed, and the `if (t.pending) return .waiting;` line in
19 `endKey` with it: a pending pane has no pump at all, so `x` falls through to
20 `.drop` and closes the tile locally. The key-loop's `.waiting` arm (a refusal
21 notice) is gone.
22 - `run()`'s settle block reduced to `settled = true;` — the `collapsePending`
23 call, its `setFocus` repair and its `relayout` are deleted. Settle comment
24 rewritten to say it gates only the picker's auto-open.
25
26 ### `src/tui/wall_layout.zig`
27 - `collapsePending` deleted (13 lines). Nothing calls it.
28
29 ### `src/tui/wall_host.zig`
30 - New private `dressSilent(w, hi)`: under `paint_mu`, every present PENDING tile
31 owned by host `hi` that is not already dark gets `state = .@"unreachable"`,
32 then one `paintDeadBarsLocked` if anything changed. Tiles of other hosts, and
33 non-pending tiles, are untouched.
34 - `applyHostList`'s `if (reachable) { ...diff... }` gained `else dressSilent(w, hi);`.
35 - Verified in `client.zig`: `SessionPoll.run` stores `list_ready = true` on BOTH
36 the reachable and the failed path, so `applyReadyLists` does reach
37 `applyHostList` for a failed poll and `dressSilent` is live in production —
38 not only in the unit fixture. The e2e mutation below proves it end to end.
39
40 ### Tests
41 - `src/tui/wall_test_wall.zig` — the endKey pending test renamed and rewritten
42 for the new claim: `x` on a pending pane is `.drop`, dark or merely waiting.
43 - `src/tui/wall_test_layout.zig` — the two `collapsePending` tests deleted (77
44 lines). "a wall left before its hosts answered saves the shape it was given"
45 kept.
46 - `src/tui/wall_test_host.zig` — new: "a silent host's saved panes survive its
47 failed poll and wear unreachable". Two hosts, three pending panes (2 on host
48 0, 1 on host 1), off-origin rects from a real `relayout`, host 0's poll failed
49 TWICE (a pane that rides out one poll and goes on the next has ridden nothing
50 out). Asserts: all three still present, still pending, rects unmoved; host 0's
51 two read `.@"unreachable"`; host 1's still reads `.waiting`; the word is
52 `"unreachable"`.
53
54 ### `test/e2e_09_hosts.sh`
55 The "a daemon that goes and comes back re-creates NOTHING" leg. The down-host
56 half used to assert ZERO occurrences of the down daemon's socket path on the
57 settled grid. It now asserts the opposite claim:
58 - `--sock $SOCKH2#0 [unreachable]` and `--sock $SOCKH2#c [unreachable]` are both
59 on the settled grid (the two panes the previous wall saved);
60 - no line naming `$SOCKH2` reads `[up]` — nothing dialled a daemon that is not
61 there;
62 - unchanged: the reborn daemon's `mux d stats` reports `sessions=1`, and
63 `--sock $SOCKH2#c` is absent from the reborn wall's grid — its own list took
64 the pane, which is exactly the evidence rule.
65 The leg's block comment and its `ok` line were rewritten to match.
66
67 ### `CLAUDE.md`
68 - Wall bullet: added "Only a host's OWN list may take a pane: a saved pane on a
69 host that answers nothing stays, wearing `unreachable`, for as long as the box
70 is dark — an eight-pane setup is not worth one quiet machine."
71 - `Ctrl-\ x` bullet: "a tile that has never come up" now says "a saved pane on a
72 dark host included", and names the reason a pending pane qualifies (it has no
73 pump at all). The bullet already ended with "birthing onto an `unreachable`
74 row is a designed path", which stays true.
75 - Layout-sidecar bullet: untouched. "Healing is per-leaf against the hosts' live
76 lists" is already the evidence rule stated positively.
77
78 ### `docscheck.budget` / `docscheck.blocks`
79 **Neither file changed.** The first `zig build check` failed with
80 `wallview.zig: 64 comment blocks of 5+ lines, baseline 63` — the new settle
81 comment was five lines. It was shortened to three rather than raising the
82 baseline, which puts wallview back at exactly 63. `doc-report` confirms the
83 figures already in the files: blocks `wall_host.zig 7`, `wall_layout.zig 2`,
84 `wallview.zig 63`; budget `wallview.zig 0`, `wall_host.zig 0`,
85 `wall_layout.zig 890`. Nothing went up, so nothing needed signing.
86
87 ## Mutation verification
88
89 Every new assertion was broken once and watched fail, then restored with the
90 inverse edit (never `git checkout --`).
91
92 | # | Mutation | Assertion that fired |
93 |---|---|---|
94 | A | drop `t.state = .@"unreachable";` from `dressSilent` | `DarkHostsPanesDoNotSayWhichSetupIsDark` |
95 | B | drop the `ownedBy(t, hi)` filter (via `hi == 99`) | `OneHostsSilenceSpreadToAnother` |
96 | C | `if (reachable)` → `if (true)`, so silence drives the diff | `SilentHostTookThePanesTheUserSaved` |
97 | D | `if (t.pending) return .none;` in `endKey` | `WaitingPaneRefusedTheKeyThatCloseIt` |
98 | E | `if (t.state == .@"unreachable") return .none;` in `endKey` | `SilentHostsPaneCannotBeClosed` |
99 | F | delete `else dressSilent(w, hi);` (e2e) | `e2e FAIL: hosts restart: the pane the user saved on a dark daemon is gone, or does not say the machine is` |
100
101 A first attempt at B (deleting the `ownedBy` term outright) was caught by the
102 COMPILER as an unused parameter, not by the test — re-done as `hi == 99` so the
103 parameter stays used and the test is the thing that catches it.
104
105 ## Gates
106
107 - `deps/zig/zig build` — rc 0.
108 - `deps/zig/zig build test` — rc 0, 1067/1067.
109 - `deps/zig/zig build check` — rc 0 after the settle comment was shortened.
110 - `make check` — rc 0.
111 - `E2E_ONLY=09_hosts make e2e` — rc 0, 12 scenarios.
112 - `E2E_ONLY=12_panes make e2e` — rc 0, 10 scenarios.
113 - `E2E_ONLY=13_birth make e2e` — rc 0, 5 scenarios.
114 - `E2E_ONLY=01_boot make e2e` — rc 0, 12 scenarios.
115 - `make ci` — **rc 0**. `check` + e2e **100 scenarios, 38 convergence points** +
116 the agent suite + throughput (client 200000 lines attached 36ms against a 60ms
117 target; repaint 15097856 bytes detached 90ms against 200ms). Read out of
118 `/tmp/t8_ci.log`, not off a pipe's exit code.
119 - `make check` re-run on the COMMITTED tree — rc 0. The full CI run had started
120 about a minute before a final two-line comment reword in `endKey` landed, so
121 the committed bytes were re-gated rather than assumed. The reword is
122 comment-only and same-length, and `docscheck` is the gate that could have
123 noticed it.
124
125 ## Commit
126
127 One commit on `shape-first`:
128
129 06756d09 feat: a silent host keeps its panes, and only its own list may take one
130
131 The syntax fix was not split out — it is one line of the feature the branch was
132 mid-way through, not a change with a story of its own.
133
134 Not pushed, not merged, not installed.
135
136 ## Deviations
137
138 - The variant is `@"unreachable"`, not a rename to `dark` — the smallest diff, as
139 preferred in the brief. It appears at four use sites and reads fine.
140 - `CLAUDE.md`'s `Ctrl-\ x` bullet needed one clarifying phrase rather than only a
141 consistency check: "a tile that has never come up, which names no session to
142 end" was arguably false of a pending pane, which DOES name a session (the
143 sidecar's). The reason it drops is that it has no pump, so that is what the
144 bullet now says.
145 - The layout-sidecar bullet was left alone; it did not contradict anything.
RETRO.md
Old New
@@ -1,138 +0,0 @@
1 - 2026-08-22 agent-chan-timeout: the webhub filter read as contradicting the hub's own -A use case until the phase-2 issue showed the hub authors those frames; and the e2e leg's expect matched the pty ECHO of the typed needle, which only shows up once a wait is long enough to run the script ahead.
2 - 2026-08-22: wall "full of stuff" was not a config split but 45 dead tiles from hand-run perf/repro rigs using the real HOME; the make scripts isolate XDG_STATE_HOME, the scratchpad habit did not.
3 - 2026-08-22 in-band resize: three false oracles in a row — `wc -l` on a dump (trims blank rows), `stty size` as proof of SIGWINCH delivery (asks the kernel, not the app), and a stale `zig-out/bin` after `make ci` (which builds into .zig-cache) — plus `git checkout --` erasing the uncommitted fix with the mutation, exactly as the memory said it would.
4 - 2026-08-23 socket-path guess: e2e pins scenario ordinals by file position, so a leg added near the top renumbered eight narrative sentences — add legs at the end. Two background `make ci` runs reported exit 0 while make had failed: the notification reads the LAST command in the compound, not make.
5 - 2026-08-23 doc gate: the first budget shape (flag counts) passed green on a mutation that grew an already-flagged block — a count cannot see prose growing inside a flag it already knows about. Two hours later the same lesson landed twice: a `//!` header rule that skips headers, and a report nobody is accountable for, both "check that never ran" wearing new hats. Also lost a mutation test to a badly chosen target — mutating a decl that was ALREADY the file's only flag proves nothing, and read as "the gate is broken" for a minute.
6 - 2026-08-23 flake: the failing assert was buried under interleaved stderr from other tests, so the first read blamed a missing HOME that had nothing to do with it — read the stack line, not the nearest error text. Root cause took one termios dump: bash's readline owns the tty and eats `ESC [` as a numeric argument.
7 - Burning down a doc block is cheap; deciding what the *decl span* can afford
8 is the slow part. Drafting to a hand-counted byte target was wasted work —
9 the loop that paid was write-all, run `doc-report`, trim the three that
10 missed by under 20 bytes.
11 - The fan-out's reports never arrived — eight agents, two rounds of asking,
12 only idle notifications. The fragment FILES they were told to write carried
13 the entire usable result. Next fan-out: the deliverable is a path, not a
14 return value.
15 - I rebuilt docscheck into the scratchpad so agents could measure without a
16 build, and left `build.zig` out of the index that build.zig:492 explicitly
17 adds. It reported a tier-1 failure the real gate never would, and I relayed
18 it as a defect. A second implementation of the checker is a second thing
19 that can be wrong.
20 - The burn-down and the drift review are different passes and I nearly
21 conflated them. Making 279 comments shorter left all 31 lies in place —
22 including one crediting the symlink-unsafe xdg.makePrivateDir for code that
23 calls makeNewPrivateDir. Length is not truth.
24 - Verifying findings caught two bad ones: a 7-vs-8 byte bound that was only
25 right because param() maxes at 8, and a reviewer's own replacement text
26 claiming a torn cache leaves a permanently cold host when readCache refuses
27 it and the cold path rewrites it. Verify the FIX, not just the finding.
28 - I dispatched seven fix assignments for eight reviewers and left drift-proto
29 idle with two confirmed findings. Count the fan-out back in.
30 - 2026-08-23 predict local gate: worktree sandbox refused every compound Bash line (&&, pipes into tail), so each build/test went to a log file and a second call to read it; e2e needed three full runs for a count pin and a /tmp cleanup list that only the trap knows about.
31 - 2026-08-23 lost-session triage: reconstructed the session table from /proc and
32 per-name muxa probes because I never ran `muxd --help`, which lists `muxd
33 stats` — one no-attach query that prints every live session by name. Probing
34 by guess twice is the signal a capability exists; ask the binary before
35 building a workaround. README files it under "Everything else" as wire stats,
36 so the capability is indexed under the wrong question.
37 - 2026-08-23 01ef82fa: two grunt runs (opencode lost on server restart; pi sandboxed with no egress/tmp so it could not build) — sandboxed grunts write diffs, they cannot run this repo's gates; verify RED/GREEN locally by mutation.
38 - 2026-08-23 chunk1 e2e rounds 7-11: two days of green legs were luck, not health — the CLI-wall expect bet on a paint order the new painter never promised, and the dead-tile leg only ran once earlier failures stopped masking it; a suite that stops at first failure hides its own tail.
39 - 2026-08-24 chunk2 tasks 1-10: the brief "make check is your gate, leave e2e alone" pre-broke the suite for the next task, and the hand-written "N scenarios" echo hid a one-behind drift for four commits — a per-task gate must cover what the task can break, and a display restating a counter is a comment that rots; derive it.
40 - 2026-08-25 chunk3 delivery tail: the hands-on re-test stalled on my own demo command — argv on `mux wall` demotes it to an authored one-tile wall, so the user never saw the restore they were testing; the command you hand a tester is part of the deliverable, run it yourself first.
41 - 2026-08-25 add-tile prompt: two reviewers returned bare idle notifications (no report) — the first dispatch lacked the file-drop contract I already knew to demand; docscheck's span for a multi-line fn signature is the signature alone, and it bit two implementers before the brief said so; a scratchpad socket path is 112 bytes > sun_path, which cost one smoke run.
42 - 2026-08-25 max_sessions 4→32: the brief said "batch 7 under max_clients=8" — wrong table; dials contend for max_observers=4 and only the grunt running the helper against a real daemon caught it. A concurrency figure in a brief is a claim; have the implementer measure it.
43 - 2026-08-25 dead-tile marker: the user tested the fix before it was installed — I held `make install` for `make ci`. When the user is waiting to test, install first (reversible), gate before landing.
44 - 2026-08-25 landing: chained `rebase && branch -f && make ci` ran past a rebase conflict — CI started on a half-rebased tree and a branch got force-moved. Never chain past a rebase; one command, read its status, then act. Fixups written on later commits conflict on context when autosquashed early: fold the story instead.
45 - 2026-08-26 demo prep: the rig script assumed `muxa` could birth a session; it attaches at 0×0 and a 0×0 claim is a JOIN, so a named session came back "shell exited with 1" — the invariant was in CLAUDE.md, I had not connected it to setup; e2e's `fill_sessions` (piped `` into a headless `mux`) was the answer one grep away.
46 - 2026-08-26 web-restore: a worktree symlink (deps/zig) slipped into the first commit because .gitignore spells it `deps/zig/` — the trailing slash matches a directory, never a symlink. `git add -A` in a worktree is not safe on this repo; stage paths, or diff --stat against the ORIGINAL base before every commit.
47 - 2026-08-26 web-restore fix round: I asserted "one birth per connection" from reading my own code, never from watching a refusal — the daemon closes the fd on every refused attach, so the flag I cleared on .closed bounded nothing and `exit` forked a new shell. A comment stating a bound is a claim; run the path that would violate it before writing it down.
48 - 2026-08-26 web-restore land: merged on green gates + closed review; the user asked for a manual browser test AFTER the merge and it found a resurrection the suite cannot see — wsclient attaches 0×0 and never runs mux.js, so every mux.js behavior is untested by `make ci`. Drive the real client once per surface before merge; a fixture pins the protocol, not the page.
49 - 2026-08-26 web-exit-hold: swept the worktree symlinks into a commit AGAIN one round after writing a retro line about it — a retro line is not a guard. The fix is in .gitignore (`deps/zig/` with a trailing slash cannot match a symlink), not in my attention.
50 - 2026-08-26 upgrade chunk E: a mutation whose BUILD failed ran the previous binary and the leg went green — capture the build's exit before trusting any mutation result, and rebuild after every revert. Same family as checks-that-fail-green.
51 - 2026-08-26 upgrade autosquash: a fixup written at the tip spanned two commits (it changed a signature AND a later commit's call sites), so autosquash conflicted on the earlier target. Re-target to the LATER commit rather than resolving the conflict — the fixup is really a correction to the last thing it touches.
52 - 2026-08-26 upgrade demo: the full gate (80 e2e scenarios, unit tests, ci) was green on a feature that renamed every session but the last across an upgrade — every test upgraded a daemon holding ONE session, and one session cannot alias its neighbour. A by-value `slot.* orelse continue` made `name()` slice a dying stack copy. Demo before merge, always; and when a test fixture has N=1, the aliasing bugs are invisible by construction.
53 - relayout H1/H2: two rewrites of the H1 race test passed against the BUG — an invariant that only fires at quiescence had to replace one that never fired while relayouts were continuous. Phrase a race assertion for the quiet moment, not the busy one.
54 - 2026-08-26 review Mediums: four agents in worktrees, all green — but the brief's shared .zig-cache symlink cost an hour: one agent's ln -sfn ran with cwd=main and turned main's .zig-cache and deps/quic/{out,work} into self-links (85G cache gone, every build SymLinkLoop), a peer's GC evicted muxd under a running e2e, and one mutation went falsely green; plus the worktree tool cut branches 95 commits behind main. Per-worktree cache, ff-only first, merge-base check before pick.
55 - 2026-08-26 hygiene: `cd ../worktree && git merge` merged a branch into itself twice in one day and each time printed a believable tip; a merge is verified by a file existing in main, not by the log line. Also: make coverage had never finished — a green gate nobody runs is not a gate.
56 - 2026-08-27 cli-flags: a test that called usageExit wedged zig build test for 20 min — stdout is the build runner's IPC; hunt hangs by progress delta, not elapsed time
57 - 2026-08-27 cli-flags mux: a bare idle notification is not a report — the file drop carried it; e2e rc read from the log, not the task exit
58 - 2026-08-27 cli-flags rest: `git add -A` on a step commit swallowed the untracked RETRO.md into a refactor branch; stage the files the step names, never the tree
59 - 2026-08-27 cli-flags rest: rewriting a branch with rm/mv of the user's untracked file is classifier-blocked — do history surgery in a scratch worktree
60 - 2026-08-27 cli-flags reduce: the refactor was +141 src lines before the reduction pass took −60; measure per step, and budget the shrink as its own pass
61 - cliflags loop 2: the brief's home for `IdleMs` (`client.zig`) was unreachable — muxd sits at client's own layer and muxa below it. Reading `build.zig`'s layer table BEFORE writing the type would have saved a build round; the existing quic -> quic_client -> client re-export chain was the answer all along.
62 - 2026-08-27 cliflags loop 3: two comment edits blew the docscheck byte budget (mux_main 319b, main.zig 249b) and cost a rebuild each — the budget is bytes of doc block vs bytes of DECL, so a refactor that shortens a function body shrinks its comment allowance at the same time. Write the shorter comment first.
63 - 2026-08-27 cli-flags loops: three survey-driven reduction loops (−60/−21/−40) paid back a +141 refactor; the third survey rejected 12 of 19 candidates on numbers — that is the signal the loops are dry
64 - 2026-08-27 v0.0.1-15: smoked dist/release/bin that does not exist — RELBIN is dist/vX; read the Makefile var before hand-smoking
65 - 2026-08-27 daemon wedge: the blocking readFrame in the pump was named as a hazard in a comment (server.zig POLLOUT arm) long before it bit — a comment that says "this would hang" is a test that was never written; and the plan put setCloexec inside initFromManifest, which broke rollback (old binary re-exec'd without its fds) — the right placement was found by running the upgrade e2e group, not by reading
66 - 2026-08-27 nonblocking frames: the review's 'two hand-kept fd lists' finding cost more to argue than to fix — close_range in the child retired the class in 6 lines; ask 'what needs no list' before adding a second list.
67 - 2026-08-27 (task 4, wall-of-hosts): an unanchored inverse sed reverting a mutation rewrote six unrelated `return true;` lines — anchor the revert on the whole function body, not on the mutated line alone.
68 - 2026-08-27 (task 4 fix round): a mutation revert built on str.replace with an EMPTY replacement inserted two lines at byte 0 and silently broke three later mutants; snapshot the file's bytes and write them back, never reverse-replace.
69 - Task 5 (wall of hosts entry): the pipe-captured wall never paints a second host's stripe, so a headless capture read as 'otherHosts did not run' — three runs lost before a real pty (ptyclient) settled it in one. Reach for the pty fixture the moment a wall claim is about layout.
70 - Task 6 (wall-of-hosts chords): the two-commit split cost a full extra `make check` cycle because the compiler forced one deletion (`forget_err` unused) into the feature commit; filtering the diff into two patch files and verifying commit 1 under `git stash --keep-index` was the only way to know it built. Splitting by hunk after the fact is slower than sequencing the edits in commit order up front.
71 - Task 7 (wall-of-hosts e2e): a whole afternoon went on inferring TWO product bugs from failing legs before measuring them. The decisive move both times was cheap and late — run the SAME e2e group against the installed v0.0.1-15 client (pass) and the branch client (fail), then bisect the difference with a 30-line hand rig. Reach for the last known-good binary as the second step after a leg fails, not the twentieth.
72 - Task 7 (rulings round): the lead's "bisect, don't hypothesise" was right for the WRONG reason I expected — bisecting the SYMPTOM (a quic snapshot count) is confounded by every other change on the branch, but bisecting the ROOT behaviour I had already measured ("does a piped mux write a layout sidecar?") took four steps and named the commit. Pick the bisect predicate from what you have measured, not from what failed.
73 - Task 7 (fix round 1): a hand rig ran against a stale zig-out left by git bisect and attached three times to the developer's LIVE daemon, because bare `mux` on a binary without `hosts` falls through to $XDG_RUNTIME_DIR/muxd.sock. Two habits, not one: `zig build` before any rig (make ci builds .zig-cache, not zig-out), and export XDG_RUNTIME_DIR to scratch in every rig, not just XDG_STATE_HOME — the fallback path is the one that finds the real daemon.
74 2026-08-28 fix wave: wallview's docscheck budget is EXACT and only falls — a one-line struct decl is exempt from tier 3, so widening HostSpec into a multi-line struct with a field doc cost 163 bytes I then had to win back. Check the shape of a decl before adding a field to it.
75 - 2026-08-28 wall-of-hosts: 8 tasks, 10 fix rounds, 23 rulings; what slowed us was the plan carrying line numbers and code that drifted under each task (three plan defects found by reviewers, one by the compiler), and messages crossing with implementers mid-round — the ledger, not memory, is what kept it straight.
76 - 2026-08-28 demo rig: a unix socket path caps at 107 bytes, so the scratchpad is too deep for XDG_RUNTIME_DIR; and test/ptyclient execs argv[0] verbatim (absolute path, no PATH) — two silent exits before the wall opened.
77 - Task 1 (stripes go): docscheck's tier 3 counts only blocks HEAVIER than their decl, so deleting a field's checks LOWERED a decl and pushed a fine comment over budget — the figure went up, not down. Deleting code can raise a prose budget; check doc-report before assuming which way it moves.
78 - Task 1 fix round: inserting a function directly ABOVE another silently gave my new fn the neighbour's doc comment and left the old one undocumented — docscheck caught it as a 561-byte block, not as the lost rationale it really was. Insert below, or move the comment with the function.
79 - Task 2 (daemon lives until muxd stop): the only single-threaded way to observe "the pump did NOT end" is the pump's own return value — which the change deletes. Cost a rethink to land on run() on a thread with the assertions made over the socket.
80 2026-08-28 host picker: two full e2e runs (~50 min each) because a late fix landed mid-run — the suite's cost makes 'edit while it runs' a false economy; and the popup's 1Hz repaint silently broke every ptyclient `settle` until the frame was hashed.
81 2026-08-28 picker fix round 1: two review findings (clear the auto flag on a user close / keep the popup once on an empty wall) shared one boolean and cancelled each other — the e2e caught it in 40s, the review could not. Split the flag, name the failure in the test.
82 2026-08-28 picker fix round 2: my round-1 guard read a flag the other thread owns instead of asking the object whose state it was — and armed a retry with no lifetime. Both invisible to check + two e2e groups. Guards belong in a named function with a mutation-checked test, not inline in a thread loop.
83 2026-08-28 picker fix round 3: my round-2 guard ran only on the claim's REFUSAL, so it missed the arm that mattered — the one retried after the popup closed. Two rounds lost to guarding a sub-case. And a guard inline in a detached thread's loop cannot be mutation-checked at all: making claimFocus the one door was what made the pin bite.
84 - 2026-08-28 fixwave: the m3 control assertion failed first time because the notice
85 survives the call by design — the test had to consume it the way the pump does.
86 - 2026-08-28 host picker: three fix rounds on one focus-claim guard, every round green on every gate — a guard inside a detached thread's loop is neither reviewable nor testable; what stuck was one door (claimFocus) plus a mutation check on the production line, not a better comment.
87 - 2026-08-28 no-autostart: docscheck tier 3 bounced three commits before I
88 internalised that it weighs a `///` block against the decl's CODE bytes only
89 — comments in the body are free, so rationale belongs there and the doc
90 block has to stay under ~2 short lines for a small fn. Second drag: renaming
91 `report_fallback` to `asked` was not a rename, because `pumpTile` cleared it
92 before the FIRST dial; the flag had to be re-plumbed (spent by `dial`, unset
93 on the stored spec) before a picker Enter could reach `openHandoff` at all.
94 - 2026-08-28 no-autostart: the review found the bug moved (poll → picker-born tile redial) because 'asked' was spent on a copy no redial reads; a flag cleared downstream is one a new road misses — clear at the construction site.
95 - 2026-08-28 no-autostart demo: I recorded the user's finishing answer as "merge" when it is "demo" — a standing answer is a memory to confirm, not infer; the demo rig's first run died on a 120-byte socket path (scratchpad paths exceed the 107-byte unix cap — short XDG_RUNTIME_DIR, always).
96 - 2026-08-28 client fix-round: 20 min lost to two 10-minute `zig build test`
97 hangs. A unit test asserted `hostsMain(… "--help") == 0`, and `cliflags.help`
98 writes the usage to STDOUT_FILENO — which under the build runner IS the test
99 runner's protocol stream, so the runner waited forever. A wedged test step
100 prints nothing, so it reads as "slow build". mux_main's own `printOut`
101 comment already records the rule; I did not read it before writing the test.
102 Assertions about anything a binary prints to fd 1 belong in e2e.
103 - Server fix-round: proving H1 red cost a 7-minute wedged `zig build test` (RC=124, zero output) — the only signal a blocking-write hang gives. Worth budgeting a `timeout` around any red run whose failure mode is a hang, not an assertion.
104 - 2026-08-28 tests fix-round: the review's cheap-looking L1 (`[refused]` e2e) cost three runs to place — a picker birth is refused by the WALL (rows/aspect) long before the daemon ever sees it, so the leg only exists on a tall narrow terminal against a full CLIENT table. Read which layer refuses before writing the fixture.
105 - step2a folder move: the folder rule the brief specified is broken by the folder map the same brief fixed — client.zig imports interact.zig, and both placements were binding. Cost a round of thinking to resolve as a signed exemption rather than a silent weakening; a brief that states a rule and a layout should say which one yields.
106 step2b (wallview split): Zig refuses a local that shadows a file-level decl, which is what made a mechanical name-qualifier safe — but NOT for a nested container: a struct method named `run` became `fn wv.run`. Guard declaration sites, and prove the generator by round-tripping it to a byte-identical original before trusting one line of its output.
107 - 2026-08-28 (one binary): the fork-into-run mechanism the brief specified cost ~2h — it crashed every Debug daemon through a private pid cache in std.debug.MemoryAccessor, and the panic ate its own stack trace, so the diagnosis needed strace + coredumpctl rather than the log. What would have saved it: running the product by hand ONCE right after the change, before the e2e told me at scenario 21.
108 - 2026-08-29 09_hosts flake: two defects hid behind one symptom line; the byte ORDER of a failing capture (grep -aob) named the race in minutes, the src diff read could not — and a loop script that globs /tmp residue without a pid key moves other agents evidence (twice).
109 - Step 4 (ssh as argv): the doc gate caught three dead `handoff.sshLine` citations I would have shipped; grepping for the symbol before renaming would have been cheaper than the check round-trip.
110 - 2026-08-29 fix round: two comment-budget stalls cost a check cycle each — a new `///` block outweighs a small decl and a header edit crossed the 1024b cap; rationale that belongs to a mechanism goes in `//` inside the body, and the doc gate says so before I do.
111 - 2026-08-29 simplify: slowed by (1) a stale .xversion/new prefix — `make xversion` never rebuilds, one false red; (2) fork-into-run crashing on std.debug's cached pid, found only after a daemon died minutes in; (3) a pre-existing wall paint race the one-binary timing exposed as a 3/12 flake; (4) src LOC went UP under a 'reduce LOC' commission — every rule, pin and split costs preamble, and the only big win was docs.
112 2026-08-29 chunk B: docscheck tier 3 fired three times on decls that SHRANK (a shorter qualifier, a folded body) — the budget is a ratio in disguise, so every fold costs a prose cut nobody planned for; and the one mutation that proves the blocked-stream retry wedges the runner for 10 minutes instead of failing, so verifying it costs a full timeout.
113 2026-08-29 chunk C: docscheck tier 3 measures a decl by its SIGNATURE when the params are one-per-line — so a multi-line signature caps its own doc at ~110 bytes however long the body is, and three folds had to trim rationale that the body would have paid for on one line; and a blind `&w` -> `w` regex silently ate the address-of in `&w.tiles[at]` and rewrote ten comments, which only the compiler caught — substitute inside a function body by code line, never file-wide.
114 - chunk D1: docscheck's tier-3 span stops at a multi-line signature's last argument, so a doc block over a wrapped `pub fn` is measured against ~99 bytes of declaration; the fix that keeps the prose is a single-line signature, and I lost a round finding that out. dup2.py's product/test split is also wrong for webhub.zig — unbalanced braces inside JSON string literals keep its depth counter off zero, so every test in that file counts as product.
115 2026-08-29 dedup: slowed by (1) docscheck tier-3 shaping code — three fix rounds were about prose relocated to dodge an exact byte budget, and the gate measures a wrapped signature wrong (issue 0025a395); (2) a mechanical rename rewrote four user-visible notices and stayed green through ci+xversion — no notice was byte-pinned; (3) area-partitioned finders double-counted (1,780 claimed → 730 verified) until a collator grepped the whole tree; (4) a background `make ci` was SIGTERMed mid-e2e — foreground with a 600s timeout is the only reliable gate.
116 - 2026-08-29 D2 (retire wall file): the brief contradicted the invariant on a stopped daemon (step 7 vs 'unreachable removes nothing') — write the e2e steps FROM the invariant list, not from intuition; the implementer caught it, a reviewer would have too, but a self-consistent brief costs nothing.
117 - 2026-08-29 collab triage: 48 issues, 7 dead — every one named a symbol the src/ split or D2 removed; a title that names a file path rots on the next move, name the behaviour
118 - 2026-08-29 handoff --start: two mutations passed GREEN for reasons that had nothing to do with the code. (1) `grep -oE "^error: '[^']*' failed"` misses every test whose NAME contains an apostrophe — three mutation rounds read as "the test does not bite" when it did; grep for the test name, not for a quoted pattern. (2) `std.testing.expectError` renders the success value with `{any}`, and a live `Transport` holds an allocator vtable that formatting FAULTS on: the runner died inside its own message, named no test, and took every later test in that binary with it — a caught mutation that looks like an unrelated crash. Also: a one-line usage addition broke an e2e panic-bound 11 groups away (complaint + usage <= 15 lines); the usage literal is load-bearing, grep the suite for it before growing it.
119 - 2026-08-29 handoff --start fix round: a leg's cheap first assertion (exit code) MASKED its OS-side ones under mutation — "the mutation is caught" is not "the oracle bites". To find out which assertion owns a claim, relax the ones above it and re-run; here rc, then (a) pid, then (c) default-socket each fired in turn, and only (c) named the actual defect.
120 - 2026-08-29 handoff --start round 2: deleting a guard silently invalidated a comment two decls away — `announceOrStart`'s `start_argv.len == 0` check was what made "empty means nothing to start" TRUE, and the fold took the guard and left the sentence. When a fold deletes a condition, grep for the prose that condition was holding up. Also: widening a rule (`quiet` from one spawn to every spawn) does not widen the test that pins it — the loop has to gain the dimension too.
121 - 2026-08-29 handoff --start: the brief spelled the ensure's argv as a literal (`&.{}`) instead of naming the existing helper's shape (ensureForAttach forwards --sock); the implementer built the literal faithfully and it was a stray-daemon bug — name the sibling, not the bytes
122 - 2026-08-29 handoff step table: the SCENARIO pin and the CONVERGENCE pin are two numbers in the same file, and adding one leg moves both — `E2E_ONLY=04_handoff` never checks either (it prints "the pin is the whole suite's"), so a group-scoped run went green and the full `make ci` cost 7 minutes to say "38, the pin says 37". Bump both pins the moment an `assert_converged` lands. Also: an `s.field.?` in a test PANICS rather than failing, which kills the binary and hides every later test that names the same defect — `x != null and eql(x.?, …)` keeps the catch legible (this is the same shape as last round's expectError fault). And a policy extracted into a table is not free: the driver grew 27 product lines while the table cost 135, so "roughly flat" was wrong by 162 — testability was the purchase, not brevity.
123 - 2026-08-29 handoff step table, fix round: (1) `git rebase -i --autosquash` on a THROWAWAY branch also rewrote the real branch — git >=2.38 `rebase.updateRefs` moves every ref pointing inside the rebased range, and the branch I was protecting pointed at the temp branch's own tip; verify with `git branch -v` after any rebase, not just `git log`. (2) A `--fixup` cannot target a commit whose successor uses the fixed-up lines as diff CONTEXT: two of six comment blocks had to be demoted from the table commit to the row commit, and the first autosquash found it as a `UU` conflict, not the review. (3) The review's "delete 33 comment lines to save 33 product lines" was arithmetic against the wrong model — `dup2.py` skips every `//` line, so the comment count and the product count are disjoint; when a size finding cites a tool's number, read that tool before believing the projection.
124 - 2026-08-29 handoff table: the hands-on rig named its host 'box', so the warm QUIC dial failed DNS and read as a product bug for one run — a demo host must resolve (127.0.0.1), the e2e already knew
125 - 2026-08-29 handoff demo: rigs isolated XDG_STATE_HOME and XDG_RUNTIME_DIR but not XDG_CACHE_HOME — the per-host QUIC cache leaked 4 throwaway entries into ~/.cache/mux/hosts and a demo 'cold' attach warm-hit another rig's daemon; isolate every XDG dir mux reads (state, runtime, config, cache)
126 - 2026-08-29 publish: server already held v16 (published by this account 76 s earlier, not this session) — `release publish` refused; had to download the tarball by guessing the URL and cmp the binary to prove it was the same build. A `git collab release fetch` verb, or `publish` printing the server sha of the BINARY, would have made it one command.
127 - 2026-08-29 askpass: two hours went to an e2e whose tiles came up over QUIC. The first poll's `mux d endpoint` announced a port and key, the client cached it, and every dial after that was a UDP handshake with no ssh in it — so the fixture was green about a prompt that was never raised. A fixture that exercises the ssh path has to make the remote unable to announce; "the shim ran once" was the only symptom, and it read as a broken shim for far too long.
128 - 2026-08-29 askpass fix round: a mutation that does not fire can mean the LEG is switched off, not that the code is right — `tiles[0].pre = null` passed green because the entry-dial leg ran on a pipe, where no listener exists at all. A leg asserting that a mechanism was NOT used has to run where the mechanism is armed, or it agrees with itself.
129 - 2026-08-29 ssh-reason/askpass: three demo failures were all rig, not product (no session until an attach; keys before first attach lost; config-home-as-dir gave the remote a key and silently removed the prompt path); the e2e report had warned about the last one — read the report's CONCERNS before writing a rig. pgrep -f self-match killed the tool shell a second time: ps|grep -F|grep -v grep, always.
130 - 2026-08-30 git-collab: `issue open` prints auto-sync ref names, not the new id; I read an archive ref's prefix as the id and labeled a closed issue twice (4 syncs to undo). Take the id from `issue list | grep -F <title>`.
131 - 2026-08-30 retro: responsibility bled (sun_path rule in 5 places, 3 daemon starters for 2 callers) because we never named who OWNS a noun before changing code; review checked each hunk's correctness, never its shape. Trial: next feature in 3 worktrees — control / ownership-map-first / inline-no-subagents — judged on net lines, pub names ±, rule-sites, user's read.
132 - 2026-08-30 ways-of-working trial: map-first caught the 2nd fork + log mode the brief missed; a map row that asserts code ("same connect") shipped a bug — verify map rows like review findings
133 - 2026-08-30 ownership map adopted as the method; the map row itself is the thing to verify first (one false row = one shipped bug)
134 - 2026-08-30 ssh-reason+askpass rebase: the only conflicts were prose and one test argv, but I still checked out the branch before reading its CLAUDE.md — the file swapping under me (`d run` vs `d start -d`) cost a re-read. Diff the doc bullets across the two tips first, then rebase.
135 - Task 8 (comment-essay ratchet): the tool's own //! header sat 5 bytes under its 1024 cap, so documenting a new tier meant trimming two sentences first — a gate whose contract has no room left to state the gate.
136 - 2026-08-30 module-collapse: two plan claims died on contact with the compiler (probe grep wording; relative imports cannot leave a module root dirname) — spike one-file probes of toolchain claims before writing them into step text.
137 - 2026-08-30 shape-first: a stale .zig-cache mux binary reproduced the OLD behavior perfectly in the hands-on rig; and rail_cols reads a pane's label bar as a rail — both cost a debug loop each
138 2026-08-30 evidence-only silence: `unreachable` is a Zig keyword, so the handed-over State field would not parse — `@"unreachable"` fixed it in one sed, but the 5-minute lesson cost more later: the settle's replacement comment was five lines, which tripped docscheck's block ratchet at the END of the run rather than when it was written. Bigger drag was serial verification: six mutations, each a full `zig build test` or a 2-minute e2e group, then a ~35-minute `make ci` I polled instead of trusting the monitor.
docs/superpowers/plans/2026-08-30-module-collapse.md
Old New
@@ -1,442 +0,0 @@
1 # Module-Table Collapse Implementation Plan
2
3 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
5 **Goal:** Collapse mux's 41-entry build module table to ~15: six domain modules whose child files are relative imports, shared leaves stay tabled.
6
7 **Architecture:** A module in the table = an independently owned component; a file consumed by one domain only becomes a relative `@import("file.zig")` child of that domain's root, reachable from outside (when needed) via `pub const` re-exports on the root. Zig's one-file-one-module rule then blocks cross-domain file grabs at compile time. One file moves (`predict.zig` → `src/tui/`), making "prediction is an overlay" structural: `term` cannot import `wall`.
8
9 **Tech Stack:** Zig 0.15.2 (vendored: `deps/zig/zig`; system zig will NOT build this). Gates: `make check` per task, `make ci` + `make xversion` at the end.
10
11 **Spec:** This plan is its own spec. The agreed end state:
12
13 | Module | Root | Children (leave the table, become relative imports) |
14 |---|---|---|
15 | `term` | `src/engine/term.zig` (new) | `protocol` `engine` `delta` `replica` |
16 | `daemon` | `src/server/server.zig` | `quic_server` `cmd` `shellint` `upgrade` (+ `server_agent` `server_sessions`, already children) |
17 | `client` | `src/client/client.zig` | `client_core` `hosts` `handoff` `layout` `keymap` `askpass` |
18 | `wall` | `src/tui/wallview.zig` | `interact` `paint` `select` `predict` (+ `wall_*`, already children) |
19 | `agent` | `src/cli/muxa.zig` | — |
20 | `mux` | `src/cli/mux.zig` | `main.zig` `mux_main.zig` `webhub_main.zig` `webhub.zig` |
21
22 Shared leaves stay tabled (≥2 domains consume them): `quic` `proxy` `xdg` `sockpath` `spawn` `pty` `cliflags` `testtmp`. Test fixtures stay tabled: `script` `rawmode` `delaypipe` `render` `ptyclient` `wsclient`.
23
24 Final production import lists (this IS the layer graph):
25 - `term` → nothing (plus the `ghostty-vt` dep edge, wired outside the table)
26 - `daemon` → `term` `pty` `quic` `proxy` `xdg` `sockpath`
27 - `client` → `term` `quic` `xdg` `sockpath`
28 - `wall` → `term` `client` `spawn` `proxy` (hosts/handoff/layout/askpass/keymap/core are reached as `@import("client").X`; proxy for ignoreSigpipe — ruled in final review, the header had omitted what Task 1's own row spec included)
29 - `agent` → `term` `quic` `xdg` `sockpath` `cliflags`
30 - `mux` → `term` `daemon` `client` `wall` `agent` `quic` `proxy` `xdg` `sockpath` `spawn` `cliflags`
31
32 ## Global Constraints
33
34 - Build ONLY with `deps/zig/zig` (the Makefile already points at it). Never system zig.
35 - `make check` green before every commit; capture `$?` before piping (`make check; echo rc=$?`).
36 - NO directory reorganization. One `git mv`: `src/engine/predict.zig` → `src/tui/predict.zig`. Every other file stays put.
37 - `docscheck.budget` keys are basenames — the predict move needs no budget edit. New files (`term.zig`) need a budget line; if `zig build check` reports a mismatch after comment edits, run `deps/zig/zig build doc-report 2>&1 | tail -20` and set the line to the reported figure. Lowering is free; raising an existing file's figure is forbidden without user sign-off.
38 - Comments say *why*, not *how*. When editing import blocks, keep existing comments attached to their imports. `zig build check` verifies comment symbol refs resolve.
39 - `test_order` in build.zig must cover every `mod_table` row exactly once (comptime-enforced) — every row you delete from the table must also leave `test_order`.
40 - The layer check is comptime: imports point strictly downward. When a row gains an import, its `.layer` must exceed the dep's. Renumber only in the touched rows; final renumbering happens in Task 6.
41 - Commit per task, message style matches the repo (`refactor: <narrative subject>`).
42 - A unit test that writes to stdout wedges `zig build test` silently — if `make check` hangs with no output for minutes, suspect that first.
43
44 ---
45
46 ### Task 0: Branch
47
48 - [ ] **Step 0.1:** `git -C /home/xanderle/code/rad/mux checkout -b modules` (from main, which must be clean apart from RETRO.md).
49
50 ---
51
52 ### Task 1: Wall collapse
53
54 `interact`, `paint`, `select` become child files of the `wallview` module; `predict.zig` moves to `src/tui/` and becomes a child too. Rename the module `wallview` → `wall`.
55
56 **Files:**
57 - Move: `src/engine/predict.zig` → `src/tui/predict.zig` (git mv)
58 - Modify: `src/tui/wallview.zig`, `src/tui/interact.zig`, `src/tui/wall_picker.zig`, `src/tui/wall_layout.zig`, `src/tui/wall_pump.zig`, `src/tui/wall_test_pump.zig`, `src/tui/wall_test_wall.zig`, `src/tui/wall_test_picker.zig`, `src/cli/mux_main.zig` (imports `wallview` by name), `build.zig`
59 - Also check: `grep -rn '@import("wallview")' src test` and rewrite every hit to `@import("wall")`.
60
61 **Interfaces:**
62 - Consumes: current table rows `interact`, `paint`, `select`, `predict`, `wallview` (build.zig:220–294 region).
63 - Produces: table row `.{ .name = "wall", .path = "src/tui/wallview.zig", ... }`; no re-exports needed (nothing outside tui consumes interact/paint/select/predict).
64
65 - [ ] **Step 1.1: Move predict**
66
67 ```bash
68 git mv src/engine/predict.zig src/tui/predict.zig
69 ```
70
71 - [ ] **Step 1.2: Rewrite name-imports to relative imports inside src/tui/**
72
73 Every tui file importing a now-child sibling switches to the `.zig` relative form. Exact current sites (re-grep before editing; line numbers drift):
74
75 ```bash
76 grep -rn '@import("interact")\|@import("paint")\|@import("select")\|@import("predict")' src/tui/
77 ```
78
79 In each hit, change `@import("interact")` → `@import("interact.zig")`, `@import("paint")` → `@import("paint.zig")`, `@import("select")` → `@import("select.zig")`, `@import("predict")` → `@import("predict.zig")`. Sites as of writing: wallview.zig:29,30,35; interact.zig:26,30,32; wall_picker.zig:12; wall_layout.zig:8; wall_pump.zig:10; wall_test_pump.zig:5; wall_test_wall.zig:5; wall_test_picker.zig:5.
80
81 - [ ] **Step 1.3: Chain the new children's tests from the root**
82
83 `src/tui/wallview.zig` ends with a `test { ... }` block (currently ~line 2416) listing `wall_test_*`. Add the four new children so their `test` blocks stay in the suite:
84
85 ```zig
86 test {
87 _ = @import("interact.zig");
88 _ = @import("paint.zig");
89 _ = @import("select.zig");
90 _ = @import("predict.zig");
91 _ = @import("wall_test_harness.zig");
92 // ... existing lines stay ...
93 }
94 ```
95
96 - [ ] **Step 1.4: Edit the module table**
97
98 In `build.zig`:
99 1. Delete rows `interact` (line ~248), `paint` (~220), `select` (~231), `predict` (~170).
100 2. Rename the `wallview` row to `wall` and merge in the deleted rows' import needs (interact consumed `replica`, `client_core`, `keymap`; paint consumed `engine`, `protocol`). New row:
101
102 ```zig
103 .{ .name = "wall", .path = "src/tui/wallview.zig", .layer = 4, .link_libc = true, .imports = &.{ "protocol", "client", "hosts", "handoff", "proxy", "engine", "replica", "client_core", "keymap", "layout", "askpass", "spawn" }, .test_imports = &.{"testtmp"}, .quic_tests = true },
104 ```
105
106 3. Remove `interact`, `paint`, `select`, `predict` from `test_order` (~line 816); rename `wallview` → `wall` there.
107 4. `grep -n 'idxOf("wallview")\|idxOf("interact")\|idxOf("paint")\|idxOf("select")\|idxOf("predict")\|"wallview"' build.zig` — rewrite every named-handle reference to the surviving names (`idxOf("wall")`); delete wiring that only served deleted rows.
108 5. `src/cli/mux_main.zig` row (`client_main`, ~307): change `"wallview"` → `"wall"` in its imports; change the source line `@import("wallview")` → `@import("wall")` in `src/cli/mux_main.zig`.
109
110 - [ ] **Step 1.5: Gate**
111
112 ```bash
113 make check; echo rc=$?
114 ```
115 Expected: `rc=0`. If docscheck complains about `predict.zig`'s folder flags, re-read the error — budget keys are basenames, so a failure here is something else; fix what it names.
116
117 - [ ] **Step 1.6: Commit** (BEFORE the mutation probe — a probe against an uncommitted tree cannot be reverted safely; the baseline must be committed first)
118
119 ```bash
120 git add -A && git commit -m "refactor: the wall owns its interaction loop, painter, selector and overlay as child files"
121 ```
122
123 - [ ] **Step 1.7: Watch the new guard fail once (mutation check, never committed)**
124
125 The collapse's safety claim: cross-domain file grabs are compiler-blocked. Prove it fires:
126
127 ```bash
128 echo 'const sneak = @import("../client/hosts.zig");' >> src/tui/interact.zig
129 make check 2>&1 | grep -i 'file exists in modules' ; echo probe=$?
130 git checkout -- src/tui/interact.zig
131 git status --porcelain src/tui/interact.zig
132 ```
133 Expected: the grep finds Zig's file-in-multiple-modules error (probe=0), and the final status prints nothing (mutation gone). If probe=1, the guard did NOT fire — stop and investigate before proceeding; do not continue on an unproven safety claim.
134
135 ---
136
137 ### Task 2: Daemon collapse
138
139 `cmd`, `shellint`, `upgrade`, `quic_server` become children of the `server` module; rename it `daemon`. `pty` stays tabled (the `ptyclient` fixture consumes it). The root re-exports `quic_server` and `upgrade` for `src/cli/main.zig`.
140
141 **Files:**
142 - Modify: `src/server/server.zig`, `src/server/server_test_quic.zig`, `server_test_agent.zig`, `server_test_harness.zig`, `server_test_session.zig`, `server_test_upgrade.zig`, `server_test_await.zig`, `src/cli/main.zig`, `build.zig`
143 - Also re-grep: `grep -rn '@import("server")\|@import("cmd")\|@import("shellint")\|@import("upgrade")\|@import("quic_server")' src test`
144
145 **Interfaces:**
146 - Produces: table row `daemon` rooted at `src/server/server.zig`; re-exports on the root:
147
148 ```zig
149 // Re-exported for the daemon's own main (src/cli/main.zig) — the only
150 // consumer outside this folder; nobody else may know these exist.
151 pub const quic_server = @import("quic_server.zig");
152 pub const upgrade = @import("upgrade.zig");
153 ```
154
155 - [ ] **Step 2.1: Rewrite src/server/ name-imports to relative**
156
157 Sites as of writing: server.zig:16 (`cmd`→`cmd.zig`, keep the local alias `cmdmod`), server.zig:17 (`shellint`), server.zig:23 (`quic_server`), server.zig:25 (`upgrade`); server_test_quic.zig:5, server_test_agent.zig:4, server_test_harness.zig:6, server_test_session.zig:5 (all `quic_server`); server_test_upgrade.zig:3 (`upgrade`); server_test_await.zig:3 (`shellint`). Same transformation as Task 1: `@import("X")` → `@import("X.zig")`.
158
159 - [ ] **Step 2.2: Add the two re-exports to server.zig** (code block above, near the existing top-of-file imports) **and chain the children's tests** into server.zig's closing `test {}` block (~line 3600):
160
161 ```zig
162 _ = @import("cmd.zig");
163 _ = @import("shellint.zig");
164 _ = @import("upgrade.zig");
165 _ = @import("quic_server.zig");
166 ```
167
168 - [ ] **Step 2.3: Point main.zig at the re-exports**
169
170 `src/cli/main.zig`: line ~12 `const quic_server = @import("quic_server");` → `const quic_server = @import("daemon").quic_server;`; line ~18 `const upgrade = @import("upgrade");` → `const upgrade = @import("daemon").upgrade;`; and its `@import("server")` → `@import("daemon")`.
171
172 - [ ] **Step 2.4: Edit the module table**
173
174 1. Delete rows `cmd` (~193), `shellint` (~211), `upgrade` (~204), `quic_server` (~165); remove the four from `test_order`.
175 2. Rename row `server` → `daemon`; its children's deps fold in (cmd: `engine`+`protocol` — already present; shellint: `xdg` — present; upgrade: `protocol` — present; quic_server: `quic` — present). Drop `cmd`, `shellint`, `upgrade`, `quic_server` from its imports:
176
177 ```zig
178 .{ .name = "daemon", .path = "src/server/server.zig", .layer = 2, .link_libc = true, .imports = &.{ "engine", "pty", "protocol", "delta", "sockpath", "quic", "xdg", "proxy" }, .test_imports = &.{ "replica", "testtmp" }, .quic_tests = true },
179 ```
180
181 3. `daemon_main` row (~279): imports drop `quic_server`, `upgrade`; `"server"` → `"daemon"`.
182 4. `grep -n '"server"\|idxOf("server")\|idxOf("quic_server")\|idxOf("upgrade")\|idxOf("cmd")\|idxOf("shellint")' build.zig` — fix every handle. Note build.zig:594's test-sibling group table names `src/server/server.zig` by path — unchanged.
183
184 - [ ] **Step 2.5: Gate and commit**
185
186 ```bash
187 make check; echo rc=$?
188 git add -A && git commit -m "refactor: the daemon owns its command surface, shell integration, upgrade and QUIC arm as child files"
189 ```
190
191 ---
192
193 ### Task 3: Client collapse
194
195 `client_core`, `hosts`, `handoff`, `layout`, `keymap`, `askpass` become children of `client`; the root re-exports all six (every one has an outside consumer). The wasm canary and `wasm_core.zig` switch to relative imports; build.zig's wasm wiring drops the two dead twins.
196
197 **Files:**
198 - Modify: `src/client/client.zig`, `src/client/wasm_core.zig`, `src/client/client_core_wasm_check.zig`, `src/tui/wallview.zig`, `wall_picker.zig`, `wall_layout.zig`, `wall_host.zig`, `wall_pump.zig`, `interact.zig`, `wall_test_layout.zig`, `wall_test_harness.zig`, `wall_test_host.zig`, `wall_test_pump.zig`, `wall_test_picker.zig`, `src/cli/mux.zig`, `src/cli/main.zig`, `src/cli/mux_main.zig`, `src/cli/webhub_main.zig`, `build.zig`
199
200 **Interfaces:**
201 - Produces on `src/client/client.zig`:
202
203 ```zig
204 // The client link's public seams: the wall, the hub and the mains reach
205 // these as client.X — the table stays one row, the files stay children.
206 pub const hosts = @import("hosts.zig");
207 pub const handoff = @import("handoff.zig");
208 pub const layout = @import("layout.zig");
209 pub const keymap = @import("keymap.zig");
210 pub const askpass = @import("askpass.zig");
211 pub const core = @import("client_core.zig");
212 ```
213
214 (`core`, not `client_core` — `client.client_core` stutters; every outside consumer spells `@import("client").core`.)
215
216 - [ ] **Step 3.1: Inside src/client/: name → relative**
217
218 client.zig:20 `keymap`, :24 `handoff`, :25 `askpass`, :26 `hosts` → `.zig` forms. wasm_core.zig:23 `keymap`, :25 `client_core` → `.zig` forms. client_core_wasm_check.zig:1 `client_core` → `client_core.zig`. Re-grep first:
219
220 ```bash
221 grep -rn '@import("client_core")\|@import("hosts")\|@import("handoff")\|@import("layout")\|@import("keymap")\|@import("askpass")' src
222 ```
223
224 - [ ] **Step 3.2: Outside consumers → re-exports**
225
226 For every hit OUTSIDE `src/client/` from the same grep, rewrite (adding `const client = @import("client");` to the file's import block if absent — wall files: it's the module the wall already imports):
227 - `@import("hosts")` → `@import("client").hosts`
228 - `@import("handoff")` → `@import("client").handoff`
229 - `@import("layout")` → `@import("client").layout`
230 - `@import("askpass")` → `@import("client").askpass`
231 - `@import("keymap")` → `@import("client").keymap` (interact.zig:35 imports `.detach_key` — becomes `@import("client").keymap.detach_key`)
232 - `@import("client_core")` → `@import("client").core`
233
234 Sites as of writing: wallview.zig:23,24,25,36; wall_picker.zig:10,11,13; wall_layout.zig:7,9; wall_host.zig:9,10; wall_pump.zig:9; wall_test_layout.zig:4,5; wall_test_harness.zig:7; wall_test_host.zig:5; wall_test_pump.zig:7; wall_test_picker.zig:6; interact.zig:27,31,35; mux.zig:17; main.zig:16; mux_main.zig:22,25; webhub_main.zig:16.
235
236 - [ ] **Step 3.3: client.zig root additions**
237
238 Add the re-export block (Interfaces above) and chain child tests into client.zig's `test {}` block (grep `test {` in client.zig; create the block at file end if none):
239
240 ```zig
241 _ = @import("client_core.zig");
242 _ = @import("hosts.zig");
243 _ = @import("handoff.zig");
244 _ = @import("layout.zig");
245 _ = @import("keymap.zig");
246 _ = @import("askpass.zig");
247 ```
248
249 - [ ] **Step 3.4: Edit the module table**
250
251 1. Delete rows `client_core` (~161), `hosts` (~199), `handoff` (~180), `askpass` (~185), `layout` (~224), `keymap` (~131); remove all six from `test_order`.
252 2. `client` row (~272): drop `keymap`, `handoff`, `hosts`, `askpass` from imports (now children); keep `protocol`, `replica`, `quic`, `xdg`, `sockpath`; keep `test_imports = &.{"testtmp"}` (hosts/handoff/askpass tests used testtmp — it must stay reachable in the test twin).
253 3. Rows that imported the deleted names: `wall` (drop `hosts` `handoff` `client_core` `keymap` `layout` `askpass` — it has `client`), `daemon_main` (drop `handoff`), `client_main` (drop `handoff` `hosts`), `hub_main` (drop `hosts`), `mux` (drop `askpass`), `agent_main`/`interact` — interact row is gone; agent_main never had them. Re-grep the table: `grep -n '"hosts"\|"handoff"\|"layout"\|"keymap"\|"client_core"\|"askpass"' build.zig`.
254 4. wasm wiring (~line 1001–1030): the table-driven twin loop now only covers `protocol` `engine` `replica` (still tabled with `.wasm`). Hand edits: delete `const client_core_wasm_mod = ...` handle; `wasm_core_mod` keeps `engine`/`protocol`/`replica` addImports, DROP the `client_core` and `keymap` addImports (now relative children of wasm_core's own module). The canary: `client_core_wasm_check_mod.addImport("client_core", ...)` → replace with `client_core_wasm_check_mod.addImport("protocol", wasm_mods[comptime idxOf("protocol")].?);` (client_core.zig, now its relative child, imports `protocol` by name).
255
256 - [ ] **Step 3.5: Gate and commit**
257
258 ```bash
259 make check; echo rc=$?
260 git add -A && git commit -m "refactor: the client link owns its core, hosts file, handoff, layout, keymap and askpass as child files behind pub seams"
261 ```
262
263 ---
264
265 ### Task 4: Executable collapse
266
267 The four mains and `webhub` become children of the `mux` dispatcher module. `agent` (muxa) stays its own module — `cliflags` therefore stays a tabled leaf.
268
269 **Files:**
270 - Modify: `src/cli/mux.zig`, `src/cli/webhub_main.zig`, `build.zig`
271 - Rename module `agent_main` → `agent` (grep `@import("agent_main")` — only mux.zig:14).
272
273 **Interfaces:**
274 - Consumes: `@import("client").askpass` (already rewritten in Task 3).
275 - Produces: one row `mux` whose children are the mains; `agent` row unchanged but renamed.
276
277 - [ ] **Step 4.1: mux.zig dispatch → relative children**
278
279 mux.zig:13–16: `@import("daemon_main")` → `@import("main.zig")`, `@import("agent_main")` → `@import("agent")` (module — stays a name import), `@import("hub_main")` → `@import("webhub_main.zig")`, `@import("client_main")` → `@import("mux_main.zig")`.
280
281 - [ ] **Step 4.2: webhub becomes a child too**
282
283 webhub_main.zig:15: `@import("webhub")` → `@import("../client/webhub.zig")`. (Legal: relative imports may traverse up; webhub.zig joins the mux module. Its own `@import("protocol")`/`@import("client")` resolve against the mux row's imports.)
284
285 - [ ] **Step 4.3: Edit the module table**
286
287 1. Delete rows `daemon_main` (~279), `client_main` (~307), `hub_main` (~301), `webhub` (~284); remove from `test_order`; rename `agent_main` → `agent` (row ~260).
288 2. New `mux` row = union of the deleted rows' imports minus dead names:
289
290 ```zig
291 .{ .name = "mux", .path = "src/cli/mux.zig", .layer = 6, .link_libc = true, .imports = &.{ "daemon", "client", "wall", "agent", "protocol", "proxy", "quic", "xdg", "spawn", "sockpath", "cliflags" }, .test_imports = &.{"testtmp"}, .quic_tests = true },
292 ```
293
294 3. The anonymous imports (~line 1053): `hub_main_mod.addAnonymousImport("index.html", ...)` etc. — the handle `hub_main_mod` is gone; attach all three to `mux_mod` instead (webhub_main.zig is now a mux child; `@embedFile` resolves against its module). The comment about sequencing the wasm build before the hub's stays true — keep it.
295 4. Fix named handles: `grep -n 'idxOf("daemon_main")\|idxOf("client_main")\|idxOf("hub_main")\|idxOf("agent_main")\|idxOf("webhub")\|hub_main_mod\|daemon_main\|client_main' build.zig`. The `mux` exe wiring (~line 914–960) already builds from `idxOf("mux")` — unchanged.
296
297 - [ ] **Step 4.4: Chain the mains' tests from mux.zig**
298
299 Grep each of main.zig / mux_main.zig / webhub_main.zig / webhub.zig for `test `. Add to (or create at the end of) mux.zig:
300
301 ```zig
302 test {
303 _ = @import("main.zig");
304 _ = @import("mux_main.zig");
305 _ = @import("webhub_main.zig");
306 _ = @import("../client/webhub.zig");
307 }
308 ```
309
310 - [ ] **Step 4.5: Gate and commit**
311
312 ```bash
313 make check; echo rc=$?
314 git add -A && git commit -m "refactor: the one binary's mains and the hub page are children of the dispatcher"
315 ```
316
317 ---
318
319 ### Task 5: Term collapse
320
321 The big sed: `protocol`, `engine`, `delta`, `replica` fold under a new root `src/engine/term.zig`. Every consumer spells `@import("term").X`; the four files import each other relatively. ghostty-vt moves to the term module (native and wasm).
322
323 **Files:**
324 - Create: `src/engine/term.zig`
325 - Modify: every file the greps below hit (~25 files), `build.zig`, `docscheck.budget`
326
327 **Interfaces:**
328 - Produces `src/engine/term.zig`:
329
330 ```zig
331 //! The terminal component: the wire contract, the authoritative engine,
332 //! the delta minting that feeds replicas, and the one replay core. One
333 //! table row — the four files are one owner, and only this root is a seam.
334 pub const protocol = @import("protocol.zig");
335 pub const engine = @import("engine.zig");
336 pub const delta = @import("delta.zig");
337 pub const replica = @import("replica.zig");
338
339 test {
340 _ = protocol;
341 _ = engine;
342 _ = delta;
343 _ = replica;
344 }
345 ```
346
347 - [ ] **Step 5.1: Inside src/engine/: name → relative**
348
349 delta.zig imports `engine`+`protocol`; replica.zig imports `engine`+`protocol`; check engine.zig and protocol.zig too:
350
351 ```bash
352 grep -rn '@import("engine")\|@import("protocol")\|@import("delta")\|@import("replica")' src/engine/
353 ```
354 Rewrite each to the `.zig` relative form.
355
356 - [ ] **Step 5.2: Everywhere else: name → term.X**
357
358 ```bash
359 grep -rln '@import("protocol")\|@import("engine")\|@import("delta")\|@import("replica")' src test
360 ```
361 In every hit (~25 files, none under src/engine/ after 5.1), apply:
362 - `@import("protocol")` → `@import("term").protocol`
363 - `@import("engine")` → `@import("term").engine`
364 - `@import("delta")` → `@import("term").delta`
365 - `@import("replica")` → `@import("term").replica`
366
367 This is sed-able per file; verify with a final grep that zero name-form sites remain outside src/engine/. Keep local alias names as they are (`const protocol = @import("term").protocol;` — call sites don't change).
368
369 - [ ] **Step 5.3: Edit the module table**
370
371 1. Add the row (and to `test_order`):
372
373 ```zig
374 .{ .name = "term", .path = "src/engine/term.zig", .layer = 0, .wasm = true },
375 ```
376
377 2. Delete rows `protocol`, `engine`, `delta`, `replica`; remove from `test_order`.
378 3. Every row importing any of the four: replace with a single `"term"` (dedup!). As of Task 4's end that is: `daemon` (had engine, protocol, delta), `client` (protocol, replica), `wall` (protocol, engine, replica), `agent` (protocol), `mux` (protocol), `render` (engine), `wsclient` (engine, replica, protocol), `paint`/`cmd`/etc. are already children. `daemon`'s `test_imports` drops `replica` (term is a production import now). Re-grep: `grep -n '"protocol"\|"engine"\|"delta"\|"replica"' build.zig` until only comments remain.
379 4. ghostty native (~line 930): `engine_mod.addImport("ghostty-vt", ...)` — the handle comes from `idxOf("engine")`; change to `idxOf("term")` / `term_mod`. The import name `"ghostty-vt"` is consumed by engine.zig, now a term child — attaching it to the term module is exactly right.
380 5. wasm (~line 1001+): the `.wasm` twin loop now instantiates only `term`. Replace the explicit engine/protocol/replica handles: `term_wasm_mod = wasm_mods[comptime idxOf("term")].?`; ghostty wasm import attaches to `term_wasm_mod`; `wasm_core_mod` drops addImports `engine`/`protocol`/`replica`/`client_core`/`keymap` (the latter two died in Task 3) and gains `wasm_core_mod.addImport("term", term_wasm_mod);`. The canary swaps its `protocol` addImport for `term`: `client_core_wasm_check_mod.addImport("term", term_wasm_mod);` (client_core.zig now spells `@import("term").protocol`).
381 6. src/client/wasm_core.zig source: its `@import("engine")` etc. were already rewritten to `term.X` by Step 5.2 — confirm.
382 7. The wasm-closure comptime check (~line 346) still holds (term has no imports); leave it.
383
384 - [ ] **Step 5.4: Budget line for the new file**
385
386 `zig build check` will demand a `term.zig` figure. Run `deps/zig/zig build doc-report 2>&1 | grep term` and add the exact reported line to `docscheck.budget` (alphabetical position matching the file's ordering convention — read the neighboring lines).
387
388 - [ ] **Step 5.5: Gate and commit**
389
390 ```bash
391 make check; echo rc=$?
392 git add -A && git commit -m "refactor: one term component — wire contract, engine, delta and replay core behind a single seam"
393 ```
394
395 ---
396
397 ### Task 6: Layers, folder rules, docs
398
399 Renumber layers to the real ranks, audit the folder rules against the shrunken table, update CLAUDE.md and decisions.md.
400
401 **Files:**
402 - Modify: `build.zig`, `CLAUDE.md`, `docs/decisions.md`
403
404 - [ ] **Step 6.1: Renumber `.layer`**
405
406 Final ranks — leaves 0, domains by depth:
407 - 0: `term` `quic` `proxy` `xdg` `sockpath` `pty` `cliflags` `testtmp` `spawn` `script` `rawmode` `delaypipe`
408 - 1: `daemon` `client` `agent` `render` `ptyclient` `wsclient`
409 - 2: `wall` `webhub` (webhub survived Task 4 as a row: Zig forbids relative imports outside the module root's dirname, so src/client/webhub.zig cannot be a child of the src/cli/ dispatcher without a file move; ruled kept-as-row)
410 - 3: `mux`
411
412 (`spawn` at 0 assumes its row still lists no imports — verify before renumbering.)
413
414 Adjust every row; the comptime check (imports strictly downward) is the verifier — `make check` fails loudly on any wrong rank. Also update the comment at the comptime block (~line 329) that says "32-row, 76-edge table" to the new true counts (count them: `grep -c '.name =' build.zig` minus non-table hits).
415
416 - [ ] **Step 6.2: Folder-rules audit**
417
418 `checkFolderRules` (build.zig:402) iterates table rows — it still compiles unchanged, but verify the three domain rules still have teeth by reading what edges remain: rule 3 (client imports no tui) now guards the `client` row; rule 2 (server imports no client/tui) guards `daemon`; rule 1 guards `term`+`client`. The rules 4–6 source bans are path-keyed and untouched — but rule 4's file set: `predict.zig` moved OUT of src/engine/ into src/tui/, where the VT-byte ban does not apply. Check `folder_exemptions` (should stay empty) and the `source_bans` file lists for any literal `predict` path; fix if named. Run the mutation probe from Task 1 Step 1.6 once more if any rule text was edited.
419
420 - [ ] **Step 6.3: CLAUDE.md**
421
422 Rewrite the Layout section: the folder table (module column now names roots + child files), the layer table (new 0–3 ranks above), and the sentence "Layers are enforced in the same module table". Update the module names in prose (`server`→`daemon` module, `wallview`→`wall`; file paths unchanged — say so). The reading-guide file sizes are unchanged (no content moved except predict) — leave them. Update the line "grep `.layer =` for the graph" only if still true (it is).
423
424 - [ ] **Step 6.4: decisions.md entry**
425
426 Append (matching the file's dated-entry format — read the last entry for shape) a decision: module = owned component, not compilable file; table 41→~15; children are relative imports; cross-domain grabs now die on Zig's one-file-one-module error where they used to die on a missing table edge; predict lives with the wall so term structurally cannot see it.
427
428 - [ ] **Step 6.5: Gate and commit**
429
430 ```bash
431 make check; echo rc=$?
432 git add -A && git commit -m "refactor: layers renumbered to the six-component graph; docs follow"
433 ```
434
435 ---
436
437 ### Task 7: Delivery gates
438
439 - [ ] **Step 7.1:** `make ci; echo rc=$?` — expected rc=0 (check + e2e + agent + throughput). E2e runs real binaries; nothing in this refactor touches the wire or behavior, so any red here is a genuine wiring mistake — debug, don't waive.
440 - [ ] **Step 7.2:** `XVER_OLD_WORKTREE=../mux-xver-old make xversion-build xversion; echo rc=$?` — expected rc=0 (12/12). The old worktree must exist at `../mux-xver-old`; a stale prefix reads as a failed gate — rebuild first, per the target's own recipe.
441 - [ ] **Step 7.3:** Review the branch story: `git log --oneline main..HEAD` — 7-ish commits, each a component. Squash fixups if any accumulated (`git rebase -i` is unavailable; use `git commit --fixup` + `GIT_SEQUENCE_EDITOR=true git rebase --autosquash main` only if needed).
442 - [ ] **Step 7.4:** STOP. Do not merge to main, push, or install — report completion with the gate outputs and wait for the user.
try.sh
Old New
@@ -1,25 +0,0 @@
1 #!/bin/sh
2 # Feel the shape-first restore in your own terminal, fully isolated from
3 # your live daemon and wall. Run it twice: the second run IS the demo —
4 # the wall opens already wearing the beside cut you left, no stripes, no
5 # 2 s rearrange, even though two dead ssh hosts are on the wall.
6 set -eu
7 cd "$(dirname "$0")"
8 deps/zig/zig build
9 S="${TMPDIR:-/tmp}/mux-try-shape"
10 mkdir -p "$S/state/mux" "$S/rt"
11 chmod 700 "$S/rt"
12 export XDG_STATE_HOME="$S/state" XDG_RUNTIME_DIR="$S/rt"
13 export XDG_CONFIG_HOME="$S/cfg" XDG_CACHE_HOME="$S/cache"
14 SOCK="$S/d.sock"
15 zig-out/bin/mux d start --sock "$SOCK" >/dev/null 2>&1 || true
16 {
17 echo "--sock $SOCK"
18 echo "alex@203.0.113.7" # TEST-NET: never answers, like a box that is off
19 echo "ubuntu@203.0.113.8"
20 } > "$S/state/mux/hosts"
21 echo "try: one local daemon + two dead ssh hosts on the wall."
22 echo "try: split with Ctrl-\\ | , resize with Ctrl-\\ r, leave with Ctrl-\\ d,"
23 echo "try: then run me again — the wall must open ALREADY in your layout."
24 echo
25 exec zig-out/bin/mux