a73x

docs/superpowers/plans/2026-08-30-module-collapse.md

Ref:   Size: 27.7 KiB   History

# Module-Table Collapse Implementation Plan

> **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.

**Goal:** Collapse mux's 41-entry build module table to ~15: six domain modules whose child files are relative imports, shared leaves stay tabled.

**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`.

**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.

**Spec:** This plan is its own spec. The agreed end state:

| Module | Root | Children (leave the table, become relative imports) |
|---|---|---|
| `term` | `src/engine/term.zig` (new) | `protocol` `engine` `delta` `replica` |
| `daemon` | `src/server/server.zig` | `quic_server` `cmd` `shellint` `upgrade` (+ `server_agent` `server_sessions`, already children) |
| `client` | `src/client/client.zig` | `client_core` `hosts` `handoff` `layout` `keymap` `askpass` |
| `wall` | `src/tui/wallview.zig` | `interact` `paint` `select` `predict` (+ `wall_*`, already children) |
| `agent` | `src/cli/muxa.zig` | — |
| `mux` | `src/cli/mux.zig` | `main.zig` `mux_main.zig` `webhub_main.zig` `webhub.zig` |

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`.

Final production import lists (this IS the layer graph):
- `term` → nothing (plus the `ghostty-vt` dep edge, wired outside the table)
- `daemon` → `term` `pty` `quic` `proxy` `xdg` `sockpath`
- `client` → `term` `quic` `xdg` `sockpath`
- `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)
- `agent` → `term` `quic` `xdg` `sockpath` `cliflags`
- `mux` → `term` `daemon` `client` `wall` `agent` `quic` `proxy` `xdg` `sockpath` `spawn` `cliflags`

## Global Constraints

- Build ONLY with `deps/zig/zig` (the Makefile already points at it). Never system zig.
- `make check` green before every commit; capture `$?` before piping (`make check; echo rc=$?`).
- NO directory reorganization. One `git mv`: `src/engine/predict.zig` → `src/tui/predict.zig`. Every other file stays put.
- `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.
- Comments say *why*, not *how*. When editing import blocks, keep existing comments attached to their imports. `zig build check` verifies comment symbol refs resolve.
- `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`.
- 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.
- Commit per task, message style matches the repo (`refactor: <narrative subject>`).
- A unit test that writes to stdout wedges `zig build test` silently — if `make check` hangs with no output for minutes, suspect that first.

---

### Task 0: Branch

- [ ] **Step 0.1:** `git -C /home/xanderle/code/rad/mux checkout -b modules` (from main, which must be clean apart from RETRO.md).

---

### Task 1: Wall collapse

`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`.

**Files:**
- Move: `src/engine/predict.zig` → `src/tui/predict.zig` (git mv)
- 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`
- Also check: `grep -rn '@import("wallview")' src test` and rewrite every hit to `@import("wall")`.

**Interfaces:**
- Consumes: current table rows `interact`, `paint`, `select`, `predict`, `wallview` (build.zig:220–294 region).
- Produces: table row `.{ .name = "wall", .path = "src/tui/wallview.zig", ... }`; no re-exports needed (nothing outside tui consumes interact/paint/select/predict).

- [ ] **Step 1.1: Move predict**

```bash
git mv src/engine/predict.zig src/tui/predict.zig
```

- [ ] **Step 1.2: Rewrite name-imports to relative imports inside src/tui/**

Every tui file importing a now-child sibling switches to the `.zig` relative form. Exact current sites (re-grep before editing; line numbers drift):

```bash
grep -rn '@import("interact")\|@import("paint")\|@import("select")\|@import("predict")' src/tui/
```

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.

- [ ] **Step 1.3: Chain the new children's tests from the root**

`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:

```zig
test {
    _ = @import("interact.zig");
    _ = @import("paint.zig");
    _ = @import("select.zig");
    _ = @import("predict.zig");
    _ = @import("wall_test_harness.zig");
    // ... existing lines stay ...
}
```

- [ ] **Step 1.4: Edit the module table**

In `build.zig`:
1. Delete rows `interact` (line ~248), `paint` (~220), `select` (~231), `predict` (~170).
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:

```zig
    .{ .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 },
```

