74730ed7
refactor: isolate native core and component development gates
a73x 2026-09-05 15:42
Commit message
CLAUDE.md
| Old | New | ||
|---|---|---|---|
| @@ -16,6 +16,8 @@ make mac-sdk # Darwin only, and `make build` runs it: a shadow | |||
| 16 | make build test e2e # Makefile already points at it | 16 | make build test e2e # Makefile already points at it |
| 17 | make check # fmt + unit tests + shell syntax + comment-claim refs — pre-commit gate | 17 | make check # fmt + unit tests + shell syntax + comment-claim refs — pre-commit gate |
| 18 | make ci # check + e2e + agent + throughput — the delivery gate | 18 | make ci # check + e2e + agent + throughput — the delivery gate |
| 19 | make daemon-test client-test # focused component tests; same artifacts as make test | ||
| 20 | make native-core-test # GUI workspace/interaction tests without window/font libraries | ||
| 19 | make agent soak bench throughput | 21 | make agent soak bench throughput |
| 20 | make native native-e2e # opt-in muxg; SDL3/freetype/fontconfig/HarfBuzz + GL headers; e2e also needs python3 | 22 | make native native-e2e # opt-in muxg; SDL3/freetype/fontconfig/HarfBuzz + GL headers; e2e also needs python3 |
| 21 | make vm # real user journeys against the mux-e2e VM (test/vm.sh) | 23 | make vm # real user journeys against the mux-e2e VM (test/vm.sh) |
| @@ -528,6 +530,9 @@ native session panes; it is built only by the native steps and is never installe | |||
| 528 | 530 | ||
| 529 | ## Where the answers live | 531 | ## Where the answers live |
| 530 | 532 | ||
| 533 | `docs/component-ownership.md` maps the daemon, shared client/wire, and native GUI | ||
| 534 | boundaries to focused gates and proposes ownership responsibilities. | ||
| 535 | |||
| 531 | `docs/roadmap.md` (~660 ln) the ranked queue, but stale past 2026-08-16 — | 536 | `docs/roadmap.md` (~660 ln) the ranked queue, but stale past 2026-08-16 — |
| 532 | `git-collab issue list` is the live order · `docs/decisions.md` (9.2k ln, grep | 537 | `git-collab issue list` is the live order · `docs/decisions.md` (9.2k ln, grep |
| 533 | only) every decision + measurement · `README.md` user-facing usage. Design | 538 | only) every decision + measurement · `README.md` user-facing usage. Design |
Makefile
| Old | New | ||
|---|---|---|---|
| @@ -31,7 +31,7 @@ SHA256 ?= shasum -a 256 | |||
| 31 | endif | 31 | endif |
| 32 | MUX_TARGET ?= x86_64-linux-musl | 32 | MUX_TARGET ?= x86_64-linux-musl |
| 33 | 33 | ||
| 34 | .PHONY: build check ci test e2e soak bench agent native native-e2e native-stress throughput vm coverage deps clean clean-deps xversion xversion-build install release release-mac mac-sdk mac xos provision-mac | 34 | .PHONY: build check ci test daemon-test client-test native-core-test e2e soak bench agent native native-e2e native-stress throughput vm coverage deps clean clean-deps xversion xversion-build install release release-mac mac-sdk mac xos provision-mac |
| 35 | 35 | ||
| 36 | # The QUIC stack (deps/quic) is built on demand by build.zig, so no target | 36 | # The QUIC stack (deps/quic) is built on demand by build.zig, so no target |
| 37 | # here needs to depend on this one. It exists to make the one-time cost | 37 | # here needs to depend on this one. It exists to make the one-time cost |
| @@ -269,6 +269,11 @@ ci: | |||
| 269 | agent: mac-sdk | 269 | agent: mac-sdk |
| 270 | $(ZIG) build agent | 270 | $(ZIG) build agent |
| 271 | 271 | ||
| 272 | # Component gates reuse the aggregate test artifacts. Native core needs no | ||
| 273 | # window/font libraries; the full native gate below includes its tests too. | ||
| 274 | daemon-test client-test native-core-test: mac-sdk | ||
| 275 | $(ZIG) build $@ | ||
| 276 | |||
| 272 | # Opt-in because these link system SDL3, freetype, fontconfig and HarfBuzz. | 277 | # Opt-in because these link system SDL3, freetype, fontconfig and HarfBuzz. |
| 273 | # They stay outside every default build and delivery gate. | 278 | # They stay outside every default build and delivery gate. |
| 274 | native: mac-sdk | 279 | native: mac-sdk |
RETRO.md
| Old | New | ||
|---|---|---|---|
| @@ -576,8 +576,95 @@ still being written. | |||
| 576 | out of `frame.zig`, preserving passive frame and state observations. | 576 | out of `frame.zig`, preserving passive frame and state observations. |
| 577 | - [ ] A future recording-tooling package should promote the recorder/clip driver; | 577 | - [ ] A future recording-tooling package should promote the recorder/clip driver; |
| 578 | only the scale gate moved into the repository in this bounded sprint. | 578 | only the scale gate moved into the repository in this bounded sprint. |
| 579 | - [ ] If the terminal host-picker timeout recurs, capture its menu/discovery | 579 | - [x] Investigate the terminal host-picker timeout if it recurs. The component |
| 580 | ordering before adjusting waits. Its source path is `test/e2e_09_hosts.sh`. | 580 | sprint below reproduced the ordering hazard and replaced the missing readiness |
| 581 | condition with an event-based wait in `test/e2e_09_hosts.sh`. No timeout grew. | ||
| 581 | - [x] Obtain authorization before the next production sprint. The user requested | 582 | - [x] Obtain authorization before the next production sprint. The user requested |
| 582 | another separation sprint aimed at independent daemon/GUI work, followed by | 583 | another separation sprint aimed at independent daemon/GUI work, followed by |
| 583 | clearer code ownership. That authorization does not imply demo acceptance. | 584 | clearer code ownership. That authorization does not imply demo acceptance. |
| 585 | |||
| 586 | |||
| 587 | ## Component separation — independent daemon and GUI work, 2026-09-05 | ||
| 588 | |||
| 589 | The user authorized another separation sprint, followed by a clearer ownership | ||
| 590 | split. Implementation, closing review and all final delivery gates are complete, | ||
| 591 | including full CI after the fixture fix. The actual GUI recording is retained. The | ||
| 592 | [delivery record](docs/superpowers/plans/2026-09-05-component-separation.md) | ||
| 593 | contains the graph, commands and evidence. Recorded-demo acceptance is pending. | ||
| 594 | |||
| 595 | ### Results and cleanup | ||
| 596 | |||
| 597 | - The new `native_core` module exclusively owns workspace, runtime, picker, | ||
| 598 | persistence and interaction. The frame/painter consumes its declared import. | ||
| 599 | No behavior or file placement changed; compatibility aliases keep the native | ||
| 600 | module's existing public surface available. | ||
| 601 | - `make native-core-test` runs without window/font libraries. `make native` | ||
| 602 | includes the core and frame/painter suites; their ReleaseSafe run passed 47 | ||
| 603 | tests. `make daemon-test` and `make client-test` reuse the same artifacts as | ||
| 604 | the aggregate unit gate, without running unrelated components. | ||
| 605 | - Six disposable-snapshot probes proved the boundaries: broken GUI compilation | ||
| 606 | leaves daemon/core tests usable; broken daemon compilation leaves GUI tests | ||
| 607 | usable; core tests need no window-library package metadata. Matching negative | ||
| 608 | controls failed as expected. No poisoned source entered the working checkout. | ||
| 609 | - Opening assessment preserved the existing shared client facade and module | ||
| 610 | table; neither needed a broad rewrite. Closing cleanup removed a redundant | ||
| 611 | test alias and corrected source-ban/build comments. All core child files have | ||
| 612 | one source owner, and native tests explicitly retain both suites. | ||
| 613 | - The recurring terminal CI failure was a fixture race, not a native module | ||
| 614 | change: selecting row 2 immediately after Enter could use a stale one-session | ||
| 615 | poll and clamp to row 1. The fix waits for the newly born row itself, retains | ||
| 616 | the selected-row/holder assertions, and changes no timeout. The focused | ||
| 617 | 15-scenario host group and subsequent independent full CI both passed. | ||
| 618 | |||
| 619 | ### Validation limits and recording | ||
| 620 | |||
| 621 | Native integration (including real old-daemon compatibility), five real OpenSSH | ||
| 622 | recovery groups, high-DPI scale transitions and the actual GUI recording passed. | ||
| 623 | The initial stress run missed frame p99 at 20.176 ms; the previous release also | ||
| 624 | missed the same 20 ms budget on that compositor. A normal-logging compositor | ||
| 625 | repeat passed without budget changes: frame p99 18.992 ms before reopen and | ||
| 626 | 18.246 ms afterward, sampled input maxima 55.5/55.4 ms for shared/separate | ||
| 627 | daemons, 104.6 MB over 30.1 seconds. The exact cause of the variation is unproven; | ||
| 628 | all results and renderer provenance limits are retained. The input measurements | ||
| 629 | are polling upper bounds, and no new macOS evidence is claimed. | ||
| 630 | |||
| 631 | The 56.8-second final release recording has two disclosed restart cuts and no | ||
| 632 | audio. Fixtures and the owned compositor were stopped. The retained local video | ||
| 633 | is `native-component-trial.mp4` beside this thread's other visualization artifacts; | ||
| 634 | no new sharing payload was published. Acceptance remains a user decision. | ||
| 635 | |||
| 636 | ### Delegation and ownership | ||
| 637 | |||
| 638 | Fresh short briefs and immediately checked session metadata confirmed Luna for | ||
| 639 | implementation and Terra for review. Luna wired the core/frame source split and | ||
| 640 | repaired the test fixture; root owned build wiring, independent probes, delivery | ||
| 641 | gates and documentation. Terra caught the insufficient stale-row readiness wait | ||
| 642 | and reviewed the corrected event condition, build artifacts and ownership map. | ||
| 643 | |||
| 644 | Available cumulative counters for these sprint-specific agent sessions are below. | ||
| 645 | Cached input is included in input, not additive; counters are not a monetary bill. | ||
| 646 | |||
| 647 | | Agent | Input tokens | Cached input (included) | Output tokens | | ||
| 648 | | --- | ---: | ---: | ---: | | ||
| 649 | | Luna implementer | 3,801,635 | 3,675,648 | 10,737 | | ||
| 650 | | Terra reviewer | 3,593,717 | 3,406,080 | 10,881 | | ||
| 651 | |||
| 652 | The [ownership proposal](docs/component-ownership.md) recommends daemon, native | ||
| 653 | GUI and shared wire/client responsibilities first. GUI policy and rendering have | ||
| 654 | an internal seam when separate owners become useful. Shared build/release and | ||
| 655 | wire-contract changes still need coordination. No people or CODEOWNERS rules | ||
| 656 | were invented, and mixed directories are called out explicitly. | ||
| 657 | |||
| 658 | ### Next actions | ||
| 659 | |||
| 660 | - [ ] Record acceptance of this demo separately from earlier pending recordings | ||
| 661 | and hands-on feedback. | ||
| 662 | - [ ] Assign people or agents to the proposed ownership areas when those names | ||
| 663 | are known; use separate worktrees and the focused gates for internal work. | ||
| 664 | - [ ] Keep shared wire/lifecycle changes coordinated across affected frontends. | ||
| 665 | Independent compilation does not make their semantics independent. | ||
| 666 | - [ ] If revisiting performance measurement, distinguish frame preparation from | ||
| 667 | compositor swap waiting and record display/logging conditions before drawing | ||
| 668 | conclusions from a narrow frame-budget miss. | ||
| 669 | - [ ] Promote the general recorder/clip fixture in a future tooling package; the | ||
| 670 | repository scale gate already removed one temporary-script dependency. | ||
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -325,7 +325,8 @@ const mod_table = [_]ModSpec{ | |||
| 325 | .{ .name = "mux", .path = "src/cli/mux.zig", .link_libc = true, .imports = &.{ "daemon", "client", "wall", "agent", "webhub", "term", "proxy", "quic", "xdg", "spawn", "sockpath", "cliflags", "dial", "server_os", "client_os" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, | 325 | .{ .name = "mux", .path = "src/cli/mux.zig", .link_libc = true, .imports = &.{ "daemon", "client", "wall", "agent", "webhub", "term", "proxy", "quic", "xdg", "spawn", "sockpath", "cliflags", "dial", "server_os", "client_os" }, .test_imports = &.{"testtmp"}, .quic_tests = true }, |
| 326 | // Opt-in rows: no default artifact or test names them, so machines | 326 | // Opt-in rows: no default artifact or test names them, so machines |
| 327 | // without the viewer's system libraries keep all existing gates. | 327 | // without the viewer's system libraries keep all existing gates. |
| 328 | .{ .name = "native", .path = "src/gui/native.zig", .link_libc = true, .imports = &.{ "client", "term" }, .opt_in = true }, | 328 | .{ .name = "native_core", .path = "src/gui/native_core.zig", .link_libc = true, .imports = &.{ "client", "term" }, .opt_in = true }, |
| 329 | .{ .name = "native", .path = "src/gui/native.zig", .link_libc = true, .imports = &.{ "native_core", "client", "term" }, .opt_in = true }, | ||
| 329 | .{ .name = "muxg", .path = "src/cli/muxg.zig", .link_libc = true, .imports = &.{ "native", "client", "term", "cliflags", "sockpath", "xdg" }, .opt_in = true }, | 330 | .{ .name = "muxg", .path = "src/cli/muxg.zig", .link_libc = true, .imports = &.{ "native", "client", "term", "cliflags", "sockpath", "xdg" }, .opt_in = true }, |
| 330 | }; | 331 | }; |
| 331 | 332 | ||
| @@ -1071,7 +1072,14 @@ pub fn build(b: *std.Build) void { | |||
| 1071 | // a clean checkout — the dependency must reach the test binaries, | 1072 | // a clean checkout — the dependency must reach the test binaries, |
| 1072 | // not only the binary (decisions.md, M8). | 1073 | // not only the binary (decisions.md, M8). |
| 1073 | if (mod_table[i].quic_tests) linkQuic(b, t, quic); | 1074 | if (mod_table[i].quic_tests) linkQuic(b, t, quic); |
| 1074 | test_step.dependOn(&b.addRunArtifact(t).step); | 1075 | const run = b.addRunArtifact(t); |
| 1076 | test_step.dependOn(&run.step); | ||
| 1077 | // Component owners use the same artifact as the full gate, without | ||
| 1078 | // compiling or running another component's suite. | ||
| 1079 | if (std.mem.eql(u8, name, "daemon") or std.mem.eql(u8, name, "client")) { | ||
| 1080 | const component_test = b.step(b.fmt("{s}-test", .{name}), b.fmt("Run {s} component tests", .{name})); | ||
| 1081 | component_test.dependOn(&run.step); | ||
| 1082 | } | ||
| 1075 | } | 1083 | } |
| 1076 | 1084 | ||
| 1077 | // web/verify.js drives mux_core.wasm through the page's real call | 1085 | // web/verify.js drives mux_core.wasm through the page's real call |
| @@ -1131,8 +1139,8 @@ pub fn build(b: *std.Build) void { | |||
| 1131 | const agent_step = b.step("agent", "Run the agent-surface end-to-end suite"); | 1139 | const agent_step = b.step("agent", "Run the agent-surface end-to-end suite"); |
| 1132 | agent_step.dependOn(&agent.step); | 1140 | agent_step.dependOn(&agent.step); |
| 1133 | 1141 | ||
| 1134 | // The native client is entirely opt-in. These are the only compile | 1142 | // The native client and its core are opt-in. Only the window/painter |
| 1135 | // steps that reach its rows or ask pkg-config for its system libraries. | 1143 | // artifacts ask pkg-config for the native system libraries. |
| 1136 | // GL functions are resolved through the window library at runtime, so | 1144 | // GL functions are resolved through the window library at runtime, so |
| 1137 | // there is deliberately no libGL link here. | 1145 | // there is deliberately no libGL link here. |
| 1138 | const native_mod = mods[comptime idxOf("native")]; | 1146 | const native_mod = mods[comptime idxOf("native")]; |
| @@ -1146,6 +1154,15 @@ pub fn build(b: *std.Build) void { | |||
| 1146 | const native_step = b.step("native", "Build muxg (opt-in; needs SDL3, freetype, fontconfig, HarfBuzz)"); | 1154 | const native_step = b.step("native", "Build muxg (opt-in; needs SDL3, freetype, fontconfig, HarfBuzz)"); |
| 1147 | native_step.dependOn(&install_muxg.step); | 1155 | native_step.dependOn(&install_muxg.step); |
| 1148 | 1156 | ||
| 1157 | // Policy tests have their own artifact: running them does not compile | ||
| 1158 | // the frame adapter or resolve any window/font system libraries. | ||
| 1159 | const native_core_tests = b.addTest(.{ .root_module = mods[comptime idxOf("native_core")] }); | ||
| 1160 | linkerFor(native_core_tests); | ||
| 1161 | linkQuic(b, native_core_tests, quic); | ||
| 1162 | const run_native_core_tests = b.addRunArtifact(native_core_tests); | ||
| 1163 | const native_core_test_step = b.step("native-core-test", "Run native workspace and interaction tests without window libraries"); | ||
| 1164 | native_core_test_step.dependOn(&run_native_core_tests.step); | ||
| 1165 | |||
| 1149 | // This compiles the same native component and links the same libraries, | 1166 | // This compiles the same native component and links the same libraries, |
| 1150 | // but its tests do not initialize video or open a window. | 1167 | // but its tests do not initialize video or open a window. |
| 1151 | const native_tests = b.addTest(.{ .root_module = native_mod }); | 1168 | const native_tests = b.addTest(.{ .root_module = native_mod }); |
| @@ -1154,6 +1171,7 @@ pub fn build(b: *std.Build) void { | |||
| 1154 | native_tests.linkSystemLibrary2(lib, .{ .use_pkg_config = .force }); | 1171 | native_tests.linkSystemLibrary2(lib, .{ .use_pkg_config = .force }); |
| 1155 | linkQuic(b, native_tests, quic); | 1172 | linkQuic(b, native_tests, quic); |
| 1156 | const native_test_step = b.step("native-test", "Run the native client's unit tests (opt-in)"); | 1173 | const native_test_step = b.step("native-test", "Run the native client's unit tests (opt-in)"); |
| 1174 | native_test_step.dependOn(&run_native_core_tests.step); | ||
| 1157 | native_test_step.dependOn(&b.addRunArtifact(native_tests).step); | 1175 | native_test_step.dependOn(&b.addRunArtifact(native_tests).step); |
| 1158 | 1176 | ||
| 1159 | // The runner is added with the full event path; declaring it here keeps | 1177 | // The runner is added with the full event path; declaring it here keeps |
docs/component-ownership.md
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,82 @@ | |||
| 1 | # Component boundaries and proposed ownership | ||
| 2 | |||
| 3 | The repository already separates the daemon from the GUI in its production | ||
| 4 | module graph. The component-separation sprint adds independent test targets and | ||
| 5 | separates native workspace policy from the window/painter module. These are | ||
| 6 | working boundaries; the roles below are a proposal for assigning people or | ||
| 7 | agents. No named reviewers or CODEOWNERS enforcement have been assigned. | ||
| 8 | |||
| 9 | ## Start with three ownership areas | ||
| 10 | |||
| 11 | | Area | Primary responsibility and files | Focused development gate | | ||
| 12 | | --- | --- | --- | | ||
| 13 | | Daemon | Session lifecycle, authoritative terminal engine, PTYs, scheduling, server transport and platform operations: `src/server/`, `src/engine/engine.zig`, `src/engine/delta.zig`, `src/os/server_os*` | `make daemon-test` | | ||
| 14 | | Native GUI | Pane/workspace policy, persistence, input, rendering and window lifecycle: `src/gui/`, `src/cli/muxg.zig` | `make native-core-test` for policy; `make native` for the complete GUI | | ||
| 15 | | Shared wire and client services | Frame/schema compatibility and the reusable connection, discovery and session interfaces: the `term` module (`src/engine/term.zig`, `protocol.zig`, `grid.zig`, `replica.zig`), `src/client/` shared files, `src/dial.zig`, `src/link.zig`, `src/quic.zig` and client OS support | `make client-test` for client changes; `make check` for the complete shared unit coverage | | ||
| 16 | |||
| 17 | The daemon mode entrypoint, `src/cli/main.zig`, belongs with daemon behavior but | ||
| 18 | is compiled by the shared `mux` dispatcher. Changes there require the complete | ||
| 19 | CLI checks. Likewise `make daemon-test` exercises daemon tests, not every test | ||
| 20 | in the engine or transport dependencies it compiles. Focused gates accelerate | ||
| 21 | development; they do not replace dependency and integration validation. | ||
| 22 | |||
| 23 | Terminal UI (`src/tui/`, `src/cli/mux_main.zig`) and browser UI (`web/`, | ||
| 24 | `src/client/webhub.zig`, `src/cli/webhub_main.zig`, `src/client/wasm_core.zig`) | ||
| 25 | remain separate frontend responsibilities. In particular, a blanket ownership | ||
| 26 | rule for `src/client/` would incorrectly include the browser frontend. Assign | ||
| 27 | exceptions explicitly when named CODEOWNERS entries are introduced. | ||
| 28 | |||
| 29 | Build configuration, the CLI dispatcher, release tooling and integration tests | ||
| 30 | need a maintainer who coordinates changes crossing these areas. They should not | ||
| 31 | be edited independently by several implementation agents in the same checkout. | ||
| 32 | |||
| 33 | ## The native GUI has two internal components | ||
| 34 | |||
| 35 | `native_core` owns `workspace.zig`, `runtime.zig`, `picker.zig`, `persistence.zig` | ||
| 36 | and `interaction.zig`, exported by `src/gui/native_core.zig`. It imports only | ||
| 37 | the shared `client` and `term` modules. It owns native pane identity, attachment | ||
| 38 | lifetimes, authored layout and interaction policy, and receives measured geometry | ||
| 39 | and translated input through its public interfaces. | ||
| 40 | |||
| 41 | `native` owns `frame.zig` and the painter/font/atlas files. It consumes | ||
| 42 | `native_core` through a declared module import; it cannot take a second ownership | ||
| 43 | claim on the core files through relative imports. SDL stays in `frame.zig`. | ||
| 44 | `native-test` aggregates the independent core suite and the window/painter suite. | ||
| 45 | |||
| 46 | Keep these under one GUI owner initially. Assigning separate policy and rendering | ||
| 47 | owners becomes useful when there is enough parallel work to justify it; their | ||
| 48 | contract already exists without another file reorganization. | ||
| 49 | |||
| 50 | ## Changes that need coordination | ||
| 51 | |||
| 52 | A daemon internal change can be developed and tested without compiling the GUI. | ||
| 53 | A GUI change can be developed without compiling the daemon. Each owner should | ||
| 54 | use an isolated worktree so unfinished shared build-file edits do not interfere | ||
| 55 | with another owner's work. Repository-wide source rules still apply to every | ||
| 56 | build in a checkout. | ||
| 57 | |||
| 58 | A wire-format or lifecycle-semantics change is a shared contract change. Its | ||
| 59 | author should identify affected clients, preserve old-peer behavior where | ||
| 60 | required, and run relevant cross-version and real-boundary tests. GUI recovery, | ||
| 61 | join-only restoration, End/Detach semantics, and remote attachment identity are | ||
| 62 | examples where coordination matters even if only daemon code changes. | ||
| 63 | |||
| 64 | Keep module internals private to their component and add reusable operations to | ||
| 65 | the existing shared client/wire interfaces. Native layout policy stays separate | ||
| 66 | from terminal layout policy. Domain owners can decide internal implementation; | ||
| 67 | changing a shared contract needs the affected owners' review. | ||
| 68 | |||
| 69 | ## Evidence behind the split | ||
| 70 | |||
| 71 | The [component-separation delivery record](superpowers/plans/2026-09-05-component-separation.md) | ||
| 72 | retains independent compilation probes in a disposable source snapshot: | ||
| 73 | |||
| 74 | - A deliberate GUI compilation failure leaves daemon and native-core tests usable. | ||
| 75 | - A deliberate daemon compilation failure leaves the GUI build/tests usable. | ||
| 76 | - Native-core tests pass with window-library package metadata unavailable, while | ||
| 77 | a control GUI build correctly fails. | ||
| 78 | |||
| 79 | The matching negative controls prove the injected failures were active. The | ||
| 80 | source snapshot is restored after probing; the working checkout is never poisoned. | ||
| 81 | The full delivery gates remain `make ci` plus the relevant native integration, | ||
| 82 | scale, recovery and responsiveness checks. | ||
docs/native-sprint-workflow.md
| Old | New | ||
|---|---|---|---|
| @@ -98,13 +98,22 @@ packages. Keep one sprint active, ending in a working demo and explicit acceptan | |||
| 98 | export ZIG_GLOBAL_CACHE_DIR=/tmp/muxg-zig-cache | 98 | export ZIG_GLOBAL_CACHE_DIR=/tmp/muxg-zig-cache |
| 99 | make check | 99 | make check |
| 100 | make ci | 100 | make ci |
| 101 | deps/zig/zig build native-core-test -Doptimize=ReleaseSafe | ||
| 101 | deps/zig/zig build native native-test -Doptimize=ReleaseSafe | 102 | deps/zig/zig build native native-test -Doptimize=ReleaseSafe |
| 102 | make native-e2e | 103 | make native-e2e |
| 103 | make native-stress # Linux raw-output responsiveness | 104 | make native-stress # Linux raw-output responsiveness |
| 104 | ``` | 105 | ``` |
| 105 | 106 | ||
| 106 | `make ci` excludes native tests. Use the pinned compiler. A sandbox denial is | 107 | `make ci` excludes native tests. Use the pinned compiler. |
| 107 | not a product failure: run required local socket/PTY checks with the appropriate | 108 | |
| 109 | The focused `make native-core-test` gate compiles workspace, runtime, picker, | ||
| 110 | persistence and interaction without window/font libraries. `native-test` | ||
| 111 | includes that same test artifact plus the frame/painter suite. Daemon and | ||
| 112 | shared client work can use `make daemon-test` and `make client-test`, which | ||
| 113 | reuse their artifacts from the aggregate unit gate. These focused commands | ||
| 114 | complement the full delivery checks above. | ||
| 115 | |||
| 116 | A sandbox denial is not a product failure: run required local socket/PTY checks with the appropriate | ||
| 108 | authorized permissions. Rerun affected gates after meaningful changes; do not | 117 | authorized permissions. Rerun affected gates after meaningful changes; do not |
| 109 | repeatedly run broad suites without a new reason. | 118 | repeatedly run broad suites without a new reason. |
| 110 | 9. **Record and share the actual demo.** Use Rig with `MUXG_VIDEODRIVER=wayland` | 119 | 9. **Record and share the actual demo.** Use Rig with `MUXG_VIDEODRIVER=wayland` |
docs/superpowers/plans/2026-09-05-component-separation.md
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,132 @@ | |||
| 1 | # Component separation and independent development | ||
| 2 | |||
| 3 | The user authorized another separation sprint after interaction ownership commit | ||
| 4 | `efc99d5`, followed by a clearer division of ownership so daemon and GUI work can | ||
| 5 | proceed independently. Prior recorded-demo acceptance remains pending; this is | ||
| 6 | explicit advancement authorization, not retrospective demo approval. | ||
| 7 | |||
| 8 | Status: implemented, reviewed and validated. All final delivery gates passed; | ||
| 9 | the retained recording awaits user acceptance. Ownership roles are proposed; | ||
| 10 | no people are assigned. | ||
| 11 | |||
| 12 | ## Scope and contract | ||
| 13 | |||
| 14 | Opening inspection found the daemon and GUI already independent in the production | ||
| 15 | module graph. The shared client facade serves actual common transport/discovery | ||
| 16 | needs; splitting it would require a larger change without evidence of benefit. | ||
| 17 | No separate preparatory cleanup was needed. | ||
| 18 | |||
| 19 | The smallest useful change is independent component testing and an independently | ||
| 20 | owned native policy module: | ||
| 21 | |||
| 22 | | Module | Owns | Declared production imports | | ||
| 23 | | --- | --- | --- | | ||
| 24 | | `native_core` | workspace, runtime, picker, persistence, interaction | client, term | | ||
| 25 | | `native` | frame adapter, font/atlas/painter, frame timing | native_core, client, term | | ||
| 26 | | `daemon` | existing server core and children | Unchanged | | ||
| 27 | |||
| 28 | `src/gui/native_core.zig` is the only module root that reaches its five child | ||
| 29 | files by relative import. The frame adapter consumes them through the declared | ||
| 30 | `native_core` import. Compatibility aliases in `native.zig` preserve its public | ||
| 31 | API without creating a second source owner. Native layout, runtime, shared | ||
| 32 | client behavior and all controls remain unchanged. | ||
| 33 | |||
| 34 | `native-core-test` compiles its own test artifact with shared QUIC support and | ||
| 35 | no window/font libraries. `native-test` runs that same artifact plus the | ||
| 36 | frame/painter tests. `daemon-test` and `client-test` reuse their existing run | ||
| 37 | nodes from aggregate `test`; there is no second test implementation or duplicate | ||
| 38 | artifact. All native targets remain opt-in. | ||
| 39 | |||
| 40 | Root owned the build graph, Makefile, independent validation and documentation. | ||
| 41 | Luna implemented and wired the core root and GUI imports. Terra independently | ||
| 42 | reviewed both changes and the ownership/test contracts. Fresh short contexts | ||
| 43 | were used, and recorded `turn_context.model` metadata confirmed Luna and Terra | ||
| 44 | immediately at startup. Closing cleanup corrected a forbidden window-library | ||
| 45 | name in the new root's comment, removed a redundant test alias and updated the | ||
| 46 | build comments. No production behavior was changed during cleanup. | ||
| 47 | |||
| 48 | ## Acceptance and evidence | ||
| 49 | |||
| 50 | The final integrated ReleaseSafe build passed 47 native tests: 20 in the | ||
| 51 | independent core and 27 in the frame/painter suite. The new root contributes one | ||
| 52 | registration test; existing test coverage is retained. | ||
| 53 | |||
| 54 | Independent compilation was tested in a disposable snapshot containing the final | ||
| 55 | module/build changes. Each deliberate failure has a matching negative control: | ||
| 56 | |||
| 57 | | Snapshot condition | Expected independent result | Negative control | | ||
| 58 | | --- | --- | --- | | ||
| 59 | | Frame contains a deliberate compile error | `daemon-test native-core-test` pass | `native-test` fails with the planted GUI diagnostic | | ||
| 60 | | Daemon contains a deliberate compile error | `native native-test` pass | `daemon-test` fails with the planted daemon diagnostic | | ||
| 61 | | Window-library pkg-config search paths are empty | `native-core-test` passes | `native` fails to resolve its required packages | | ||
| 62 | |||
| 63 | All six checks passed. The snapshot files were restored in `finally`; the shared | ||
| 64 | checkout was never modified by the probes. This proves compile/test isolation, | ||
| 65 | not that shared wire changes can be made without coordinating their semantics. | ||
| 66 | Repository-wide source rules and shared build-file validity still apply. | ||
| 67 | |||
| 68 | Final gates use `ZIG_GLOBAL_CACHE_DIR=/tmp/muxg-zig-cache`, the pinned compiler, | ||
| 69 | and authorized local socket/PTY access: | ||
| 70 | |||
| 71 | | Gate | Result | | ||
| 72 | | --- | --- | | ||
| 73 | | `deps/zig/zig build install native native-test -Doptimize=ReleaseSafe -p dist/native-component-release --summary all` | Passed; both release binaries from one graph, 47 tests | | ||
| 74 | | Independent compilation probes above | Passed | | ||
| 75 | | `make native-e2e` | Passed; 62 scenarios | | ||
| 76 | | Prior-daemon compatibility | Passed separately; 63 total native integration scenarios | | ||
| 77 | | Real OpenSSH/Wayland recovery | All five groups passed at 200% | | ||
| 78 | | Wayland scale and raw-output stress | Scale passed; stress passed on normal-logging repeat, earlier misses retained | | ||
| 79 | | `make check` / `make ci` | Passed on the final fixture repair | | ||
| 80 | |||
| 81 | The full CI attempt exposed the recurring terminal picker fixture race from the | ||
| 82 | prior sprint. Its script pressed Enter and immediately selected row 2 while the | ||
| 83 | host poll could still contain only session 0. That selection was clamped; waiting | ||
| 84 | for silence afterward could not repair it. The repaired script waits for the | ||
| 85 | actual unselected born-session row (`2 1`) before pressing 2, retaining the | ||
| 86 | selected-row and holder-count assertions and all original timeouts. Terra | ||
| 87 | rejected an initial wait on session 0 because it would accept the stale list. | ||
| 88 | The focused 15-scenario host group and the subsequent independent full CI run | ||
| 89 | both passed. | ||
| 90 | No terminal production code changed. | ||
| 91 | |||
| 92 | The first Wayland stress run narrowly missed the 20 ms frame p99 budget at | ||
| 93 | 20.176 ms. The previously validated interaction release also missed it on that | ||
| 94 | compositor (20.567 ms before reopen, 20.270 ms afterward). After restarting the | ||
| 95 | owned compositor with normal logging, preserving its geometry, scale and GLES2 | ||
| 96 | renderer settings, the final release passed unchanged budgets: 18.992 ms before | ||
| 97 | reopen and 18.246 ms afterward. This does not establish the exact cause of the | ||
| 98 | variation. Debug startup identified NVIDIA GeForce RTX 3080; normal-logging | ||
| 99 | startup did not repeat the device identification. Keep that provenance limit | ||
| 100 | with the measurement rather than inferring a performance improvement. | ||
| 101 | |||
| 102 | The successful 30.1-second stress run read 104.6 MB. Sampled input-to-painted | ||
| 103 | maxima were 55.5 ms on the shared daemon and 55.4 ms on the separate daemon, | ||
| 104 | 94 samples each. The 5 ms polling interval and observation overhead make these | ||
| 105 | upper bounds. Runs used an isolated compositor without concurrent GUI fixtures | ||
| 106 | or recording, and no budget was relaxed. No new macOS result is claimed. | ||
| 107 | |||
| 108 | A 56.8-second actual GUI recording uses the final release at 200%, H.264 | ||
| 109 | 1100×700 with no audio. It shows the unchanged menu, resize, delayed End, | ||
| 110 | restoration and replacement behavior through three continuous clips with two | ||
| 111 | restart cuts. Input is through ordinary SDL events; this is not a new | ||
| 112 | compositor mouse-path trial. The fixtures and owned compositor were stopped. | ||
| 113 | The recording is retained at: | ||
| 114 | `/home/xanderle/.codex/visualizations/2026/09/05/01a071c2-5767-7790-a335-1a84536c9ca5/native-component-trial.mp4`. | ||
| 115 | |||
| 116 | Logs, exact probe commands, both stress failures and the successful repeat, | ||
| 117 | source/release hashes, recording driver and model metadata are in the sibling | ||
| 118 | `component-evidence/` directory. User recording acceptance is separate from all | ||
| 119 | automated checks. No new Tailscale route or payload was published. | ||
| 120 | |||
| 121 | ## Ownership follow-up | ||
| 122 | |||
| 123 | The [ownership proposal](../../component-ownership.md) starts with daemon, native | ||
| 124 | GUI, and shared wire/client services. GUI policy and rendering now have a usable | ||
| 125 | internal boundary, but assigning them separate people is optional. Terminal and | ||
| 126 | browser frontends retain their own responsibilities. Shared build/release and | ||
| 127 | contract changes require coordination; ordinary internal work uses the focused | ||
| 128 | component gates and separate worktrees. | ||
| 129 | |||
| 130 | No directory reshuffle, repository split, named CODEOWNERS assignment or shared | ||
| 131 | wire redesign is included. Future assignments should follow these tested module | ||
| 132 | boundaries and account for mixed folders such as `src/client/` and `src/engine/`. | ||
docs/superpowers/plans/2026-09-05-native-interaction-debt.md
| Old | New | ||
|---|---|---|---|
| @@ -5,8 +5,9 @@ remains accepted; the SSH recording's acceptance remains pending. Advancing to | |||
| 5 | this sprint does not imply acceptance of that recording. | 5 | this sprint does not imply acceptance of that recording. |
| 6 | 6 | ||
| 7 | Status: implementation, review and all required validation complete. | 7 | Status: implementation, review and all required validation complete. |
| 8 | Recorded-demo acceptance is pending. The user authorized another separation | 8 | Recorded-demo acceptance is pending. The user authorized the |
| 9 | sprint before this recording was accepted; that authorization is not demo approval. | 9 | [component-separation sprint](2026-09-05-component-separation.md) before this |
| 10 | recording was accepted; that authorization is not demo approval. | ||
| 10 | 11 | ||
| 11 | ## Scope and acceptance | 12 | ## Scope and acceptance |
| 12 | 13 | ||
src/gui/frame.zig
| Old | New | ||
|---|---|---|---|
| @@ -3,11 +3,12 @@ const std = @import("std"); | |||
| 3 | const client = @import("client"); | 3 | const client = @import("client"); |
| 4 | const term = @import("term"); | 4 | const term = @import("term"); |
| 5 | const keymap = client.keymap; | 5 | const keymap = client.keymap; |
| 6 | const model = @import("workspace.zig"); | 6 | const native_core = @import("native_core"); |
| 7 | const runtime = @import("runtime.zig"); | 7 | const model = native_core.workspace; |
| 8 | const picker_mod = @import("picker.zig"); | 8 | const runtime = native_core.runtime; |
| 9 | const persistence = @import("persistence.zig"); | 9 | const picker_mod = native_core.picker; |
| 10 | const interaction = @import("interaction.zig"); | 10 | const persistence = native_core.persistence; |
| 11 | const interaction = native_core.interaction; | ||
| 11 | const font = @import("font.zig"); | 12 | const font = @import("font.zig"); |
| 12 | const atlas = @import("atlas.zig"); | 13 | const atlas = @import("atlas.zig"); |
| 13 | const quads = @import("quads.zig"); | 14 | const quads = @import("quads.zig"); |
src/gui/native.zig
| Old | New | ||
|---|---|---|---|
| @@ -5,23 +5,28 @@ | |||
| 5 | //! terminal wall and confine the window API to frame.zig. The | 5 | //! terminal wall and confine the window API to frame.zig. The |
| 6 | //! remaining painter code stays usable in unit tests that open no window. | 6 | //! remaining painter code stays usable in unit tests that open no window. |
| 7 | //! | 7 | //! |
| 8 | //! Imports `client` and `term` and nothing else of ours. | 8 | //! The window-free model is the separate `native_core` component; this module |
| 9 | //! owns the window and painter files. | ||
| 9 | const std = @import("std"); | 10 | const std = @import("std"); |
| 10 | const client = @import("client"); | 11 | const client = @import("client"); |
| 11 | const term = @import("term"); | 12 | const term = @import("term"); |
| 12 | 13 | ||
| 13 | pub const interaction = @import("interaction.zig"); | 14 | pub const core = @import("native_core"); |
| 14 | pub const frame = @import("frame.zig"); | 15 | pub const frame = @import("frame.zig"); |
| 15 | pub const workspace = @import("workspace.zig"); | ||
| 16 | pub const runtime = @import("runtime.zig"); | ||
| 17 | pub const picker = @import("picker.zig"); | ||
| 18 | pub const persistence = @import("persistence.zig"); | ||
| 19 | pub const bench = @import("bench.zig"); | 16 | pub const bench = @import("bench.zig"); |
| 20 | pub const atlas = @import("atlas.zig"); | 17 | pub const atlas = @import("atlas.zig"); |
| 21 | pub const font = @import("font.zig"); | 18 | pub const font = @import("font.zig"); |
| 22 | pub const quads = @import("quads.zig"); | 19 | pub const quads = @import("quads.zig"); |
| 23 | pub const gl = @import("gl.zig"); | 20 | pub const gl = @import("gl.zig"); |
| 24 | 21 | ||
| 22 | // Compatibility aliases for callers that previously reached these through | ||
| 23 | // `native`; the source ownership remains with native_core. | ||
| 24 | pub const interaction = core.interaction; | ||
| 25 | pub const workspace = core.workspace; | ||
| 26 | pub const runtime = core.runtime; | ||
| 27 | pub const picker = core.picker; | ||
| 28 | pub const persistence = core.persistence; | ||
| 29 | |||
| 25 | pub fn run(alloc: std.mem.Allocator, opts: frame.Options) !u8 { | 30 | pub fn run(alloc: std.mem.Allocator, opts: frame.Options) !u8 { |
| 26 | return frame.run(alloc, opts); | 31 | return frame.run(alloc, opts); |
| 27 | } | 32 | } |
| @@ -29,12 +34,8 @@ pub fn run(alloc: std.mem.Allocator, opts: frame.Options) !u8 { | |||
| 29 | test { | 34 | test { |
| 30 | _ = client; | 35 | _ = client; |
| 31 | _ = term; | 36 | _ = term; |
| 32 | _ = interaction; | 37 | _ = core; |
| 33 | _ = frame; | 38 | _ = frame; |
| 34 | _ = workspace; | ||
| 35 | _ = runtime; | ||
| 36 | _ = picker; | ||
| 37 | _ = persistence; | ||
| 38 | _ = bench; | 39 | _ = bench; |
| 39 | _ = atlas; | 40 | _ = atlas; |
| 40 | _ = font; | 41 | _ = font; |
src/gui/native_core.zig
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,21 @@ | |||
| 1 | //! Window-free native GUI model, transport runtime, picker, persistence, and | ||
| 2 | //! interaction policy. The native window and painter consume this component; | ||
| 3 | //! keeping it as its own module lets its tests run without window libraries. | ||
| 4 | const client = @import("client"); | ||
| 5 | const term = @import("term"); | ||
| 6 | |||
| 7 | pub const workspace = @import("workspace.zig"); | ||
| 8 | pub const runtime = @import("runtime.zig"); | ||
| 9 | pub const picker = @import("picker.zig"); | ||
| 10 | pub const persistence = @import("persistence.zig"); | ||
| 11 | pub const interaction = @import("interaction.zig"); | ||
| 12 | |||
| 13 | test { | ||
| 14 | _ = client; | ||
| 15 | _ = term; | ||
| 16 | _ = workspace; | ||
| 17 | _ = runtime; | ||
| 18 | _ = picker; | ||
| 19 | _ = persistence; | ||
| 20 | _ = interaction; | ||
| 21 | } | ||
test/e2e_09_hosts.sh
| Old | New | ||
|---|---|---|---|
| @@ -1085,6 +1085,11 @@ expect x forget 15000 | |||
| 1085 | send 2 | 1085 | send 2 |
| 1086 | settle 400 15000 | 1086 | settle 400 15000 |
| 1087 | send \r | 1087 | send \r |
| 1088 | # Enter changes the prefix filter to the session level before the poller's | ||
| 1089 | # post-birth reply necessarily arrives. Wait for the unselected row for the | ||
| 1090 | # newly born session 1 as the readiness event, so the following numeric | ||
| 1091 | # selection cannot race the stale list containing only session 0. | ||
| 1092 | expect 2 1 15000 | ||
| 1088 | # The BORN session's row, named. Daemon 2's list is 0 then the session c | 1093 | # The BORN session's row, named. Daemon 2's list is 0 then the session c |
| 1089 | # just made, so row 2 is the new one, and a bare "on this wall" would be | 1094 | # just made, so row 2 is the new one, and a bare "on this wall" would be |
| 1090 | # satisfied by row 1 -- the pane the layout seeded -- whatever the birth | 1095 | # satisfied by row 1 -- the pane the layout seeded -- whatever the birth |