From 821835b8c5761cbfb4df4a73c3e40ff5c340bae8 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Thu, 23 Jul 2026 09:51:22 -0400 Subject: [PATCH] Frame one-command GPU invocation broker Record the approved managed-launch contract, daemon lifecycle and process-group rules, strict GPU CLI behavior, headless acceptance seam, and the complete acceptance matrix before implementation. --- docs/gpu-invocation-framing.md | 613 +++++++++++++++++++++++++++++++++ 1 file changed, 613 insertions(+) create mode 100644 docs/gpu-invocation-framing.md diff --git a/docs/gpu-invocation-framing.md b/docs/gpu-invocation-framing.md new file mode 100644 index 0000000..425369e --- /dev/null +++ b/docs/gpu-invocation-framing.md @@ -0,0 +1,613 @@ +# GPU invocation — one-command broker framing + +**Revision 3 — pre-implementation. Ground truth: canonical `main` @ +`96d0bae`, protocol v19, 2026-07-23.** + +The GPU editor works, but reaching it is still a development-session ritual: +build two packages with different feature requirements, keep a foreground +daemon alive in one terminal, reconstruct its resolved Unix-socket path, and +pass that raw path to a second binary in another terminal. This framing makes +the normal local GPU path one explicit command: + +```sh +pmacs --gpu +pmacs --gpu --socket research +``` + +This is an additive first stage. It does **not** yet change bare `pmacs` from +TUI to GUI, and it does not pretend that `pmacs --gpu FILE` works before the +daemon has a real per-frontend initial-file contract. It preserves the +separate `pmacs-gpu` binary and its independent dependency graph. + +Revision 2 closes the first review round: the spawned daemon is isolated from +the launcher's foreground process group; the required Vterm headless probe is +retained; retry/error/reaping behavior is complete; a real display-less +managed-attach seam is named; bare `--socket` stops being silently ignored; +and the non-CRDT gate is explicitly justified as default-socket protection. + +Revision 3 makes the managed probe deterministic for signal/reaper tests, +keeps `Interrupted` / `WouldBlock` transient inside the post-spawn retry +window, states the process-group signal simulation in CI-executable terms, +and distinguishes the socket type check from liveness inference. + +## Ground truth + +### Current user path + +The daemon is a mode of the root `pmacs` binary; there is no +`pmacs-daemon` executable (`Cargo.toml`, `src/main.rs`). `pmacs-gpu` is a +separate unpublished workspace package and binary +(`pmacs-gpu/Cargo.toml`). The current source-checkout path is: + +```sh +cargo build --release --workspace --features pmacs/crdt + +# terminal 1 +target/release/pmacs --daemon + +# terminal 2 +runtime="${XDG_RUNTIME_DIR:-/tmp/pmacs-$(id -u)}" +target/release/pmacs-gpu --attach "$runtime/pmacs/default.sock" +``` + +The unified workspace build above succeeds. The README currently documents +two separate build commands instead. Its `cargo run --release -- ` +example is not runnable as written: the root package has no `default-run`, +and Cargo reports that it cannot choose among `pmacs`, `pmacs-audit`, +`pmacs_fake_lsp`, and `pmacs_fake_mcp`. + +A live smoke on this base exposed the cost of independent builds: the first +`target/release/pmacs-gpu` was protocol v15 while the daemon was v19. The GPU +opened but rejected the attach. Rebuilding `pmacs-gpu` produced a successful +v19 attach. The handshake caught the mismatch correctly; the invocation path +made it easy to create. + +### Existing CLI and process boundaries + +- `pmacs [FILE]` runs the in-process TUI. `-nw` / `--no-window` is already + parsed as an explicit TUI choice, though it is currently equivalent to the + default (`src/main.rs:94-131,288-318`). +- The source comment at `src/main.rs:28-49` reserves the future shape: + explicit `-nw` wins, a future `--gui` can select GUI, then an environment / + display-based default may choose GUI. None of that GUI selection is + implemented today. +- `pmacs --daemon [--socket NAME|PATH]` runs in the foreground. Bare names + resolve to `/pmacs/NAME.sock`; omission means `default.sock`; a + value containing `/` is used as a path (`src/socket_path.rs`). +- `` is nonempty `$XDG_RUNTIME_DIR`, otherwise + `/tmp/pmacs-`. The daemon creates a private parent, locks a sibling + lockfile, removes a stale socket only after acquiring the lock, and binds + the socket owner-only (`src/socket_path.rs`, `src/lockfile.rs`, + `src/daemon.rs:437-530`). +- `pmacs-gpu --attach ` takes a raw pathname. It has no default/name + resolver and no file positional (`pmacs-gpu/src/main.rs:548-565,802-848`). +- `pmacs-gpu` has three current modes: bare hello-world, direct `--attach`, + and the test/acceptance seam `--headless-probe `. The + required Vterm Stage 3 acceptance invokes that third mode as a real + subprocess (`tests/vterm_stage3_acceptance.rs:679-691`). +- Bare `pmacs-gpu` still opens the inert Session-2 `hello, pmacs` window. It + is scaffolding, not an editor session. +- The GPU parser consumes the attach/probe operands but does not reject later + argv, so trailing values are silently ignored. +- The GPU package intentionally depends on `pmacs-protocol`, not the root + editor crate. The distribution decision in + `docs/pmacs-gpu-design.md:195-202` keeps wgpu, winit, font, and + window-system dependencies out of TUI-only installs. That boundary still + holds. + +### Capability and attach constraints + +A usable GPU daemon must advertise all of `multi_frontend`, `crdt_replica`, +and `semantic_render`. The root `crdt` feature enables those capabilities; +`pmacs-gpu` itself has no Cargo features. The GPU validates the daemon's +`Hello` before sending `AttachRequest` and produces an actionable capability +mismatch instead of waiting forever (`pmacs-gpu/src/attach.rs`). + +`pmacs-gpu` currently attempts one `UnixStream::connect` from +`ApplicationHandler::resumed`. A connect/handshake failure stays visible in +the window but is not retried. Later disconnect also requires a manual +relaunch. Automatic reconnect is an existing named deferral, separate from +startup (`docs/gpu-attach-robustness-framing.md:175-186`). + +The root already has daemon auto-start precedent in `src/daemon_attach.rs`: +try an existing socket, otherwise spawn `current_exe --daemon --socket PATH`, +wait up to five seconds, then enter its byte bridge. That helper must retain +the successful connection because a disposable connect probe makes the +daemon send `Hello` into a stream the probe drops, producing a broken pipe. +The GPU cannot directly reuse the helper: it lives in the root crate and +returns the stream to the stdio bridge, while `pmacs-gpu` owns its own direct +Unix transport. + +The SSH-side daemon auto-start precedent does not isolate the daemon into a +new process group: `daemon_attach.rs` relies on the SSH spawn having no +controlling terminal (`src/daemon_attach.rs:53-60`). A local `pmacs --gpu` +launcher does have one. Without an explicit process-group split, root, GPU, +and the auto-started daemon inherit the foreground group; terminal Ctrl-C +reaches all three, and the daemon deliberately treats SIGINT as graceful +shutdown (`src/daemon.rs:617-633`). SIGHUP is already ignored, so foreground +SIGINT is the specific lifecycle gap. + +### Initial-file constraint + +Every daemon attachment currently receives a fresh scratch view in +`handle_session_established` (`src/daemon.rs:1533-1552`). The code explicitly +names cloning or taking an initial-buffer argument as future work. Opening a +file in the daemon before attach would not put that file in the new GPU +frontend's view. There is no frontend `OpenPath` event and no initial target +in `AttachRequest` (`pmacs-protocol/src/message.rs:2023-2040`). + +Therefore a launcher that accepts `FILE` without new daemon/session work +would either ignore it, drive the minibuffer by synthetic keys, or open it in +the wrong view. All three are rejected. + +### Distribution gaps + +There is no editor installation recipe, desktop entry, user service, +Make/Just target, Cargo alias, or launcher wrapper. The only repository +script is the test-development helper `scripts/bite`. Cargo does not install +repository shell scripts, and `pmacs-gpu` has `publish = false`. + +## Decisions + +### Q#GI1 — Add the explicit root command `pmacs --gpu [--socket NAME|PATH]` + +The first-stage public surface is: + +```text +pmacs --gpu [--socket NAME|PATH] +``` + +It is additive. Bare `pmacs` and `pmacs FILE` continue to run the local TUI; +`pmacs -nw` remains the explicit TUI spelling. `--gpu` is mutually exclusive +with `-nw` / `--no-window`, `--daemon`, `--attach`, and `--daemon-attach`. +It rejects every positional argument with: + +```text +pmacs: --gpu does not yet accept FILE; open it from the GPU with C-x C-f +``` + +The parser also closes the adjacent existing hole: `--socket` without one of +`--daemon`, `--attach`, `--daemon-attach`, or `--gpu` exits 2 instead of being +silently discarded by `Mode::Local`. A flag is used rather than a `pmacs gpu` +subcommand because `gpu` is a valid existing positional filename. No +`PMACS_FRONTEND` environment selection and no display auto-detection land in +this stage. + +Rejected alternatives: + +- **Bare `pmacs` becomes GUI immediately** — this mixes launcher correctness, + daemon lifecycle, initial-file semantics, and a default-behavior change in + one cut. `-nw` reserves that eventual migration; it does not make an + incomplete migration safe. +- **A shell wrapper** — not installed by Cargo, duplicates readiness and path + policy, and makes version-skewed binaries easier to combine. +- **Link GPU into the root binary** — violates the deliberate independent + dependency graphs and adds wgpu/window dependencies to TUI-only builds. + +### Q#GI2 — Root owns policy and paths; `pmacs-gpu` owns the successful connection + +The root launcher owns: + +1. CLI validation; +2. the canonical `resolve_socket_path` call; +3. the CRDT-build gate; +4. discovery of the separate `pmacs-gpu` executable; +5. waiting for that executable and reflecting its outcome. + +The GPU child owns the actual connection that becomes the session. For the +managed launch, root passes two hidden/internal arguments: the resolved raw +socket path and `current_exe()` as the daemon executable. The child first +tries the socket itself; the stream that completes `Hello` / `AttachRequest` +is retained as its real `AttachClient`. There is no disposable readiness +connection and therefore no deliberate broken-pipe noise. + +The hidden argument shape is not a second user-facing launcher. Direct users +keep `pmacs-gpu --attach PATH`; the documented managed surface is +`pmacs --gpu`. + +### Q#GI3 — Managed GPU attach may start the supplied daemon, then retries boundedly + +Managed attach follows this state machine before creating the window: + +1. Try the resolved Unix socket once. +2. If connection reaches `Hello`, perform the normal version and capability + validation. Either failure is final and surfaced; never start a + replacement daemon over a live incompatible instance. +3. `NotFound` authorizes daemon startup. `ConnectionRefused` authorizes it + only when `metadata(socket)` says the existing entry is a Unix socket, or + the entry disappeared in the race between connect and metadata. An + existing non-socket file is a final error and is never handed to + `pmacs --daemon`, whose established stale-path transaction would otherwise + unlink it after acquiring the sibling lock. +4. Every other initial connect error (`PermissionDenied`, invalid path shape, + resource exhaustion, `Interrupted`, `WouldBlock`, and other errno classes) + is final, surfaced with the socket path, and invokes no daemon spawner. + During the post-spawn retry window, `NotFound`, authorized + `ConnectionRefused`, `Interrupted`, and `WouldBlock` / `EAGAIN` continue to + the deadline; all other errors remain final. This tolerates a signal- + interrupted `connect(2)` and the just-bound daemon's temporarily full + AF_UNIX accept backlog without broadening what may trigger daemon startup. +5. Spawn the supplied executable as + `pmacs --daemon --socket `, with the process-group isolation + in Q#GI13. +6. Retry the real GPU connect every 50 ms for up to five seconds, matching + `AUTO_START_POLL_INTERVAL` / `AUTO_START_TIMEOUT` in + `daemon_attach.rs:149-160`. The first successful stream becomes the real + attach; no probe is dropped. +7. If the spawned child exits during startup, reap it, retain its status, and + keep retrying until the deadline: another concurrent launcher may have won + the socket lock and be about to listen. +8. At the deadline, exit nonzero with the socket path, timeout, and spawned + child status when available. + +Connection/retry occurs before `run_app`, so the winit event thread never +freezes for five seconds behind startup polling. On success, events sent to +the already-created `EventLoopProxy` may queue until `run_app` starts; the +window then assembles with the connected frontend id. Existing direct +`--attach` behavior may retain its in-window failure banner. + +During startup the managed connector owns `Option` so every early exit +is reaped. After a successful attach, a still-running spawned daemon moves to +a named reaper thread whose only job is blocking `Child::wait`; this prevents +a daemon that later crashes or quits from remaining a zombie throughout a +long GPU session. Closing the GPU process still leaves a live daemon orphaned +and long-lived per Q#GI5. + +Do not extract a generic launcher crate for two constants, a short loop, and +one child reaper. + +### Q#GI4 — Auto-start is race-safe through the existing daemon lock + +Two simultaneous `pmacs --gpu` commands may both observe a missing socket and +spawn a daemon. The sibling lockfile is the arbiter: one daemon binds, the +other exits. Both GPU children continue their bounded connect loop and attach +to the winner. The loser child is reaped; its lock error is not treated as a +launch failure if the socket becomes usable. + +A stale socket follows the existing daemon transaction: acquire the lock, +unlink the stale socket, bind the replacement. The launcher may read +filesystem metadata solely to distinguish a socket entry from non-socket data +(Q#GI3); it neither deletes entries nor treats path existence as proof of +liveness. The daemon lock and successful protocol connection remain the +arbiters. + +### Q#GI5 — An auto-started daemon remains long-lived + +Closing the GPU window detaches that frontend but does not kill a daemon the +launcher started. This matches pmacs's long-lived-instance architecture and +the existing `--daemon-attach` auto-start behavior. A later `pmacs --gpu` +reuses the same default/named daemon and retains buffers, processes, and +language services. + +The startup child has null stdin/stdout/stderr, matching the existing +background auto-start precedent. Startup failure is reported through child +status + timeout; detailed daemon diagnostics remain available by running +`pmacs --daemon --socket ...` directly. Q#GI13, not the SSH precedent, +defines the local terminal's signal isolation. The launcher does not +daemonize, install a service, or invent idle shutdown in this stage. + +### Q#GI6 — Require a CRDT-capable root build before launching + +In a root binary compiled without `feature = "crdt"`, `pmacs --gpu` exits +before executable discovery or any socket connection with an actionable +message: + +```text +pmacs: --gpu requires pmacs built with --features crdt +``` + +This prevents default-socket poisoning: without the gate, a non-CRDT root +could auto-start an incapable daemon that successfully owns `default.sock`; +the GPU would then reject its capabilities, and Q#GI3's correct +never-replace-a-live-instance rule would make later managed launches keep +failing until the user manually stopped that daemon. + +The deliberate trade-off is that a non-CRDT root also refuses the managed +fast path when a separate capable daemon is already listening. The broker's +root executable is its daemon-start authority and must be capable for +deterministic behavior. Advanced users may still attach the independent GPU +directly to a known capable daemon with `pmacs-gpu --attach PATH`; that path +retains its own Hello capability validation. + +### Q#GI7 — Prefer the sibling GPU binary, then fall back to `PATH` + +Discovery order: + +1. test-only `PMACS_TEST_GPU_BIN` override; +2. `current_exe().parent()/pmacs-gpu` when that path exists; +3. `pmacs-gpu` through `PATH`. + +Sibling-first keeps ordinary source builds and side-by-side installations on +the same release/protocol build. The handshake remains authoritative: path +co-location is not proof of protocol compatibility. Failure names the sibling +candidate and PATH fallback rather than reporting a generic spawn error. + +There is no production `PMACS_GPU_BIN` configuration knob in v1. A permanent +override would become distribution policy; tests need substitution, users +need an installation that places the two shipped binaries coherently. + +### Q#GI8 — Root waits for the GPU child and reflects failure + +`pmacs --gpu` remains the terminal-visible parent while the window is open. +A successful GPU exit returns success; a nonzero exit returns failure and +prints which GPU executable failed. Spawn failure is immediate and +actionable. No shell, `nohup`, or detached launcher process sits between the +user and the frontend. + +The daemon is independent after startup (Q#GI5); root waits only for the GPU +child. + +### Q#GI9 — Retire bare `pmacs-gpu` hello-world and make argv strict + +The hello-world window was Session-2 dependency scaffolding and no longer +serves a user workflow. Bare `pmacs-gpu` exits with usage that points to +`pmacs --gpu` for managed startup and `pmacs-gpu --attach PATH` for direct +debugging. + +The existing test-only +`pmacs-gpu --headless-probe ` mode is retained byte-for-byte +as the Vterm Stage 3 real daemon + PTY + wgpu seam. Its parser becomes strict +about exactly those two operands; `tests/vterm_stage3_acceptance.rs` keeps its +current subprocess command and semantics. Q#GI14 adds a separate managed +headless probe rather than overloading this vterm contract. + +The GPU parser rejects: + +- trailing argv after direct-attach or probe operands; +- a missing attach/probe operand; +- user attempts to invoke the hidden managed modes without every broker + operand; +- unknown flags. + +Add `pmacs-gpu --version` so reports can name both package version and +protocol version without opening a window. `--help` labels direct attach as +an advanced/manual path and does not advertise internal broker/probe +arguments. + +### Q#GI10 — No protocol change and no initial file in this stage + +The broker changes process orchestration only. It sends the existing +`AttachRequest`, negotiates protocol v19, and uses the existing semantic/CRDT +session. `SUPPORTED` and every wire discriminant remain unchanged. + +`pmacs --gpu FILE` is rejected rather than accepted partially. The future +file feature must target the authenticated source frontend's view, preserve +non-UTF-8 local paths, define relative-path resolution, surface open failure, +and avoid a scratch-buffer flash. It receives its own framing and protocol +review. + +### Q#GI11 — Fix checkout build/run instructions with the broker + +Set root package `default-run = "pmacs"`, making the existing README +`cargo run --release -- ...` family unambiguous. Document one coherent build: + +```sh +cargo build --release --workspace --features pmacs/crdt +``` + +Then document: + +```sh +target/release/pmacs --gpu +# explicit TUI remains: +target/release/pmacs -nw [FILE] +``` + +The build line deliberately compiles both binaries together. Keep the +advanced two-process commands in a troubleshooting/manual-attach subsection, +including the canonical XDG fallback rather than Linux-only `$UID` prose. +Do not claim `cargo install` support until installation is exercised and the +unpublished GPU package has a deliberate distribution story. + +### Q#GI12 — Scope stays on invocation, not reconnect or packaging + +The managed startup retry ends when the first attach succeeds. A later daemon +disconnect retains today's `(daemon disconnected)` state and manual relaunch. +Desktop files, system services, release bundles, package managers, and remote +GPU transports are not smuggled into this feature. + +### Q#GI13 — Put the auto-started daemon in its own process group + +Before spawning the managed daemon, call the safe Unix +`std::os::unix::process::CommandExt::process_group(0)`. The daemon becomes the +leader of a new process group while the root broker and GPU child remain in +the terminal's foreground group. Terminal Ctrl-C therefore terminates the +foreground launcher/frontend without delivering SIGINT to the daemon or any +other attached frontend. + +This is process-group isolation, not a new session or full daemonization. +Direct `pmacs --daemon` remains foreground and keeps its established +SIGINT/SIGTERM graceful-shutdown contract. Terminal close remains safe through +the daemon's existing SIGHUP no-op. No `unsafe`, `setsid` helper process, or +platform-specific FFI enters the codebase. + +### Q#GI14 — Add a real display-less managed-attach acceptance seam + +Extract the pre-`run_app` production path as +`connect_managed_with_sink(socket, daemon_exe, sink)`. The normal managed +window calls it with the existing `EventLoopProxy` sink. A new hidden strict +subprocess mode: + +```text +pmacs-gpu --headless-managed-probe +``` + +calls the same function with a channel sink and keeps processing that channel +after the real `BufferSnapshot`. At the snapshot checkpoint it atomically +writes a complete report with `phase=ready` and initial named facts (protocol, +whether this invocation spawned, child status so far), then holds the live +session until stdin reaches EOF. A dedicated stdin-reader thread reports EOF +to the probe loop; the loop itself remains free to receive disconnects and +reaper observations. Each such lifecycle observation atomically refreshes the +`phase=ready` report, so a harness can wait for a named fact such as +`daemon_reaped=true` without sleeping. On EOF the probe atomically replaces +the report with `phase=complete` plus final child/reaping facts and exits +without winit or wgpu. + +Tests that need a hold spawn the probe with piped stdin, wait for +`phase=ready`, perform the signal/daemon action, wait for any required named +fact, then close stdin when they want normal probe completion. An ordinary +invocation with null stdin advances immediately from ready to complete. There +is no timing-based linger duration. + +This is the acceptance seam for Q#GI3–GI6 and Q#GI13: real binary, real Unix +socket, real Hello/capability negotiation, and real daemon subprocess. A +decoded-message fixture or a second test-only connect implementation is not +accepted evidence. + +The existing `--headless-probe ` remains separate and still +drives real offscreen wgpu for Vterm Stage 3 (Q#GI9). Narrow injected-spawner +unit tests pin rare errno/timeout branches, including post-spawn +`Interrupted` / `WouldBlock` followed by success, but they do not replace the +managed subprocess acceptance. + +## Categorical bets + +1. **An explicit broker is enough to validate lifecycle policy before changing + the default frontend.** Users gain a one-command GPU path without forcing + GUI startup into scripts, `$EDITOR`, SSH sessions, or terminals that rely + on today's bare `pmacs` TUI. +2. **Sibling-first discovery covers source and coherent installed layouts.** + A fallback to PATH handles split prefixes; the protocol handshake catches + stale or foreign binaries. +3. **The existing lock is the correct concurrency arbiter.** Launcher-side + PID files, path-existence checks, or socket deletion would duplicate and + weaken the daemon's established ownership transaction. +4. **The successful connect must be the session connect.** Disposable probes + are observably wrong because the daemon speaks first; retrying the actual + GPU connect avoids false BrokenPipe logs and frontend-id churn. +5. **A five-second pre-window startup bound is acceptable.** Existing remote + daemon auto-start uses the same bound. A missing/broken daemon fails before + creating a misleading inert window. +6. **Persistent auto-start matches user expectation.** The instance owns + buffers and services across frontend lifetime; killing it on window close + would turn the daemon split into implementation overhead with no persistence + benefit. +7. **Foreground job control must not own the daemon.** A local launcher's + terminal process group is not the SSH no-controlling-terminal precedent; + a safe process-group split preserves the daemon across Ctrl-C without full + daemonization. +8. **Rejecting FILE is better than synthetic input.** Driving `C-x C-f` or the + minibuffer from a launcher is timing-dependent, configuration-dependent, + and cannot provide an atomic initial view. +9. **Non-socket paths are data, not stale sockets.** Managed auto-start never + feeds a regular file to the daemon's existing unlink-and-bind transaction. +10. **No new shared crate is warranted.** Root already resolves paths and + hands the result to the GPU; the GPU adds only bounded connect-or-spawn + behavior around its existing transport. + +## Deferred (named) + +- **GUI as the automatic default.** Complete the reserved + `FrontendChoice::Auto` plan after the broker is proven: display detection, + `PMACS_FRONTEND`, and `pmacs -nw` precedence. This is a user-default change, + not part of additive startup. +- **Initial file(s) for an attached frontend.** A real per-session target that + opens/switches in the authenticated source's view, reports errors, handles + path bytes and relative cwd, and avoids scratch flash. This is prerequisite + to honest `pmacs --gpu FILE` and eventual bare `pmacs FILE` GUI startup. +- **Automatic reconnect/resync.** Startup retry does not reconcile an + optimistic replica after a live connection drops; retain the attach- + robustness deferral. +- **Direct `pmacs-gpu --socket NAME`.** Managed users go through root, which + already owns canonical resolution. Revisit only if direct frontend use is a + supported standalone workflow. +- **Remote GPU attach.** The TUI owns SSH/daemon-attach transport today; + generic GPU stream transports need separate latency, clipboard, and + frontend-resource semantics. +- **Daemon service management.** systemd/launchd units, socket activation, + idle shutdown, logs, restart policy, and a `pmacs --stop-daemon` command. +- **Distribution/install bundles.** Cargo install, release archives, desktop + entries, icons, and ensuring both binaries land together. +- **Multiple files and client/server open commands.** Follow the initial-file + contract rather than widening this stage's rejected positional grammar. +- **GPU executable override for users.** Keep only the test override until a + real packaging use case establishes precedence and diagnostics. + +## Acceptance + +CLI and launcher cases run against the real built binaries where process +behavior is the contract. Pure parsing/discovery helpers receive unit tests; +no source-text assertions substitute for subprocess behavior. Managed +connection cases use Q#GI14's real display-less binary seam; the existing +Vterm probe continues to cover offscreen wgpu. + +1. **Root CLI grammar:** `pmacs --gpu` and `pmacs --gpu --socket research` + select managed GPU mode. Combinations with `-nw`, `--daemon`, `--attach`, + `--daemon-attach`, or a positional file exit 2 with the conflicting + argument named. Bare `pmacs --socket research` (with or without a local + file / `-nw`) exits 2 and says which owning mode is required. +2. **Non-CRDT build fails before socket or spawn:** a default-feature + `pmacs --gpu` names `--features crdt`; a capable daemon already listening + does not weaken the gate, and fake GPU/daemon executables record zero + invocations. The default socket remains unowned. +3. **Sibling discovery wins:** with executable fixtures at the current-exe + sibling and on PATH, the sibling receives the managed arguments. Removing + it uses PATH. With neither, the error names both lookup attempts. +4. **Existing daemon fast path:** start a real CRDT daemon on a private socket, + run `--headless-managed-probe`, and assert a v19 session establishes and a + real `BufferSnapshot` arrives without invoking the supplied daemon spawner. +5. **Missing daemon auto-start:** from no socket/lock, the managed probe starts + the supplied real CRDT daemon, completes the real Hello/capability + handshake, receives the first `BufferSnapshot`, and leaves the daemon + connectable after the probe exits. +6. **Ctrl-C does not kill the daemon:** spawn the managed probe/broker in its + own process group, wait for the probe's `phase=ready`, attach a second real + frontend, then simulate terminal Ctrl-C with `kill(-pgid, SIGINT)`. Assert + the launcher/frontend exit while the separately grouped daemon and second + session remain usable. This does not require a controlling terminal or a + foreground-process-group claim in CI. Direct foreground `pmacs --daemon` + still exits cleanly on SIGINT. +7. **Concurrent launchers converge:** start two managed probes together on an + absent named socket. Exactly one daemon owns the lock; both clients + establish sessions; the losing daemon child is reaped rather than becoming + a zombie or aborting its frontend. +8. **Stale socket recovery stays daemon-owned:** leave a stale Unix socket + with no lock owner, launch managed GPU, and assert the daemon replaces it + and the frontend attaches. The launcher itself performs no unlink. +9. **Other connect errors fail closed; retry transients survive:** a + permission-denied initial socket path invokes no daemon spawner and reports + the path/error immediately. A regular file at the socket path survives + unchanged, invokes no daemon, and reports that managed startup refuses to + replace a non-socket entry. An injected post-spawn sequence of + `Interrupted`, `WouldBlock`, then a successful real connection stays + within the deadline and establishes the session. +10. **Live capability mismatch is not replaced:** against a non-CRDT daemon, + managed launch reports the existing capability mismatch, invokes no + second daemon, and leaves the live daemon untouched. +11. **Live protocol mismatch is not replaced:** a real/fake-Hello listener + advertising an unsupported protocol produces the version-mismatch error, + invokes no daemon spawner, and leaves the listener/path untouched. +12. **Bounded startup failure:** substitute a daemon executable that exits + nonzero without binding. Managed GPU exits after five seconds (50 ms + polls) and reports socket + child status. A concurrent-winner fixture + proves an early losing-child exit does not abort while another process + binds before the deadline. +13. **Post-attach daemon exit is reaped:** let the spawned daemon establish a + managed session, wait for the probe's `phase=ready`, then terminate the + daemon while keeping the probe's stdin open. Poll the atomically replaced + ready report until it records both disconnect and named reaper completion, + close stdin, and assert the `phase=complete` report retains the wait + outcome. The daemon never remains a zombie until frontend exit. +14. **Root reflects the GPU outcome:** fake GPU success makes `pmacs --gpu` + succeed; fake nonzero and spawn failure make it fail with the executable + named. +15. **GPU argv is strict without breaking probes:** bare invocation points to + `pmacs --gpu`; direct `--attach PATH`, existing + `--headless-probe SOCKET REPORT`, and hidden + `--headless-managed-probe SOCKET REPORT DAEMON_EXE` accept exactly their + operands. Missing/trailing/unknown/incomplete args exit 2. `--version` + prints package and protocol versions without initializing winit/wgpu. +16. **Existing direct and Vterm paths remain intact:** rebuilt + `pmacs-gpu --attach RAW_PATH` still renders an existing CRDT daemon, and + `tests/vterm_stage3_acceptance.rs` still invokes its unchanged + `--headless-probe` command and passes the real daemon + PTY + wgpu + criterion. +17. **One-command visible smoke:** on a Vulkan/display-capable machine, build + the workspace once, run only `target/release/pmacs --gpu`, observe the GPU + scratch buffer attach at protocol v19, close the window, then invoke the + same command again and observe reuse of the still-running daemon. +18. **Documentation commands are executable:** the README's unified release + build succeeds; `cargo run --release -- --version` selects `pmacs` through + `default-run`; no documented command requires users to spell the resolved + socket pathname for managed GPU startup.