4f370210
chore: untrack the working files an add -A swept in
a73x 2026-08-30 20:19
Commit message
.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 | ||