3. Remove `interact`, `paint`, `select`, `predict` from `test_order` (~line 816); rename `wallview` → `wall` there.
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.
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`.

- [ ] **Step 1.5: Gate**

```bash
make check; echo rc=$?
```
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.

- [ ] **Step 1.6: Commit** (BEFORE the mutation probe — a probe against an uncommitted tree cannot be reverted safely; the baseline must be committed first)

```bash
git add -A && git commit -m "refactor: the wall owns its interaction loop, painter, selector and overlay as child files"
```

- [ ] **Step 1.7: Watch the new guard fail once (mutation check, never committed)**

The collapse's safety claim: cross-domain file grabs are compiler-blocked. Prove it fires:

```bash
echo 'const sneak = @import("../client/hosts.zig");' >> src/tui/interact.zig
make check 2>&1 | grep -i 'file exists in modules' ; echo probe=$?
git checkout -- src/tui/interact.zig
git status --porcelain src/tui/interact.zig
```
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.

---

### Task 2: Daemon collapse

`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`.

**Files:**
- 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`
- Also re-grep: `grep -rn '@import("server")\|@import("cmd")\|@import("shellint")\|@import("upgrade")\|@import("quic_server")' src test`

**Interfaces:**
- Produces: table row `daemon` rooted at `src/server/server.zig`; re-exports on the root:

```zig
// Re-exported for the daemon's own main (src/cli/main.zig) — the only
// consumer outside this folder; nobody else may know these exist.
pub const quic_server = @import("quic_server.zig");
pub const upgrade = @import("upgrade.zig");
```

- [ ] **Step 2.1: Rewrite src/server/ name-imports to relative**

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")`.

- [ ] **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):

```zig
    _ = @import("cmd.zig");
    _ = @import("shellint.zig");
    _ = @import("upgrade.zig");
    _ = @import("quic_server.zig");
```

- [ ] **Step 2.3: Point main.zig at the re-exports**

`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")`.

- [ ] **Step 2.4: Edit the module table**

1. Delete rows `cmd` (~193), `shellint` (~211), `upgrade` (~204), `quic_server` (~165); remove the four from `test_order`.
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:

```zig
    .{ .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 },
```

3. `daemon_main` row (~279): imports drop `quic_server`, `upgrade`; `"server"` → `"daemon"`.
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.

- [ ] **Step 2.5: Gate and commit**

```bash
make check; echo rc=$?
git add -A && git commit -m "refactor: the daemon owns its command surface, shell integration, upgrade and QUIC arm as child files"
```

---

### Task 3: Client collapse

`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.

**Files:**
- 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`

**Interfaces:**
- Produces on `src/client/client.zig`:

```zig
// The client link's public seams: the wall, the hub and the mains reach
// these as client.X — the table stays one row, the files stay children.
pub const hosts = @import("hosts.zig");
pub const handoff = @import("handoff.zig");
pub const layout = @import("layout.zig");
pub const keymap = @import("keymap.zig");
pub const askpass = @import("askpass.zig");
pub const core = @import("client_core.zig");
```

(`core`, not `client_core` — `client.client_core` stutters; every outside consumer spells `@import("client").core`.)

- [ ] **Step 3.1: Inside src/client/: name → relative**

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:

```bash
grep -rn '@import("client_core")\|@import("hosts")\|@import("handoff")\|@import("layout")\|@import("keymap")\|@import("askpass")' src
```

- [ ] **Step 3.2: Outside consumers → re-exports**

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):
- `@import("hosts")` → `@import("client").hosts`
- `@import("handoff")` → `@import("client").handoff`
- `@import("layout")` → `@import("client").layout`
- `@import("askpass")` → `@import("client").askpass`
- `@import("keymap")` → `@import("client").keymap` (interact.zig:35 imports `.detach_key` — becomes `@import("client").keymap.detach_key`)
- `@import("client_core")` → `@import("client").core`

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.

- [ ] **Step 3.3: client.zig root additions**

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):

```zig
    _ = @import("client_core.zig");
    _ = @import("hosts.zig");
    _ = @import("handoff.zig");
    _ = @import("layout.zig");
    _ = @import("keymap.zig");
    _ = @import("askpass.zig");
```

- [ ] **Step 3.4: Edit the module table**

1. Delete rows `client_core` (~161), `hosts` (~199), `handoff` (~180), `askpass` (~185), `layout` (~224), `keymap` (~131); remove all six from `test_order`.
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).
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`.
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).

- [ ] **Step 3.5: Gate and commit**

```bash
make check; echo rc=$?
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"
```

---

### Task 4: Executable collapse

The four mains and `webhub` become children of the `mux` dispatcher module. `agent` (muxa) stays its own module — `cliflags` therefore stays a tabled leaf.

**Files:**
- Modify: `src/cli/mux.zig`, `src/cli/webhub_main.zig`, `build.zig`
- Rename module `agent_main` → `agent` (grep `@import("agent_main")` — only mux.zig:14).

**Interfaces:**
- Consumes: `@import("client").askpass` (already rewritten in Task 3).
- Produces: one row `mux` whose children are the mains; `agent` row unchanged but renamed.

- [ ] **Step 4.1: mux.zig dispatch → relative children**

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")`.

