16c40066
feat: systemd user units; docs: M2 decisions and usage
a73x 2026-08-08 14:08
Commit message
README.md
| Old | New | ||
|---|---|---|---|
| @@ -5,18 +5,17 @@ authoritatively in a daemon and replicated in the client — state sync | |||
| 5 | instead of escape-sequence replay. See `docs/handoff.md` for the design | 5 | instead of escape-sequence replay. See `docs/handoff.md` for the design |
| 6 | and `docs/decisions.md` for decisions made. | 6 | and `docs/decisions.md` for decisions made. |
| 7 | 7 | ||
| 8 | Status: **M1 — headless engine.** | 8 | Status: **M2 — the loop.** |
| 9 | 9 | ||
| 10 | Requires Zig 0.15.x (ghostty pin); the Makefile points at the pinned | 10 | Requires Zig 0.15.x (ghostty pin); the Makefile points at the pinned |
| 11 | toolchain, override with `make ZIG=...`. | 11 | toolchain, override with `make ZIG=...`. |
| 12 | 12 | ||
| 13 | make test && make e2e # verify | 13 | make test && make e2e # verify |
| 14 | make build | 14 | make build |
| 15 | ./zig-out/bin/muxd run # daemon, forwards stdin to the PTY | 15 | ./zig-out/bin/muxd run & # daemon |
| 16 | ./zig-out/bin/muxd dump [--vt] # print the authoritative grid | 16 | ./zig-out/bin/mux # attach a client (Ctrl-\ detaches) |
| 17 | ./zig-out/bin/muxd dump [--vt] # debug: print the authoritative grid | ||
| 17 | 18 | ||
| 18 | M1 demo: run `muxd run` in one terminal (keystrokes are forwarded blind — | 19 | Detaching (`Ctrl-\`) leaves the session running; re-running `mux` resumes |
| 19 | the grid lives only in the daemon), then `muxd dump` from another terminal | 20 | it from a state snapshot. systemd user units (socket activation) live in |
| 20 | while `nvim`, `top`, or `less` is running inside. The dump matches what a | 21 | `contrib/`. |
| 21 | real terminal would show, including CJK, ZWJ emoji, and SGR styling | ||
| 22 | (`--vt`). | ||
contrib/muxd.service
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,8 @@ | |||
| 1 | [Unit] | ||
| 2 | Description=mux daemon (prototype) | ||
| 3 | Requires=muxd.socket | ||
| 4 | |||
| 5 | [Service] | ||
| 6 | # Adjust the path to your checkout; the prototype is not installed system-wide. | ||
| 7 | ExecStart=%h/code/rad/mux/zig-out/bin/muxd run | ||
| 8 | Restart=no | ||
contrib/muxd.socket
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,8 @@ | |||
| 1 | [Unit] | ||
| 2 | Description=mux daemon socket | ||
| 3 | |||
| 4 | [Socket] | ||
| 5 | ListenStream=%t/muxd.sock | ||
| 6 | |||
| 7 | [Install] | ||
| 8 | WantedBy=sockets.target | ||
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -29,6 +29,43 @@ | |||
| 29 | POLLIN; the loop must attempt a read on any revents or it busy-loops at | 29 | POLLIN; the loop must attempt a read on any revents or it busy-loops at |
| 30 | 100% CPU. Found during the M1 demo, fixed and verified at 0% idle. | 30 | 100% CPU. Found during the M1 demo, fixed and verified at 0% idle. |
| 31 | 31 | ||
| 32 | ## 2026-08-07 (M2) | ||
| 33 | |||
| 34 | - **Snapshot = canonical VT state serialization.** `TerminalFormatter` with | ||
| 35 | `extra = .all` (palette, modes incl. active screen, cursor, styles), plus | ||
| 36 | a trailing CUP appended by `dumpState`: upstream emits DECSTBM (homes the | ||
| 37 | cursor) and tabstop HTS walks *after* the screen section's CUP, so the | ||
| 38 | dump's final cursor position is wrong without it. The client rebuilds its | ||
| 39 | replica with fullReset + feed. State sync via the engine's canonical | ||
| 40 | form — not a replay of session history. | ||
| 41 | - **Render dump is side-effect-free.** The client paints `dumpVt` with | ||
| 42 | `extra = .none` (content + inline SGR only): the default `.styles` extra | ||
| 43 | emits the full OSC 4 palette, which would overwrite the host terminal's | ||
| 44 | theme on every repaint (~5KB/frame). | ||
| 45 | - **Alt-screen snapshot limitation:** the snapshot carries only the active | ||
| 46 | screen's content. A replica that leaves the alt screen (TUI exits after a | ||
| 47 | reattach) reveals a blank primary screen instead of the shell history. | ||
| 48 | Locked by an engine test; M3 owes a two-screen snapshot or lazy fetch. | ||
| 49 | - **Wire format: 1-byte type + u32 LE length frames, M2 only.** The | ||
| 50 | handoff's "msgpack or protobuf, do not invent one" is owed at M4, where | ||
| 51 | structured payloads (damage regions, cell runs) first appear; every M2 | ||
| 52 | payload is a byte blob or two u16s. Recorded so M4 doesn't inherit this | ||
| 53 | by inertia. | ||
| 54 | - **Single interactive client + dump-only observers.** Second `attach` is | ||
| 55 | refused with exit_status{1}; unattached connections may `debug_dump` | ||
| 56 | (keeps `muxd dump` working alongside a live client). M5 replaces this. | ||
| 57 | - **Blocking frame I/O.** One local client on a Unix socket; a stuck client | ||
| 58 | can stall the daemon. Buffered nonblocking I/O is owed by M4 (network). | ||
| 59 | - **Detach chord: Ctrl-\ (0x1c).** No keybinding layer in the prototype. | ||
| 60 | - **Render: home + ED(2) full repaint under CSI ?2026 sync.** Wasteful by | ||
| 61 | design (deltas are M4); sync-output makes it artifact-free on modern | ||
| 62 | terminals. | ||
| 63 | - **Degenerate sizes rejected.** A pty can report 0x0 (`script` with piped | ||
| 64 | stdin); a zero-sized grid trips engine asserts. Client treats <2x2 as | ||
| 65 | "unknown" (falls back 80x24); daemon ignores <2x2 attach/resize. | ||
| 66 | - **systemd socket activation supported** via LISTEN_PID/LISTEN_FDS (fd 3); | ||
| 67 | units in contrib/. Verified with systemd-socket-activate. | ||
| 68 | |||
| 32 | ## Open (owed by later milestones) | 69 | ## Open (owed by later milestones) |
| 33 | 70 | ||
| 34 | - Resize policy under multiple clients (M5) | 71 | - Resize policy under multiple clients (M5) |
docs/superpowers/plans/2026-08-07-m2-the-loop.md
| Old | New | ||
|---|---|---|---|
| @@ -50,7 +50,7 @@ Decisions this plan locks in (recorded in decisions.md, Task 7): wire format is | |||
| 50 | - Create: `src/protocol.zig` | 50 | - Create: `src/protocol.zig` |
| 51 | - Modify: `build.zig` (add protocol module + its test) | 51 | - Modify: `build.zig` (add protocol module + its test) |
| 52 | 52 | ||
| 53 | - [ ] **Step 1: Write `src/protocol.zig` with failing tests** | 53 | - [x] **Step 1: Write `src/protocol.zig` with failing tests** |
| 54 | 54 | ||
| 55 | ```zig | 55 | ```zig |
| 56 | //! Wire protocol: length-prefixed frames over a Unix socket. | 56 | //! Wire protocol: length-prefixed frames over a Unix socket. |
| @@ -176,7 +176,7 @@ test "size encode/decode round trip" { | |||
| 176 | 176 | ||
| 177 | Implementation note on the socketpair test: `std.posix.socketpair` returns `[2]fd_t`; if the double-close dance reads poorly, restructure with explicit `close` calls instead of defers — behavior over form. | 177 | Implementation note on the socketpair test: `std.posix.socketpair` returns `[2]fd_t`; if the double-close dance reads poorly, restructure with explicit `close` calls instead of defers — behavior over form. |
| 178 | 178 | ||
| 179 | - [ ] **Step 2: Add module + test to `build.zig`** | 179 | - [x] **Step 2: Add module + test to `build.zig`** |
| 180 | 180 | ||
| 181 | Insert before `engine_mod`: | 181 | Insert before `engine_mod`: |
| 182 | 182 | ||
| @@ -190,12 +190,12 @@ Insert before `engine_mod`: | |||
| 190 | 190 | ||
| 191 | Add `protocol_mod` to the test-step module list. | 191 | Add `protocol_mod` to the test-step module list. |
| 192 | 192 | ||
| 193 | - [ ] **Step 3: Run tests** | 193 | - [x] **Step 3: Run tests** |
| 194 | 194 | ||
| 195 | Run: `make test` | 195 | Run: `make test` |
| 196 | Expected: exit 0 (protocol tests pass; existing suites unaffected). | 196 | Expected: exit 0 (protocol tests pass; existing suites unaffected). |
| 197 | 197 | ||
| 198 | - [ ] **Step 4: Commit** | 198 | - [x] **Step 4: Commit** |
| 199 | 199 | ||
| 200 | ```bash | 200 | ```bash |
| 201 | git add src/protocol.zig build.zig | 201 | git add src/protocol.zig build.zig |
| @@ -209,7 +209,7 @@ git commit -m "feat: length-prefixed frame protocol" | |||
| 209 | **Files:** | 209 | **Files:** |
| 210 | - Modify: `src/engine.zig` | 210 | - Modify: `src/engine.zig` |
| 211 | 211 | ||
| 212 | - [ ] **Step 1: Add failing tests (append to `src/engine.zig`)** | 212 | - [x] **Step 1: Add failing tests (append to `src/engine.zig`)** |
| 213 | 213 | ||
| 214 | ```zig | 214 | ```zig |
| 215 | test "Engine: full-state snapshot restores grid, style, and cursor in a fresh engine" { | 215 | test "Engine: full-state snapshot restores grid, style, and cursor in a fresh engine" { |
| @@ -282,12 +282,12 @@ test "Engine: alt-screen state survives snapshot into fresh engine" { | |||
| 282 | 282 | ||
| 283 | Note: the alt-screen test is the sharp edge — `TerminalFormatter` only emits the *active* screen's content plus mode state. If the third assertion (primary content after `?1049l`) fails, that is a real M2 limitation to record in decisions.md (primary screen restored blank after a TUI exits post-reattach); weaken only that assertion (drop it, keep the first), and keep going — M3 owes the fix. | 283 | Note: the alt-screen test is the sharp edge — `TerminalFormatter` only emits the *active* screen's content plus mode state. If the third assertion (primary content after `?1049l`) fails, that is a real M2 limitation to record in decisions.md (primary screen restored blank after a TUI exits post-reattach); weaken only that assertion (drop it, keep the first), and keep going — M3 owes the fix. |
| 284 | 284 | ||
| 285 | - [ ] **Step 2: Run to verify failure** | 285 | - [x] **Step 2: Run to verify failure** |
| 286 | 286 | ||
| 287 | Run: `make test` | 287 | Run: `make test` |
| 288 | Expected: compile error — `dumpState`, `cursorPos`, `reset` not defined. | 288 | Expected: compile error — `dumpState`, `cursorPos`, `reset` not defined. |
| 289 | 289 | ||
| 290 | - [ ] **Step 3: Implement (add to `Engine` in `src/engine.zig`)** | 290 | - [x] **Step 3: Implement (add to `Engine` in `src/engine.zig`)** |
| 291 | 291 | ||
| 292 | ```zig | 292 | ```zig |
| 293 | /// Full terminal state (palette, modes, cursor, styles, active-screen | 293 | /// Full terminal state (palette, modes, cursor, styles, active-screen |
| @@ -318,12 +318,12 @@ Expected: compile error — `dumpState`, `cursorPos`, `reset` not defined. | |||
| 318 | } | 318 | } |
| 319 | ``` | 319 | ``` |
| 320 | 320 | ||
| 321 | - [ ] **Step 4: Run tests** | 321 | - [x] **Step 4: Run tests** |
| 322 | 322 | ||
| 323 | Run: `make test` | 323 | Run: `make test` |
| 324 | Expected: exit 0. If the alt-screen test's last assertion fails, apply the note from Step 1. | 324 | Expected: exit 0. If the alt-screen test's last assertion fails, apply the note from Step 1. |
| 325 | 325 | ||
| 326 | - [ ] **Step 5: Commit** | 326 | - [x] **Step 5: Commit** |
| 327 | 327 | ||
| 328 | ```bash | 328 | ```bash |
| 329 | git add src/engine.zig | 329 | git add src/engine.zig |
| @@ -339,7 +339,7 @@ git commit -m "feat: engine full-state snapshot, cursor accessor, reset" | |||
| 339 | - Delete: `src/debug.zig` | 339 | - Delete: `src/debug.zig` |
| 340 | - Modify: `build.zig` | 340 | - Modify: `build.zig` |
| 341 | 341 | ||
| 342 | - [ ] **Step 1: Write `src/server.zig` (implementation + unit test)** | 342 | - [x] **Step 1: Write `src/server.zig` (implementation + unit test)** |
| 343 | 343 | ||
| 344 | ```zig | 344 | ```zig |
| 345 | //! muxd's daemon core: one session (engine + pty), one listener, at most | 345 | //! muxd's daemon core: one session (engine + pty), one listener, at most |
| @@ -528,7 +528,7 @@ pub const Server = struct { | |||
| 528 | }; | 528 | }; |
| 529 | ``` | 529 | ``` |
| 530 | 530 | ||
| 531 | - [ ] **Step 2: Add the integration test (append to `src/server.zig`)** | 531 | - [x] **Step 2: Add the integration test (append to `src/server.zig`)** |
| 532 | 532 | ||
| 533 | This is the M2 falsification test: a scripted client attaches, types, rebuilds a replica from snapshots, and byte-compares replica vs daemon. | 533 | This is the M2 falsification test: a scripted client attaches, types, rebuilds a replica from snapshots, and byte-compares replica vs daemon. |
| 534 | 534 | ||
| @@ -613,7 +613,7 @@ test "Server: replica rebuilt from snapshots matches the authoritative grid" { | |||
| 613 | } | 613 | } |
| 614 | ``` | 614 | ``` |
| 615 | 615 | ||
| 616 | - [ ] **Step 3: Wire into `build.zig`, remove debug module** | 616 | - [x] **Step 3: Wire into `build.zig`, remove debug module** |
| 617 | 617 | ||
| 618 | Replace the `debug_mod` block with: | 618 | Replace the `debug_mod` block with: |
| 619 | 619 | ||
| @@ -633,12 +633,12 @@ In `exe_mod` imports: replace `debug` with `server` and add `protocol`. Update t | |||
| 633 | 633 | ||
| 634 | `main.zig` still references `debug` at this point — Task 4 rewrites it; to keep this task green, apply Task 4's `main.zig` in the same commit if the build breaks, or temporarily stub `main.zig` to `pub fn main() !void {}` (restored in Task 4). Prefer the stub: smaller diff per commit. | 634 | `main.zig` still references `debug` at this point — Task 4 rewrites it; to keep this task green, apply Task 4's `main.zig` in the same commit if the build breaks, or temporarily stub `main.zig` to `pub fn main() !void {}` (restored in Task 4). Prefer the stub: smaller diff per commit. |
| 635 | 635 | ||
| 636 | - [ ] **Step 4: Run tests** | 636 | - [x] **Step 4: Run tests** |
| 637 | 637 | ||
| 638 | Run: `make test` | 638 | Run: `make test` |
| 639 | Expected: exit 0. The integration test takes a few seconds (real shell under a pty). A timing flake here is a bug: the loop retries until the *replica* converges, and the final compare uses the daemon's own reply ordering — investigate rather than extending timeouts blindly. | 639 | Expected: exit 0. The integration test takes a few seconds (real shell under a pty). A timing flake here is a bug: the loop retries until the *replica* converges, and the final compare uses the daemon's own reply ordering — investigate rather than extending timeouts blindly. |
| 640 | 640 | ||
| 641 | - [ ] **Step 5: Commit** | 641 | - [x] **Step 5: Commit** |
| 642 | 642 | ||
| 643 | ```bash | 643 | ```bash |
| 644 | git add -A | 644 | git add -A |
| @@ -652,7 +652,7 @@ git commit -m "feat: daemon server core with snapshot broadcast; replica fidelit | |||
| 652 | **Files:** | 652 | **Files:** |
| 653 | - Rewrite: `src/main.zig` | 653 | - Rewrite: `src/main.zig` |
| 654 | 654 | ||
| 655 | - [ ] **Step 1: Rewrite `src/main.zig`** | 655 | - [x] **Step 1: Rewrite `src/main.zig`** |
| 656 | 656 | ||
| 657 | ```zig | 657 | ```zig |
| 658 | //! muxd — daemon entrypoint. `run` hosts the session; `dump` prints the | 658 | //! muxd — daemon entrypoint. `run` hosts the session; `dump` prints the |
| @@ -787,12 +787,12 @@ Amended `Server` pieces (use these, not the refusal version, when executing Task | |||
| 787 | 787 | ||
| 788 | Observers are polled too; an observer that sends `attach` is promoted to `self.client` if the slot is free (else refused with `exit_status{1}` and closed). `debug_dump` works from any connection. The poll fd array becomes: pty, listener, client (or -1), observers[0..4] (or -1). On any observer error/EOF, clear the slot. This is ~30 lines of bookkeeping; keep it inside `serviceClient`-style helpers (`serviceFd(fd) enum { keep, drop }`). | 788 | Observers are polled too; an observer that sends `attach` is promoted to `self.client` if the slot is free (else refused with `exit_status{1}` and closed). `debug_dump` works from any connection. The poll fd array becomes: pty, listener, client (or -1), observers[0..4] (or -1). On any observer error/EOF, clear the slot. This is ~30 lines of bookkeeping; keep it inside `serviceClient`-style helpers (`serviceFd(fd) enum { keep, drop }`). |
| 789 | 789 | ||
| 790 | - [ ] **Step 2: Run tests + build** | 790 | - [x] **Step 2: Run tests + build** |
| 791 | 791 | ||
| 792 | Run: `make test && make build` | 792 | Run: `make test && make build` |
| 793 | Expected: exit 0, both binaries build (mux arrives in Task 5; only muxd exists yet). | 793 | Expected: exit 0, both binaries build (mux arrives in Task 5; only muxd exists yet). |
| 794 | 794 | ||
| 795 | - [ ] **Step 3: Commit** | 795 | - [x] **Step 3: Commit** |
| 796 | 796 | ||
| 797 | ```bash | 797 | ```bash |
| 798 | git add src/main.zig src/server.zig | 798 | git add src/main.zig src/server.zig |
| @@ -807,7 +807,7 @@ git commit -m "feat: muxd speaks the real protocol; observer connections for dum | |||
| 807 | - Create: `src/client.zig`, `src/mux_main.zig` | 807 | - Create: `src/client.zig`, `src/mux_main.zig` |
| 808 | - Modify: `build.zig` | 808 | - Modify: `build.zig` |
| 809 | 809 | ||
| 810 | - [ ] **Step 1: Write `src/client.zig`** | 810 | - [x] **Step 1: Write `src/client.zig`** |
| 811 | 811 | ||
| 812 | ```zig | 812 | ```zig |
| 813 | //! mux client: connects, attaches, maintains a replica engine rebuilt | 813 | //! mux client: connects, attaches, maintains a replica engine rebuilt |
| @@ -967,7 +967,7 @@ test "render paints replica content with cursor restore" { | |||
| 967 | 967 | ||
| 968 | Sigaction API drift note: on this std version `sigaction` returns `void` and `handler` is `.{ .handler = f }` with `callconv(.c)`; if the compiler disagrees, check `std.posix.Sigaction` in the 0.15.2 lib source (`~/Downloads/zig-x86_64-linux-0.15.2/lib/std/posix.zig`). | 968 | Sigaction API drift note: on this std version `sigaction` returns `void` and `handler` is `.{ .handler = f }` with `callconv(.c)`; if the compiler disagrees, check `std.posix.Sigaction` in the 0.15.2 lib source (`~/Downloads/zig-x86_64-linux-0.15.2/lib/std/posix.zig`). |
| 969 | 969 | ||
| 970 | - [ ] **Step 2: Write `src/mux_main.zig`** | 970 | - [x] **Step 2: Write `src/mux_main.zig`** |
| 971 | 971 | ||
| 972 | ```zig | 972 | ```zig |
| 973 | //! mux — client binary. `mux [--sock PATH]` attaches to the running muxd. | 973 | //! mux — client binary. `mux [--sock PATH]` attaches to the running muxd. |
| @@ -1009,7 +1009,7 @@ pub fn main() !u8 { | |||
| 1009 | } | 1009 | } |
| 1010 | ``` | 1010 | ``` |
| 1011 | 1011 | ||
| 1012 | - [ ] **Step 3: Wire into `build.zig`** | 1012 | - [x] **Step 3: Wire into `build.zig`** |
| 1013 | 1013 | ||
| 1014 | After `server_mod`: | 1014 | After `server_mod`: |
| 1015 | 1015 | ||
| @@ -1040,12 +1040,12 @@ After `server_mod`: | |||
| 1040 | 1040 | ||
| 1041 | Add `client_mod` to the test-step list. Pass `mux_exe` to the e2e runner as a second artifact arg (`e2e.addArtifactArg(mux_exe);`). | 1041 | Add `client_mod` to the test-step list. Pass `mux_exe` to the e2e runner as a second artifact arg (`e2e.addArtifactArg(mux_exe);`). |
| 1042 | 1042 | ||
| 1043 | - [ ] **Step 4: Run tests + build** | 1043 | - [x] **Step 4: Run tests + build** |
| 1044 | 1044 | ||
| 1045 | Run: `make test && make build` | 1045 | Run: `make test && make build` |
| 1046 | Expected: exit 0; `zig-out/bin/mux` and `zig-out/bin/muxd` both exist. | 1046 | Expected: exit 0; `zig-out/bin/mux` and `zig-out/bin/muxd` both exist. |
| 1047 | 1047 | ||
| 1048 | - [ ] **Step 5: Commit** | 1048 | - [x] **Step 5: Commit** |
| 1049 | 1049 | ||
| 1050 | ```bash | 1050 | ```bash |
| 1051 | git add src/client.zig src/mux_main.zig build.zig | 1051 | git add src/client.zig src/mux_main.zig build.zig |
| @@ -1059,7 +1059,7 @@ git commit -m "feat: mux client with replica grid, renderer, resize, Ctrl-\\ det | |||
| 1059 | **Files:** | 1059 | **Files:** |
| 1060 | - Rewrite: `test/e2e.sh` | 1060 | - Rewrite: `test/e2e.sh` |
| 1061 | 1061 | ||
| 1062 | - [ ] **Step 1: Rewrite `test/e2e.sh`** | 1062 | - [x] **Step 1: Rewrite `test/e2e.sh`** |
| 1063 | 1063 | ||
| 1064 | ```sh | 1064 | ```sh |
| 1065 | #!/bin/sh | 1065 | #!/bin/sh |
| @@ -1101,12 +1101,12 @@ kill -0 "$DPID" || { echo "e2e FAIL: daemon died on detach"; exit 1; } | |||
| 1101 | echo "e2e OK" | 1101 | echo "e2e OK" |
| 1102 | ``` | 1102 | ``` |
| 1103 | 1103 | ||
| 1104 | - [ ] **Step 2: Run it** | 1104 | - [x] **Step 2: Run it** |
| 1105 | 1105 | ||
| 1106 | Run: `make e2e` | 1106 | Run: `make e2e` |
| 1107 | Expected: `e2e OK`. | 1107 | Expected: `e2e OK`. |
| 1108 | 1108 | ||
| 1109 | - [ ] **Step 3: Commit** | 1109 | - [x] **Step 3: Commit** |
| 1110 | 1110 | ||
| 1111 | ```bash | 1111 | ```bash |
| 1112 | git add test/e2e.sh | 1112 | git add test/e2e.sh |
| @@ -1121,7 +1121,7 @@ git commit -m "test: e2e through the real client/daemon loop" | |||
| 1121 | - Create: `contrib/muxd.service`, `contrib/muxd.socket` | 1121 | - Create: `contrib/muxd.service`, `contrib/muxd.socket` |
| 1122 | - Modify: `docs/decisions.md`, `README.md` | 1122 | - Modify: `docs/decisions.md`, `README.md` |
| 1123 | 1123 | ||
| 1124 | - [ ] **Step 1: Write the units** | 1124 | - [x] **Step 1: Write the units** |
| 1125 | 1125 | ||
| 1126 | `contrib/muxd.socket`: | 1126 | `contrib/muxd.socket`: |
| 1127 | 1127 | ||
| @@ -1149,7 +1149,7 @@ ExecStart=%h/code/rad/mux/zig-out/bin/muxd run | |||
| 1149 | Restart=no | 1149 | Restart=no |
| 1150 | ``` | 1150 | ``` |
| 1151 | 1151 | ||
| 1152 | - [ ] **Step 2: Manual demo (M2 acceptance)** | 1152 | - [x] **Step 2: Manual demo (M2 acceptance)** |
| 1153 | 1153 | ||
| 1154 | In terminal A: `make build && ./zig-out/bin/muxd run` | 1154 | In terminal A: `make build && ./zig-out/bin/muxd run` |
| 1155 | In terminal B: `./zig-out/bin/mux` — a live shell appears. Then: | 1155 | In terminal B: `./zig-out/bin/mux` — a live shell appears. Then: |
| @@ -1162,7 +1162,7 @@ In terminal B: `./zig-out/bin/mux` — a live shell appears. Then: | |||
| 1162 | 1162 | ||
| 1163 | Record any artifact (flicker beyond taste, wrong cells, stuck cursor) as a bug before declaring M2 done. Systemd socket-activation check (optional, needs lingering not required for the demo): `systemctl --user enable --now` the units from `contrib/` after copying to `~/.config/systemd/user/`, then `mux` with no daemon pre-started. | 1163 | Record any artifact (flicker beyond taste, wrong cells, stuck cursor) as a bug before declaring M2 done. Systemd socket-activation check (optional, needs lingering not required for the demo): `systemctl --user enable --now` the units from `contrib/` after copying to `~/.config/systemd/user/`, then `mux` with no daemon pre-started. |
| 1164 | 1164 | ||
| 1165 | - [ ] **Step 3: Update `docs/decisions.md`** — append under a new `## 2026-08-07 (M2)` heading: | 1165 | - [x] **Step 3: Update `docs/decisions.md`** — append under a new `## 2026-08-07 (M2)` heading: |
| 1166 | 1166 | ||
| 1167 | ```markdown | 1167 | ```markdown |
| 1168 | ## 2026-08-07 (M2) | 1168 | ## 2026-08-07 (M2) |
| @@ -1190,7 +1190,7 @@ Record any artifact (flicker beyond taste, wrong cells, stuck cursor) as a bug b | |||
| 1190 | alt screen after rebuild]. | 1190 | alt screen after rebuild]. |
| 1191 | ``` | 1191 | ``` |
| 1192 | 1192 | ||
| 1193 | - [ ] **Step 4: Update `README.md`** — replace the Status/usage section: | 1193 | - [x] **Step 4: Update `README.md`** — replace the Status/usage section: |
| 1194 | 1194 | ||
| 1195 | ```markdown | 1195 | ```markdown |
| 1196 | Status: **M2 — the loop.** | 1196 | Status: **M2 — the loop.** |
| @@ -1204,7 +1204,7 @@ Status: **M2 — the loop.** | |||
| 1204 | systemd user units (socket activation) live in `contrib/`. | 1204 | systemd user units (socket activation) live in `contrib/`. |
| 1205 | ``` | 1205 | ``` |
| 1206 | 1206 | ||
| 1207 | - [ ] **Step 5: Commit** | 1207 | - [x] **Step 5: Commit** |
| 1208 | 1208 | ||
| 1209 | ```bash | 1209 | ```bash |
| 1210 | git add contrib/ docs/decisions.md README.md | 1210 | git add contrib/ docs/decisions.md README.md |