a73x

11423676

docs: agent surface shipped — decisions recorded, deferrals named

a73x   2026-08-13 20:03

Commit message
docs: agent surface shipped — decisions recorded, deferrals named

Twelve tasks landed the agent surface; this records what was decided,
what was deliberately not built, and what the field will meet.

decisions.md gets the eight rulings worth keeping: rows as best-effort
locators rather than seqs; an await answered from `last_return` and never
from live tracker state (the `D`+`A` burst folds before any phase gate
runs — an await asks about a past event, live state describes now);
wrapping the dep's OSC 133 handler instead of patching it; resolution at
the run loop's 100ms tick; session death as an answer on run/await and an
error elsewhere; `--timeout` as the daemon's window plus bounded client
grace; reconnect at-most-once for the wire and never for input; and
`settled` meaning output went quiet, not that anything exited.

roadmap.md carries the same milestone as shipped-with-known-edges: the
zsh `.zshenv` gap, the bash DEBUG-trap displacement and its membership
guard's two limits, orphaned shim directories, the grace overshoot, and
`muxa`'s deliberate lack of an idle knob — each a named follow-up.

The quic.zig comment was stale twice: there are four binaries now, and
`muxa` has no `--quic-idle-ms` on purpose. The plan doc is annotated as
the historical execution record, with its known divergences from the code
listed rather than rewritten.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

