a73x

82232063

docs: M13 closed — auto-start, muxd stop, error audit; regrade 3/3 caught

a73x   2026-08-10 19:53

Commit message
docs: M13 closed — auto-start, muxd stop, error audit; regrade 3/3 caught

roadmap.md: M13 moves to done with its verdict paragraph; the candidates
section is re-cut as M14 (the trial-friction bundle shipped), carrying
the two items M13 created — log rotation via logPathFor, per-socket
daemon logs. "Now" is M1–M13.

decisions.md: the M13 section, in the file's chronological place after
M12 — verdict, the regrade table with the failure lines verbatim, what
shipped, the findings (observer-until-attach, the append-only log and
the ssh side effect that forced it, fallible review prescriptions, two
plan defects amended at source, legibility as a regrade criterion,
TCSAFLUSH), the method note, and the banks.

Spec and plan headers record execution.

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

docs/decisions.md
Old New
@@ -2017,6 +2017,192 @@ address content, not commit SHAs.
2017 sweep, ASAN/valgrind over the QUIC tests, and rows 4, 5 and 12 as 2017 sweep, ASAN/valgrind over the QUIC tests, and rows 4, 5 and 12 as
2018 properties no rendered grid can represent. 2018 properties no rendered grid can represent.
2019 2019
2020 ## 2026-08-10 (M13 — trial friction: attach auto-start, `muxd stop`, error audit)
2021
2022 **Verdict: cleared, both legs.** The three findings a week of real use
2023 produced are closed, and none of them by a message-only fix. `mux
2024 user@host` and `muxd proxy` against a box with no daemon now start
2025 one — one helper, `spawn.ensureForAttach`, at both call sites, spawning
2026 a **bare** `run --sock` (never `--quic`), with no opt-out and a
2027 contractually silent warm path. `muxd stop` is a protocol verb,
2028 `stop_req = 0x07`, armed on **both** dispatches and idempotent when
2029 nothing is listening. The audit reworded six messages and added two
2030 refusals, the `sun_path` one turning a two-second timeout story into an
2031 instant named no — **2.006s → 0.0014s** measured on the `mux` side. Leg
2032 1: the suite grew to **15 scenario checkpoints over 28 convergence
2033 points**, both pinned as literals, with `SOAK_N=10 make soak` at
2034 **10/10 on the code that ships** (414d282). Leg 2: three resurrections,
2035 three catches, each dying at exactly the check it was predicted to die
2036 at and printing exactly the predicted text. The rule the milestone
2037 produced is a grading rule: **a regrade catch must be legible** — a
2038 bare command under `set -e` with stderr redirected is an anonymous
2039 abort whose evidence the trap deletes, so the predicted-catch *text* is
2040 part of the criterion, not commentary on it. Spec:
2041 superpowers/specs/2026-08-10-m13-trial-friction-design.md.
2042
2043 ### The regrade table
2044
2045 One sitting, one tree, one set of binaries; baseline green before, final
2046 state green after, `git status` clean throughout. Each row names the
2047 resurrection, what the milestone predicted it would print, and what it
2048 actually printed.
2049
2050 | Resurrection | Predicted catch | Verdict |
2051 | --- | --- | --- |
2052 | The `muxd proxy` auto-start call site reverted | `e2e FAIL: proxy auto-start: attached daemon lost the marker`, with `muxd proxy: cannot connect to <sock>` in the dump | **CAUGHT** |
2053 | Both `stop_req` arms deleted (`serviceObserver` and `handleFrame`) | unit: both Task 2 tests fail at `expect(shutdown_flag.load(.acquire))`; e2e: `e2e FAIL: stop exited 1, want 0` followed by `muxd stop: <sock> still answering after 2s (if it was started detached, its log is <path>)` | **CAUGHT** |
2054 | `lostMsg` reverted, implementation only | unit: the old-vs-new string diff; e2e: `e2e FAIL: --via death message:` and the old string, at `test/e2e.sh:499` | **CAUGHT** |
2055
2056 **The proxy call site — the dump corroborates twice over.** The
2057 predicted line and the predicted dump content both appeared, and the
2058 capture carried two lines nobody had asked for: the probe's own
2059 `muxd dump: nothing listening on ...`, and a trailing `mux: transport
2060 command failed before a session started` — the audit's new wording
2061 naming the failure from the other end of the same event. Three
2062 independent messages, one cause, no guessing.
2063
2064 **Both `stop_req` arms — the deadline is the failure's own.** Both lines
2065 appeared, in the predicted order. The cost of the failure was ~2s
2066 (124s of wall against the baseline's 123s), and that 2s is the verb's
2067 own deadline expiring, not a suite timeout absorbing it — which is the
2068 difference between a scenario that reports and a scenario that hangs.
2069 The trap reaped every daemon even though the mutant ignores the stop
2070 verb outright: teardown does not depend on the feature under test.
2071
2072 **`lostMsg` — the first line of defense fired.** The e2e catch is at
2073 `test/e2e.sh:499`; the belt-and-braces control at `:531`, which fails if
2074 the *old* wording is still emitted anywhere in the capture, was never
2075 reached, because the positive pin had already exited. A control that
2076 never fires because the primary caught it first is the layering working.
2077
2078 ### What shipped
2079
2080 `spawn.findInPath` (`src/spawn.zig`) — PATH search taking PATH as a
2081 parameter, with empty segments skipped so the implicit current directory
2082 is never searched, while an explicit relative entry is honored as the
2083 choice somebody typed. `probe` is now public, because both the stop verb
2084 and auto-start need to ask "is anything listening" without attaching.
2085
2086 `stop_req = 0x07` (`src/protocol.zig`) — armed in **both** dispatches.
2087 The load-bearing arm is `serviceObserver`: a connection starts as an
2088 observer and is promoted only on attach, and `muxd stop` never attaches.
2089 The `handleFrame` arm exists so the verb means the same thing on any
2090 connection rather than only on the one shape of connection the client
2091 happens to use today. Safety against an old daemon rests on `MsgType`
2092 being non-exhaustive — an unknown verb is already a defined outcome.
2093
2094 `muxd stop` (`src/main.zig`) — connect, send `stop_req`, then poll
2095 probe-first/deadline-second until connect refuses, then `muxd: stopped`
2096 and exit 0. Write errors are swallowed deliberately (a daemon that dies
2097 mid-write did what was asked), but an `asked` bool records whether the
2098 request was ever delivered, so the timeout message cannot claim a
2099 request that never left. Nothing listening exits 0 — the verb is
2100 idempotent, which is what makes it usable in a teardown. A wedged daemon
2101 gets the still-answering line with the log clause attached only when
2102 there is a log path to name, and exit 1.
2103
2104 Attach auto-start at both sites via `spawn.ensureForAttach`, one
2105 `start_deadline_ms = 2000` for both. `muxd proxy` uses `/proc/self/exe`
2106 and calls from `main.zig`'s dispatch arm, so `proxy.zig` stays
2107 import-clean; local `mux` uses `findInPath`, and a missing `muxd` is
2108 fatal only if the probe *also* fails — the message is then `mux: no
2109 daemon on <sock> and no muxd in PATH to start one`, which names both
2110 halves of the situation rather than the half that happened to be checked
2111 last.
2112
2113 The audit: six rewords, two new refusals (`sun_path` over 107 bytes, in
2114 both binaries), `muxd dump`/`muxd stats` now say ``nothing listening on
2115 <sock> (`muxd start` starts a daemon)``, and `muxd start`'s
2116 already-running hint became fully actionable — ``stop it first with
2117 `muxd stop --sock <sock>` `` — which it could not have been before this
2118 milestone, because the verb it names did not exist. `lostMsg` is now
2119 `mux: transport command failed before a session started`: ssh's own
2120 stderr has already named the cause, and the old parenthetical guessed a
2121 different one.
2122
2123 e2e grew from 13 scenarios / 25 convergence points to **15 / 28**: the
2124 proxy cold/warm arc (with the warm-silence pin, `muxd stop` as the
2125 tested teardown, and an idempotence control), a pty leg for local-`mux`
2126 auto-start — the first reuse of M12's fixture by a later milestone — and
2127 the `sun_path` refusals with both a timeout-story can-fail control and a
2128 `--version` exemption pin. A `wait_pid_gone` helper replaced the
2129 open-coded waits.
2130
2131 ### Findings
2132
2133 - **A connection is an observer until it attaches.** Any verb that must
2134 work on a bare connection belongs in `serviceObserver`, not
2135 `handleFrame`. Found because the spec cited a *test helper's* switch as
2136 though it were the dispatch — the helper's shape had drifted from the
2137 daemon's, and reading the helper would have shipped a verb that worked
2138 only for clients that had already attached, which `muxd stop` never
2139 does.
2140 - **Auto-start turns "a second daemon on another socket" from an
2141 explicit act into an ssh side effect.** That is the whole reason the
2142 shared daemon log became a hazard: a truncating open on a log another
2143 live daemon is appending to leaves a NUL hole in the middle of the
2144 file. The fix is `O_APPEND` — an atomic seek-to-end per write, not a
2145 one-time seek — and the reviewer's proposed `createFile(.truncate =
2146 false)` would have been a second bug, front-overwriting the existing
2147 contents from offset 0. Truncation is now reserved for `muxd start`,
2148 where somebody asked for a fresh daemon. The cost is named and
2149 accepted: monotonic growth for a user who never types `start`,
2150 rotation banked against `logPathFor`. A CLOEXEC regression in the
2151 hand-rolled open (`createFile` had been setting it for free) was caught
2152 in review and verified by observation rather than by reading — before
2153 the fix, `/proc/<pid>/fd` showed the user's own shell holding fd 3 on
2154 `muxd.log`; after, zero.
2155 - **Review prescriptions are themselves fallible, and the pipeline
2156 caught its own.** Three in one milestone: a reviewer's blank-line fix
2157 that did not compile (it left an unattached doc comment), the
2158 `truncate = false` prescription above, and a line-number
2159 cross-reference that went stale *within its own review cycle*. An
2160 implementer rejecting a bad prescription with evidence is the process
2161 working, in the direction it is easier to forget it runs.
2162 - **Two defects were traced to the plan's prescribed code and amended
2163 there first.** The `stopCmd` loop checked its deadline before the final
2164 probe — a failure line about an interval nobody had actually checked —
2165 and the failure path called `try xdg.logPath`, which under an absent
2166 HOME replaces the verdict with a stack trace. Both were fixed in the
2167 plan before being fixed in the tree, because a defect left in the plan
2168 is a defect the next implementer re-derives.
2169 - **A regrade catch must be legible.** A bare command under `set -e`
2170 with stderr redirected produces an anonymous abort whose evidence the
2171 cleanup trap then deletes. The predicted-catch text is part of the
2172 criterion: a resurrection that kills the suite without printing its
2173 line proves the code is load-bearing but not that the check is.
2174 - **`TCSAFLUSH` discards input queued across the auto-start spawn
2175 window.** Raw-mode entry flushes what the fixture typed while the
2176 daemon was starting, so a pty scenario must open on `expect
2177 \x1b[?1049h` — the alternate-screen enter — before its first send.
2178 M12's rule again, in a new costume: the barrier has to be a byte the
2179 scenario can actually observe.
2180
2181 ### Method note
2182
2183 Subagent pipeline, as M11 and M12: one implementer and two reviewers per
2184 task, quality and spec, with every new assertion written mutation-first
2185 (write the test, break the code the prescribed way, *see* it fail,
2186 restore, see it pass). The adversarial flow is only worth its cost if it
2187 runs in both directions, and this milestone is the evidence that it
2188 does: three reviewer prescriptions were rejected with evidence rather
2189 than implemented, and two plan defects were amended at the source
2190 document before the tree was touched. The three resurrections were run
2191 against a green baseline in one sitting, with the tree verified clean
2192 before and after; no product code was left mutated.
2193
2194 ### Banked by M13
2195
2196 - **Log rotation via `logPathFor`.** The named, accepted cost of the
2197 append-only decision: a user who only ever attaches never truncates
2198 the daemon log. The seam exists; the policy does not.
2199 - **Per-socket daemon logs.** Auto-start makes a second daemon a side
2200 effect of an ssh command, and every daemon appends to the same file.
2201 Per-socket paths are what make a multi-daemon box readable.
2202 - The M11/M12 banks are unchanged: prediction polish, the unit-layer
2203 mutation sweep, ASAN/valgrind over the QUIC tests, a `make gate` for
2204 loaded runs, and the M12 pty-scenario timeout notes.
2205
2020 ## Open (owed by later milestones) 2206 ## Open (owed by later milestones)
2021 2207
2022 - Scrollback retention *tuning*. The policy itself was decided in M1 and 2208 - Scrollback retention *tuning*. The policy itself was decided in M1 and
docs/roadmap.md
Old New
@@ -5,12 +5,12 @@ 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–M12 complete; `v0.0.1-2` published as a Linux tarball; in 8 **Now:** M1–M13 complete; `v0.0.1-2` published as a Linux tarball; in
9 field trial on real VMs, and the trial has started producing the queue. 9 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 10 Trial feedback outranks everything below — what actually hurts in use is
11 better data than any of this ranking, and the three items at the top of 11 better data than any of this ranking, and the proof is that the three
12 the friction tier are there because they hurt this week, not because 12 items which headed this list a week ago got there by hurting, not by
13 they were predicted. 13 being predicted, and shipped as M13.
14 14
15 ## M9 — prediction (speculative local echo) — complete 15 ## M9 — prediction (speculative local echo) — complete
16 16
@@ -91,28 +91,39 @@ failing with the mutation's own signature — and the rule extracted is
91 **an assertion about order must be an assertion the bytes can actually 91 **an assertion about order must be an assertion the bytes can actually
92 carry**. 92 carry**.
93 93
94 ## M13 candidates — trial friction — and still outranked by trial feedback 94 ## M13 — trial friction: auto-start, `muxd stop`, error audit — complete
95 95
96 The first three are field findings from this week's real usage on a 96 **Verdict: cleared, both legs.** The three field findings that headed
97 jump-host box, not predictions. They are one natural bundle and the 97 this list are closed. `mux user@host` and `muxd proxy` against a box
98 likely next cut; the order within it is the user's call. 98 with no daemon now start one — one helper, `spawn.ensureForAttach`, at
99 99 both call sites, spawning a **bare** `run --sock` (never `--quic`), no
100 - **Attach auto-start** — now field-motivated, not merely sketched. 100 opt-out, and the warm path stays silent by contract. `muxd stop` exists
101 `mux user@host` against a box where no daemon is running fails with 101 as a protocol verb (`stop_req = 0x07`), armed on both dispatches because
102 `muxd proxy: cannot connect`, which is a correct message about the 102 a connection is an *observer* until it attaches; it is idempotent when
103 wrong thing: the user asked for a session, not for a daemon. `muxd 103 nothing is listening (exit 0) and honest when a daemon is wedged —
104 proxy` and local `mux` call the spawn helper `muxd start` already 104 still-answering, exit 1, and it names the log if the daemon was started
105 uses. Sketch banked in the M10 spec; the helper is built and shipping. 105 detached. The audit reworded six messages and added two refusals, the
106 - **`muxd stop`** — the lifecycle verb is simply missing. `muxd start 106 `sun_path` one replacing a 2s timeout story with an instant named no:
107 --quic` against an already-running default daemon says to stop it 107 **2.006s → 0.0014s**. Leg 1 held at **15 scenario checkpoints over 28
108 first, and there is no way to do that but find the pid yourself. Small 108 convergence points**, both pinned as literals, with `SOAK_N=10 make
109 on its own; it stops being small the moment auto-start ships, because 109 soak` at **10/10 on the code that ships** (414d282). Leg 2 is three for
110 auto-start makes socket-squatting common. 110 three: the proxy call site, both `stop_req` arms, and the `--via` death
111 - **Error-message audit** — the `--via` failure hint "(is muxd installed 111 message were each resurrected and each caught at exactly the check that
112 on the host?)" guessed wrong when ssh itself failed on a host-key 112 predicted them, printing the predicted text (decisions.md, M13, which
113 rejection, and sent the reading in the wrong direction. The rule to 113 carries the table and the failure lines). En route: auto-start turns "a
114 apply across every failure path: say what happened, name the way out, 114 second daemon on another socket" from an explicit act into an ssh side
115 and do not guess a cause a lower layer has already named. 115 effect, which is why the daemon log is now opened **append-only** and
116 truncation is reserved for `muxd start`; and three review prescriptions
117 were rejected with evidence, which is the adversarial flow running in
118 the direction it is easier to forget.
119
120 ## M14 candidates — and still outranked by trial feedback
121
122 The trial-friction bundle that used to head this section shipped as M13.
123 What remains is what was already ranked behind it, plus two items M13
124 created. Nothing here is a field finding; the next one that arrives
125 outranks all of it.
126
116 - **ssh→QUIC handoff**: `muxd endpoint` prints port+key over ssh; 127 - **ssh→QUIC handoff**: `muxd endpoint` prints port+key over ssh;
117 client caches both, tries QUIC first under a ~2s deadline, and the 128 client caches both, tries QUIC first under a ~2s deadline, and the
118 coordination ssh stays alive as the fallback proxy. Full sketch — 129 coordination ssh stays alive as the fallback proxy. Full sketch —
@@ -121,6 +132,19 @@ likely next cut; the order within it is the user's call.
121 - **First-backoff tuning**: a reconnect after a tear can pay a 200ms–2s 132 - **First-backoff tuning**: a reconnect after a tear can pay a 200ms–2s
122 backoff step; measured ~4x win available on fast links, ~2x on slow 133 backoff step; measured ~4x win available on fast links, ~2x on slow
123 (decisions.md, M7). 134 (decisions.md, M7).
135 - **The unit-layer mutation sweep** — carried from the test-debt section
136 below, where it has the evidence that it would pay. M13 adds a data
137 point of its own: the `stop_req` resurrection was killed at the unit
138 layer *and* end to end, and the unit pair failed first and cheapest.
139 - **Log rotation, via `logPathFor`** — created by M13 and its named,
140 accepted cost. Auto-start opens the daemon log append-only, so a user
141 who never types `muxd start` never truncates it and the file grows
142 without bound. The seam is already in place; only the policy is owed
143 (decisions.md, M13).
144 - **Per-socket daemon logs** — also from M13. Auto-start makes a second
145 daemon on a second socket an ssh side effect rather than a decision,
146 and every one of them appends to the same file. Per-socket paths are
147 what make a multi-daemon box readable after the fact.
124 148
125 ## Test debt, banked by M11 149 ## Test debt, banked by M11
126 150
docs/superpowers/plans/2026-08-10-m13-trial-friction.md
Old New
@@ -2,6 +2,8 @@
2 2
3 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. 3 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4 4
5 > **Executed in full, 2026-08-10.** Deviations and amendments recorded in decisions.md M13; the plan was amended twice mid-flight (stopCmd loop shape, log truncation) and those amendments are part of the record.
6
5 **Goal:** Attach auto-start (proxy + local mux), `muxd stop` as a protocol verb, and the error-message audit — the M13 bundle per `docs/superpowers/specs/2026-08-10-m13-trial-friction-design.md`. 7 **Goal:** Attach auto-start (proxy + local mux), `muxd stop` as a protocol verb, and the error-message audit — the M13 bundle per `docs/superpowers/specs/2026-08-10-m13-trial-friction-design.md`.
6 8
7 **Architecture:** Both auto-start call sites reuse `spawn.ensureDaemon` unchanged (M10 built it for exactly this); `muxd stop` sends a new `stop_req` frame whose handler sets the daemon's existing SIGTERM `shutdown_flag`, so no new shutdown code exists; the audit rewords seven messages from a closed table in the spec. 9 **Architecture:** Both auto-start call sites reuse `spawn.ensureDaemon` unchanged (M10 built it for exactly this); `muxd stop` sends a new `stop_req` frame whose handler sets the daemon's existing SIGTERM `shutdown_flag`, so no new shutdown code exists; the audit rewords seven messages from a closed table in the spec.
docs/superpowers/specs/2026-08-10-m13-trial-friction-design.md
Old New
@@ -1,7 +1,6 @@
1 # M13 — trial friction: attach auto-start, `muxd stop`, error audit 1 # M13 — trial friction: attach auto-start, `muxd stop`, error audit
2 2
3 **Status:** approved 2026-08-10 (design review in session; scope, the 3 **Status:** executed 2026-08-10 — verdicts and deviations in decisions.md M13.
4 no-QUIC rule, and no-opt-out each confirmed by the user).
5 **Queue:** M13 = the full trial-friction bundle. The ssh→QUIC handoff and 4 **Queue:** M13 = the full trial-friction bundle. The ssh→QUIC handoff and
6 first-backoff tuning stay banked. 5 first-backoff tuning stay banked.
7 6