- [ ] **Step 4.2: webhub becomes a child too**

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.)

- [ ] **Step 4.3: Edit the module table**

1. Delete rows `daemon_main` (~279), `client_main` (~307), `hub_main` (~301), `webhub` (~284); remove from `test_order`; rename `agent_main` → `agent` (row ~260).
2. New `mux` row = union of the deleted rows' imports minus dead names:

```zig
    .{ .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 },
```

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.
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.

- [ ] **Step 4.4: Chain the mains' tests from mux.zig**

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:

```zig
test {
    _ = @import("main.zig");
    _ = @import("mux_main.zig");
    _ = @import("webhub_main.zig");
    _ = @import("../client/webhub.zig");
}
```

- [ ] **Step 4.5: Gate and commit**

```bash
make check; echo rc=$?
git add -A && git commit -m "refactor: the one binary's mains and the hub page are children of the dispatcher"
```

---

### Task 5: Term collapse

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).

**Files:**
- Create: `src/engine/term.zig`
- Modify: every file the greps below hit (~25 files), `build.zig`, `docscheck.budget`

**Interfaces:**
- Produces `src/engine/term.zig`:

```zig
//! The terminal component: the wire contract, the authoritative engine,
//! the delta minting that feeds replicas, and the one replay core. One
//! table row — the four files are one owner, and only this root is a seam.
pub const protocol = @import("protocol.zig");
pub const engine = @import("engine.zig");
pub const delta = @import("delta.zig");
pub const replica = @import("replica.zig");

test {
    _ = protocol;
    _ = engine;
    _ = delta;
    _ = replica;
}
```

- [ ] **Step 5.1: Inside src/engine/: name → relative**

delta.zig imports `engine`+`protocol`; replica.zig imports `engine`+`protocol`; check engine.zig and protocol.zig too:

```bash
grep -rn '@import("engine")\|@import("protocol")\|@import("delta")\|@import("replica")' src/engine/
```
Rewrite each to the `.zig` relative form.

- [ ] **Step 5.2: Everywhere else: name → term.X**

```bash
grep -rln '@import("protocol")\|@import("engine")\|@import("delta")\|@import("replica")' src test
```
In every hit (~25 files, none under src/engine/ after 5.1), apply:
- `@import("protocol")` → `@import("term").protocol`
- `@import("engine")` → `@import("term").engine`
- `@import("delta")` → `@import("term").delta`
- `@import("replica")` → `@import("term").replica`

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).

- [ ] **Step 5.3: Edit the module table**

1. Add the row (and to `test_order`):

```zig
    .{ .name = "term", .path = "src/engine/term.zig", .layer = 0, .wasm = true },
```

2. Delete rows `protocol`, `engine`, `delta`, `replica`; remove from `test_order`.
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.
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.
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`).
6. src/client/wasm_core.zig source: its `@import("engine")` etc. were already rewritten to `term.X` by Step 5.2 — confirm.
7. The wasm-closure comptime check (~line 346) still holds (term has no imports); leave it.

- [ ] **Step 5.4: Budget line for the new file**

`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).

- [ ] **Step 5.5: Gate and commit**

```bash
make check; echo rc=$?
git add -A && git commit -m "refactor: one term component — wire contract, engine, delta and replay core behind a single seam"
```

---

### Task 6: Layers, folder rules, docs

Renumber layers to the real ranks, audit the folder rules against the shrunken table, update CLAUDE.md and decisions.md.

**Files:**
- Modify: `build.zig`, `CLAUDE.md`, `docs/decisions.md`

- [ ] **Step 6.1: Renumber `.layer`**

Final ranks — leaves 0, domains by depth:
- 0: `term` `quic` `proxy` `xdg` `sockpath` `pty` `cliflags` `testtmp` `spawn` `script` `rawmode` `delaypipe`
- 1: `daemon` `client` `agent` `render` `ptyclient` `wsclient`
- 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)
- 3: `mux`

(`spawn` at 0 assumes its row still lists no imports — verify before renumbering.)

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).

- [ ] **Step 6.2: Folder-rules audit**

`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.

- [ ] **Step 6.3: CLAUDE.md**

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).

- [ ] **Step 6.4: decisions.md entry**

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.

- [ ] **Step 6.5: Gate and commit**

```bash
make check; echo rc=$?
git add -A && git commit -m "refactor: layers renumbered to the six-component graph; docs follow"
```

---

### Task 7: Delivery gates

- [ ] **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.
- [ ] **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.
- [ ] **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).
- [ ] **Step 7.4:** STOP. Do not merge to main, push, or install — report completion with the gate outputs and wait for the user.