README.md
Old New
@@ -102,6 +102,16 @@ muxd stats # wire stats: deltas vs snapshot bytes, attached clients
102 make bench # typing-workload byte-ratio measurement 102 make bench # typing-workload byte-ratio measurement
103 ``` 103 ```
104 104
105 `muxa` is the agent-facing client — verbs `status`, `capture`, `send`,
106 `run`, `await`, one JSON object each, against a local socket or
107 `--quic HOST`. `muxa run "make test"` sends the command line, waits for the
108 shell to return it, and prints the exit code with the output rows; no
109 polling and no sleeps, because the daemon holds the wait. It attaches at
110 0×0 so it never resizes the session a human is using, and every reply names
111 the `mechanism` that answered — an exit code is real only under `marks`
112 (OSC 133 shell integration, injected at spawn), absent under the `pgid` and
113 `settle` fallbacks.
114
105 Multiple clients may attach to one session; the grid follows the most 115 Multiple clients may attach to one session; the grid follows the most
106 recently active client — typing, attaching, or resizing claims it (latest 116 recently active client — typing, attaching, or resizing claims it (latest
107 wins). A session survives logout (this assumes systemd-logind's default 117 wins). A session survives logout (this assumes systemd-logind's default
docs/decisions.md
Old New
@@ -2981,3 +2981,145 @@ must be painted at the origin (`clear` first); EOF on a piped mux
2981 client aborts the QUIC dial mid-handshake — probes must hold stdin 2981 client aborts the QUIC dial mid-handshake — probes must hold stdin
2982 open and detach with 0x1c, or rc=1 "did not answer" lies about the 2982 open and detach with 0x1c, or rc=1 "did not answer" lies about the
2983 network. 2983 network.
2984
2985 ## 2026-08-13 (agent surface — native LLM integration)
2986
2987 Twelve tasks, shipped the same day as M17: OSC 133 command boundaries in
2988 the daemon, three new frame pairs, and `muxa`, a fourth binary that speaks
2989 JSON to an agent's shell tool. An agent now *knows* when a command
2990 returned, with its exit code and its output span, locally or over QUIC —
2991 the thing `tmux send-keys` + `capture-pane` structurally cannot say. The
2992 spec is `docs/superpowers/specs/2026-08-13-agent-surface-design.md`; what
2993 follows is what the implementation decided, which is not always what the
2994 spec guessed.
2995
2996 **Output spans are rows, not seqs — and rows are locators, not anchors.**
2997 The tracker's `seq` is a viewport delta generation: a whole command's
2998 output can share one, and a row loses its seq the moment it scrolls into
2999 history. So a mark records the absolute screen row (`historyRows() +
3000 cursor.y`) at mark time and output recovery is the existing
3001 `fetch_scrollback`, unchanged. The honesty is on the field, not in prose:
3002 `Engine.MarkEvent.row` says that pruning past `max_scrollback` shifts the
3003 origin (a command longer than the scrollback can leave `end_row <
3004 start_row`), that resize reflow renumbers history, and that alt-screen
3005 marks live in a coordinate space where `historyRows()` is 0. Point a human
3006 at output with it; never key durable state on it.
3007
3008 **An await is answered from a persisted snapshot, never from live tracker
3009 state.** Real shell integration emits `D;code` and `A` in one burst, so
3010 both fold in a single pty read and the phase is back to `at_prompt` before
3011 any await ever looks. A gate on `cmd.phase == .returned` therefore loses
3012 deterministically — it was written that way first, and the test that
3013 caught it drives a real shell rather than a hand-built mark sequence.
3014 `last_return` is the fix and the one owner: stamped at the `D`, carrying
3015 the exit code, the span AND the seq that qualifies it, so the answer and
3016 the reason it qualifies are the same record and no later reading of live
3017 state can drift out from under it. It also answers correctly once the
3018 *next* command is already running, which is the general rule worth
3019 keeping: **an await asks about a past event; live state describes now.**
3020
3021 **OSC 133 interception wraps the dependency's handler rather than patching
3022 it.** ghostty-vt already parses semantic prompts including the `err` code,
3023 but its stock handler drops the code and exposes no callback. `vt.Stream(H)`
3024 is generic over the handler, so `MuxHandler` intercepts `.semantic_prompt`
3025 and forwards every other action verbatim: terminal state stays identical,
3026 the pinned dep stays unmodified, and there is no byte-stream scanning
3027 anywhere. The engine's hardcoded `vt.TerminalStream` became
3028 `vt.Stream(MuxHandler)` and nothing else moved.
3029
3030 **Awaits resolve at the run loop's 100ms tick, with no deadline folding
3031 into poll.** The spec expected settle/timeout/pgid deadlines to fold into
3032 `wait_ms` the way QUIC's `timeoutMs` does. They do not need to: nothing in
3033 `checkAwaits` blocks, the granularity bound is the tick the daemon already
3034 beats at, and an agent's cheapest verb costs a round trip anyway. The pass
3035 sits after every arm that can move the session on — so it sees this pump's
3036 marks, pgid and silence — and *before* the QUIC `drainAll`, because a
3037 resolved await queues a frame and `drainAll` is what puts it on the wire.
3038 The other order costs every remote await a whole extra poll cycle.
3039
3040 **Session death is an answer on the lifecycle verbs and an error on the
3041 rest.** `muxa run` and `muxa await` print
3042 `{"reason":"session_ended","exit_code":N}` and exit 0 — a command that
3043 killed its own shell answered the question that was asked. `status`,
3044 `capture` and `send` have no answer to give, so they print the house's
3045 `{"error":…,"detail":…,"exit_code":…}` shape and exit 1. **Field note that
3046 cost a test:** `send`'s ack round-trip reliably WINS the race against a pty
3047 death, so `send` cannot be relied on to report a session that is dying —
3048 the next call is what sees it. That is a property of the ordering, not a
3049 flake.
3050
3051 **`--timeout` is the daemon's window; the client adds bounded grace.** The
3052 daemon's clock starts when it reads the request, so a client that waits
3053 exactly `--timeout` always loses the race to the reply and reports a
3054 timeout the daemon never had. The grace is `min(30s, max(2s, 4 ×
3055 connect_ms))`: flat 2s over a unix socket, RTT-derived over QUIC from the
3056 one measurement the client already has (its own handshake), capped so a
3057 handshake that took a minute cannot buy a minute of grace. The cost is
3058 named because agents budget wall clock: a `--timeout N` wait can overshoot
3059 N by the grace window.
3060
3061 **Reconnect is at-most-once for the wire and exactly-never for input.**
3062 One redial per process, on `ConnectionLost` and on nothing else (a
3063 `SendStalled` peer is still there and has already spent the flush bound
3064 proving it). What is re-sent is the attach and the `await_req` with the
3065 ORIGINAL `since_seq` — idempotent by construction, and the reason a return
3066 that landed inside the gap is answered instead of missed — and never
3067 `run`'s input. A command line lost with the connection surfaces as an
3068 honest timeout, not as `make deploy` running twice: **a wait may be
3069 repeated because asking twice changes nothing; an input may not, because it
3070 changes everything.** A redial that fails is recorded rather than
3071 swallowed, so the verb reports both halves — `connection lost; reconnect
3072 failed: <err>` — and a second tear after a spent redial says so in its own
3073 words.
3074
3075 **`settled` means output went quiet, not that the process exited.** The
3076 settle and pgid resolutions carry `exit_code: null` and a `phase` derived
3077 from marks, which on a markless shell reads `at_prompt` — the tracker
3078 never saw a `C`, so it is telling the truth about what it knows. Only
3079 `mechanism == "marks"` carries a trustworthy exit code, which is why every
3080 reply names its mechanism. An agent that reads `exit_code` without reading
3081 `mechanism` is reading a guess.
3082
3083 ### Field limitations, shipped knowingly
3084
3085 - **zsh under the `ZDOTDIR` shim never sources `~/.zshenv`.** zsh looks for
3086 `.zshenv` under `$ZDOTDIR`, and the shim directory has none. The `.zshrc`
3087 is handed back (the common case); a `.zshenv` shim that restores
3088 `ZDOTDIR` the way ghostty's does is the roadmap fix.
3089 - **The bash shim's DEBUG trap displaces a user's own DEBUG trap** —
3090 silently, and that is bash-preexec, atuin and iTerm2's integration. The
3091 roadmap fix is coexistence via bash-preexec detection. Two limits of the
3092 membership guard that keeps `PROMPT_COMMAND`'s own members from being
3093 counted as commands: a typed command textually identical to a
3094 `PROMPT_COMMAND` member loses its marks (degrading to pgid/settle, not to
3095 a wrong answer), and a compound string member (`a; b`) can re-arm the
3096 guard. Arrays — bash 5.1's default and what the shim prefers — are exact.
3097 - **A SIGKILLed daemon orphans its `mux-shellint-{pid}` directory.**
3098 Bounded and per-pid, cleaned on every ordinary exit; a startup sweep of
3099 dead-pid directories is the roadmap item. `prepare()` failing is degraded
3100 and never fatal: the session runs on pgid and settle, and says so on
3101 stderr, because refusing to start a daemon over an optional enhancement
3102 would invert the module's premise.
3103 - **`muxa` has no idle-timeout flag, by design.** It takes
3104 `quic.default_idle_ms` and lets `--timeout` be the only bound on a wait;
3105 `muxd`, `mux` and `muxweb` keep `--quic-idle-ms`.
3106
3107 ### Deferred, and named so the deferral reads as a choice
3108
3109 The spec's non-goals stand: **no MCP server** (a wrapper over `muxa` needs
3110 no protocol change, so it can be layered whenever someone wants it), **no
3111 read-only or capability-scoped auth** (one key is still full control, which
3112 is the same posture every other transport has), **no input attribution**
3113 (an attached human cannot tell which keystrokes were the agent's), **no
3114 semantic event subscriptions** beyond the `cmd_state` push, and no
3115 single-shot `muxa drive` (`send` + `await` + `capture` composes it). Added
3116 by the reviews: the `.zshenv` shim, bash-preexec coexistence, and the
3117 shim-directory startup sweep.
3118
3119 **Paid in a separate commit, and only after the e2e pinned the spellings:**
3120 `parseQuicAddr`/`resolveHost` now live in `quic.zig` as one owner for the
3121 dial-address grammar, taking an allocator (`client.zig`'s copy hardcoded
3122 `std.heap.page_allocator`, so the fold is also a fix). A human typing
3123 `mux quic://HOST:PORT` and an agent typing `muxa --quic HOST:PORT` were
3124 parsing the same grammar through two copies, which is how one flag becomes
3125 two dialects.
docs/roadmap.md
Old New
@@ -5,7 +5,8 @@ The forward view, one item per line, ranked. History and evidence live in
5 this file at each milestone close and whenever the queue reorders; the 5 this file at each milestone close and whenever the queue reorders; the
6 queue's order is set by the user, not by this file. 6 queue's order is set by the user, not by this file.
7 7
8 **Now:** M1–M15 complete; `v0.0.1-2` published as a Linux tarball; in 8 **Now:** M1–M15 and M17 complete, plus the agent surface; published as a
9 Linux tarball, tagged through `v0.0.1-5`; in
9 field trial on real VMs, and the trial is now producing the queue. 10 field trial on real VMs, and the trial is now producing the queue.
10 Trial feedback outranks everything below — what actually hurts in use is 11 Trial feedback outranks everything below — what actually hurts in use is
11 better data than any of this ranking, and the proof is that the three 12 better data than any of this ranking, and the proof is that the three
@@ -220,6 +221,56 @@ u64 overflow — 14 bytes could panic the hub). Banked follow-ups, led
220 by the headless browser-boot automation ("the page never ran" was the 221 by the headless browser-boot automation ("the page never ran" was the
221 milestone's defining defect), are listed there. 222 milestone's defining defect), are listed there.
222 223
224 ## Agent surface — native LLM integration — complete
225
226 Shipped 2026-08-13 (ac9373f..cbd7007 + close-out), twelve tasks. An agent
227 driving a mux session now *knows* when a command returned, with its exit
228 code and the rows its output occupies, over a unix socket or over QUIC —
229 which is the signal `tmux send-keys` + `capture-pane` structurally cannot
230 give and the reason this exists. What shipped: OSC 133 marks injected at
231 spawn (`shellint.zig` — zsh `ZDOTDIR` shim, bash `--init-file`, fish
232 `vendor_conf.d`), a mux-owned ghostty-vt stream handler that surfaces them
233 as row-stamped events, a pure command state machine (`cmd.zig`), three
234 frame pairs (`cmd_state`, `await_req`/`await_reply`,
235 `status_req`/`status_reply`) that ride `muxd proxy` untouched, server-held
236 awaits with a pgid edge and a settle floor under the marks path, and
237 `muxa` — a fourth binary, five verbs, one JSON object per verb, attaching
238 at 0×0 so an agent never claims the human's grid. e2e is `test/agent.sh`,
239 **9 scenarios**, including a QUIC tear healed mid-await with the command
240 proven to have run exactly once. The decisions worth reading before
241 touching it are in decisions.md: rows are locators and not anchors, an
242 await is answered from `last_return` and never from live tracker state,
243 and only `mechanism == "marks"` carries a real exit code.
244
245 **Field limitations, shipped knowingly** (each is a roadmap item, none is a
246 blocker):
247
248 - **zsh loses `~/.zshenv`** under the `ZDOTDIR` shim — the shim directory
249 has none. A ghostty-style `.zshenv` shim is the fix.
250 - **The bash shim's DEBUG trap displaces the user's**, silently: that is
251 bash-preexec, atuin and iTerm2. Coexistence via bash-preexec detection is
252 the fix. Two membership-guard limits stand meanwhile: a typed command
253 textually identical to a `PROMPT_COMMAND` member loses its marks
254 (degrading to pgid/settle), and a compound string member (`a; b`) can
255 re-arm the guard — arrays, the modern default, are exact.
256 - **A SIGKILLed daemon orphans its `mux-shellint-{pid}` directory**; a
257 startup sweep of dead-pid directories is owed. Injection failing is
258 always degraded and never fatal.
259 - **`--timeout N` can overshoot N** by the client's grace window (2s over a
260 socket, up to 30s over a slow QUIC handshake): an agent budgeting wall
261 clock must add it.
262 - **`muxa` has no idle-timeout flag**, deliberately — `--timeout` is the
263 only bound on a wait.
264
265 **Deferred, and named** (the spec's non-goals plus what review added): an
266 MCP server wrapping `muxa`; read-only or capability-scoped auth (one key =
267 full control still); input attribution; semantic event subscriptions
268 beyond the `cmd_state` push; `muxa drive` as single-shot sugar; the
269 `.zshenv` shim; bash-preexec coexistence; the shim-directory startup
270 sweep. The `parseQuicAddr`/`resolveHost` dedup that review endorsed is
271 **paid**, not deferred — one owner in `quic.zig`, landed after the e2e had
272 pinned the spellings.
273
223 ## M16 candidates — and still outranked by trial feedback 274 ## M16 candidates — and still outranked by trial feedback
224 275
225 What remains is what was ranked behind M13 and M14 and survived M15, 276 What remains is what was ranked behind M13 and M14 and survived M15,
docs/superpowers/plans/2026-08-13-agent-surface.md
Old New
@@ -10,6 +10,17 @@
10 10
11 **Spec:** `docs/superpowers/specs/2026-08-13-agent-surface-design.md` — read it first. 11 **Spec:** `docs/superpowers/specs/2026-08-13-agent-surface-design.md` — read it first.
12 12
13 > **Status (2026-08-13, all twelve tasks shipped):** this file is the
14 > historical execution record — what was planned and in what order — and it
15 > is deliberately not updated to match what landed. Where the two disagree,
16 > the code and `docs/decisions.md` are the truth. The divergences worth
17 > knowing before reading below: `returned_seq` was deleted in favour of one
18 > owner, `last_return`, which carries the watermark inside the answer;
19 > `checkAwaits` runs before the QUIC `drainAll` at the end of the pump, not
20 > after it; `muxa status` nests the cursor in its JSON; and the bash shim's
21 > final shape (array-aware `PROMPT_COMMAND`, membership guard, trap
22 > installed last) postdates the text in Task 7.
23
13 **Conventions for every task:** 24 **Conventions for every task:**
14 - `ZIG=~/Downloads/zig-x86_64-linux-0.15.2/zig` — run from the worktree root (`.worktrees/agent-surface`). 25 - `ZIG=~/Downloads/zig-x86_64-linux-0.15.2/zig` — run from the worktree root (`.worktrees/agent-surface`).
15 - Full suite: `$ZIG build test`. It must pass before every commit. 26 - Full suite: `$ZIG build test`. It must pass before every commit.
src/quic.zig
Old New
@@ -39,9 +39,13 @@ pub const default_port: u16 = 4433;
39 /// gone. Long enough that a quiet terminal is not a suspicious one, short 39 /// gone. Long enough that a quiet terminal is not a suspicious one, short
40 /// enough that a client which has genuinely vanished stops being served 40 /// enough that a client which has genuinely vanished stops being served
41 /// within a few seconds of keepalives failing — keepalives run at a third 41 /// within a few seconds of keepalives failing — keepalives run at a third
42 /// of it, so an idle session is never the thing that trips it. Tunable on 42 /// of it, so an idle session is never the thing that trips it.
43 /// both binaries because the reconnect tests need death declared on a 43 /// `--quic-idle-ms` tunes it on the three binaries that dial or listen for
44 /// schedule they can wait for. 44 /// a human — `muxd`, `mux`, `muxweb` — because the reconnect tests need
45 /// death declared on a schedule they can wait for. `muxa` deliberately has
46 /// no such flag and always takes this default: an agent's wait is bounded
47 /// by `--timeout` already, and a second knob over the same wait is one
48 /// more thing for a driver to get wrong.
45 /// 49 ///
46 /// Here for the same reason as `default_port`: two copies of a number both 50 /// Here for the same reason as `default_port`: two copies of a number both
47 /// binaries default to are two numbers, and they drift in silence. 51 /// binaries default to are two numbers, and they drift in silence.