a73x

794a3433

docs: the native client plan is rebased onto cells on the wire

a73x   2026-09-04 18:50

Commit message
docs: the native client plan is rebased onto cells on the wire

The prerequisite is met, so the plan states what actually landed rather
than what it expected. Three drifts were real. `Replica.apply` has three
outcomes, not two: besides the enum it fails two ways that are not the
same failure, and the pump's `catch .resync` would have painted from a
grid that a half-decoded snapshot had already blanked. `term` no longer
links ghostty-vt, so the `native` row's import of it pulls in no
emulator and nothing under `src/gui/` has a reason to reach for the
`engine` module. And two file:line citations became symbol names,
because a line number in a document nothing compiles is a fact that
rots between merges.

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

docs/superpowers/plans/2026-09-04-native-client.md
Old New
@@ -12,7 +12,9 @@
12 12
13 ## Global Constraints 13 ## Global Constraints
14 14
15 - **Prerequisite:** the cells-on-the-wire branch must be on main first. Every type below (`term.grid.Grid`, `Row`, `Cell`, `proto.CellStyle`, `proto.Wide`, `Replica.apply` returning `.painted | .resync`, `Replica.attachArgs`) comes from it. Do not start Task 4 or later on a tree without `src/engine/grid.zig`. 15 - **Prerequisite: MET.** Cells-on-the-wire landed on main 2026-09-04 (`f07a56c8` through `406628a2`) and this plan was rebased onto it. Every type it names is present as written: `term.grid.Grid` with `cursor`/`lines`/`row(y)`, `grid.Row` with `cells` and `text: ArrayListUnmanaged(u8)` read through `textOf`, `grid.Cell{style,wide,text_off,text_len}`, `proto.CellStyle`, `proto.Wide`, `Replica.apply` returning the enum `.painted | .resync`, `Replica.attachArgs`, `Replica.state_since_attach`.
16 - **`apply` has THREE outcomes, not two.** Besides the enum it can fail two ways, and they are not the same failure: `error.BadPayload` left the grid untouched (skip the frame), `error.SnapshotAborted` blanked the grid while `last_seq` claims to be current (end the session). Task 7's `onFrame` carries both arms. `interact.Core.frame` is the wall's copy of the same rule and is the reference if anything reads ambiguously.
17 - **`term` no longer links ghostty-vt.** The merge split the engine out: `term` is the wire, the grid and the replica, and the separate `engine` module is the emulator that imports it. So the `native` row importing `term` pulls in no emulator, and nothing under `src/gui/` should ever import `engine`.
16 - Toolchain is `deps/zig/zig` (0.15.2). System zig will not build this tree. Every command below spells `deps/zig/zig` or `make`. A fresh worktree has no `deps/`: link the main checkout's before anything else (`ln -s ../../../../deps/zig deps/zig; ln -s ../../../../deps/quic deps/quic` from a worktree under `.claude/worktrees/`), and never share `.zig-cache` between worktrees. 18 - Toolchain is `deps/zig/zig` (0.15.2). System zig will not build this tree. Every command below spells `deps/zig/zig` or `make`. A fresh worktree has no `deps/`: link the main checkout's before anything else (`ln -s ../../../../deps/zig deps/zig; ln -s ../../../../deps/quic deps/quic` from a worktree under `.claude/worktrees/`), and never share `.zig-cache` between worktrees.
17 - `make build`, `make check`, `make test` and `make ci` never build or link `src/gui/`. `zig build native`, `zig build native-test` and `zig build native-e2e` are the only steps that touch it; `make native` and `make native-e2e` are their only callers. 19 - `make build`, `make check`, `make test` and `make ci` never build or link `src/gui/`. `zig build native`, `zig build native-test` and `zig build native-e2e` are the only steps that touch it; `make native` and `make native-e2e` are their only callers.
18 - `src/gui/` imports `client` and `term` and nothing else of ours. `src/cli/muxg.zig` (the entry) may also import `cliflags` and `sockpath`. 20 - `src/gui/` imports `client` and `term` and nothing else of ours. `src/cli/muxg.zig` (the entry) may also import `cliflags` and `sockpath`.
@@ -1007,7 +1009,7 @@ pub fn cursorInstance(x: u16, y: u16, ctx: Ctx) Instance {
1007 - [ ] **Step 4: Run the tests** 1009 - [ ] **Step 4: Run the tests**
1008 1010
1009 Run: `deps/zig/zig build native-test 2>&1 | tail -8; echo rc=$?` 1011 Run: `deps/zig/zig build native-test 2>&1 | tail -8; echo rc=$?`
1010 Expected: rc 0. If `grid.Row.text` is not an `ArrayListUnmanaged(u8)` on the landed branch, adapt `rowOf` to the landed type — the production code reads it only through `row.textOf`. 1012 Expected: rc 0. `grid.Row.text` is an `ArrayListUnmanaged(u8)` on the landed tree and `rowOf` builds one directly, which is a test-only liberty: the production code reads a row's bytes only through `row.textOf`, so a later change to how a row stores its text cannot reach `quads.zig`.
1011 1013
1012 - [ ] **Step 5: Commit** 1014 - [ ] **Step 5: Commit**
1013 1015
@@ -1835,7 +1837,27 @@ pub const Pump = struct {
1835 var t = try std.time.Timer.start(); 1837 var t = try std.time.Timer.start();
1836 self.mu.lock(); 1838 self.mu.lock();
1837 defer self.mu.unlock(); 1839 defer self.mu.unlock();
1838 const applied = self.replica.apply(frame.type, frame.payload) catch .resync; 1840 // The two snapshot failures are DIFFERENT, and telling them
1841 // apart is the whole point (see `Replica.apply`'s doc comment
1842 // and `interact.Core.frame`, which is the wall's copy of this
1843 // rule). `error.BadPayload` left the grid untouched, so the
1844 // frame is skipped and we paint what we already held.
1845 // `error.SnapshotAborted` did not: the grid is blank while
1846 // `last_seq` claims to be current, so no later delta can fix
1847 // it and a resync's snapshot is what already failed. That one
1848 // ENDS the session rather than paint a replica holding
1849 // nothing.
1850 const applied = self.replica.apply(frame.type, frame.payload) catch |err| switch (err) {
1851 error.BadPayload => {
1852 self.last_apply_us = @intCast(@min(t.read() / std.time.ns_per_us, std.math.maxInt(u32)));
1853 return false;
1854 },
1855 error.SnapshotAborted => {
1856 self.setPhase(.exited, "the daemon's snapshot could not be decoded");
1857 return error.SessionOver;
1858 },
1859 else => .resync,
1860 };
1839 self.last_apply_us = @intCast(@min(t.read() / std.time.ns_per_us, std.math.maxInt(u32))); 1861 self.last_apply_us = @intCast(@min(t.read() / std.time.ns_per_us, std.math.maxInt(u32)));
1840 switch (applied) { 1862 switch (applied) {
1841 .painted => { 1863 .painted => {
@@ -1958,7 +1980,7 @@ pub const Pump = struct {
1958 }; 1980 };
1959 ``` 1981 ```
1960 1982
1961 If `client.openFailure` returns a struct whose text field is not named `message`, read `OpenFailure` at `src/client/client.zig` (around line 914) and use its field. If `Transport.close` followed by re-assignment is not how `wall_pump.redial` does it, mirror `redial` (around `src/tui/wall_pump.zig:340-370`). 1983 If `client.openFailure` returns a struct whose text field is not named `message`, read the `OpenFailure` declaration in `src/client/client.zig` and use its field. If `Transport.close` followed by re-assignment is not how the wall reconnects, mirror the `redial` function in `src/tui/wall_pump.zig`.
1962 1984
1963 - [ ] **Step 4: Wire the re-export and run the tests** 1985 - [ ] **Step 4: Wire the re-export and run the tests**
1964 1986
@@ -2719,7 +2741,7 @@ chmod +x test/native.sh
2719 deps/zig/zig build native -Doptimize=ReleaseSafe 2>&1 | tail -3 2741 deps/zig/zig build native -Doptimize=ReleaseSafe 2>&1 | tail -3
2720 MUXG_OPT=ReleaseSafe test/native.sh zig-out/bin/mux zig-out/bin/muxg; echo rc=$? 2742 MUXG_OPT=ReleaseSafe test/native.sh zig-out/bin/mux zig-out/bin/muxg; echo rc=$?
2721 ``` 2743 ```
2722 Expected: six `e2e OK:` lines, `native OK (6 checkpoints)`, rc 0. On the first run, expect to adjust: the `frames_now` grep pattern must match `bench.Ring.report`'s first line exactly (`=== muxg frame timing (N frames) ===`); the status JSON field order is `"cols":N,"rows":N` per `src/cli/muxa.zig:1095`. 2744 Expected: six `e2e OK:` lines, `native OK (6 checkpoints)`, rc 0. On the first run, expect to adjust: the `frames_now` grep pattern must match `bench.Ring.report`'s first line exactly (`=== muxg frame timing (N frames) ===`); the status JSON field order is `"cols":N,"rows":N`, which `printStatus` in `src/cli/muxa.zig` spells.
2723 2745
2724 If step 1's `wait_until` fails and `$GLOG` says `SDL_GL_CreateContext`, the offscreen driver has no GL here: run with `MUXG_VIDEODRIVER=x11` under `xvfb-run -a` and record that in the spec's Testing section. 2746 If step 1's `wait_until` fails and `$GLOG` says `SDL_GL_CreateContext`, the offscreen driver has no GL here: run with `MUXG_VIDEODRIVER=x11` under `xvfb-run -a` and record that in the spec's Testing section.
2725 2747