82232063
docs: M13 closed — auto-start, muxd stop, error audit; regrade 3/3 caught
a73x 2026-08-10 19:53
Commit message
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 | ||