a73x

fa0eb22a

docs: M10 record and roadmap close — cleared on the real box

a73x   2026-08-09 15:48

Commit message
docs: M10 record and roadmap close — cleared on the real box

Kill criterion evidence, five defect writeups with the assert-the-
literal rule, accepted gaps with rationale, and the two banked stages
(attach auto-start, ssh->QUIC handoff) promoted to the head of the
trial-friction tier with their sketches referenced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

docs/decisions.md
Old New
@@ -1369,6 +1369,131 @@ Engine, over a socket. The full render-vs-dump harness stays banked.
1369 It still fails structurally on fast links; the form that fixes it is now 1369 It still fails structurally on fast links; the form that fixes it is now
1370 standard and written down. 1370 standard and written down.
1371 1371
1372 ## 2026-08-09 (M10 — QUIC ergonomics)
1373
1374 **Verdict: cleared, on the real box.** The whole QUIC story is four
1375 commands — `muxd keygen`; one ssh line placing the key; `ssh HOST 'muxd
1376 start --quic 0.0.0.0'`; `mux quic://HOST` — with no port or key path
1377 typed anywhere and the trust posture unchanged (32-byte PSK, permissive
1378 files refused, no unauthenticated mode). Kill criterion held on the LAN
1379 box from a clean slate: the commands as written landed a session on the
1380 first attach; a marker survived full ssh logout (the spawning session
1381 died while daemon pid 8829 lived on — noting the box had `Linger=yes`,
1382 which governs `systemd --user` and not a setsid daemon, so
1383 `KillUserProcesses` at its default `no` is the operative mechanism and
1384 the one the README's survival sentence claims); ten attach/detach
1385 cycles never
1386 started a second daemon (enumeration matched the tracked up-line pid
1387 exactly); the `start` rerun no-op'd with exit 0; `dump` carried the
1388 marker. Spec: superpowers/specs/2026-08-09-m10-quic-ergonomics-design.md
1389 (re-cut mid-design from "daemon auto-start" when the user pointed at the
1390 lower-hanging fruit; the auto-start and ssh→QUIC-handoff stages are
1391 banked there with full sketches).
1392
1393 Shipped: `muxd keygen` (0600 key in a 0700 dir, refusal is atomic at the
1394 syscall and leaves bytes untouched); default key path
1395 `$XDG_CONFIG_HOME/mux/key` with resolution `--key` > `MUX_KEY_FILE` >
1396 default-if-present on BOTH binaries (`pickKey` pure and unit-pinned, its
1397 call site pinned separately by e2e — see the third gate below; the
1398 daemon reading the env var is new); default port 4433 both parsers,
1399 single constant in quic_server.zig; `muxd start` (probe → fork/setsid →
1400 poll; already-running is a no-op exit 0; failure names the log;
1401 `~/.local/state/mux/muxd.log` truncated per spawn); honest `--via`
1402 failure (`lostMsg`: new wording iff `via != null` and no frame ever
1403 arrived — for any other transport it would be a different lie);
1404 `--version` on both binaries from one build.zig constant; systemd fully
1405 deleted (contrib/ units, `LISTEN_FDS` socket activation — verified
1406 working same-day before removal, this commit range is the resurrection
1407 reference — and `owns_sock_file`, whose dev/ino teardown check stays and
1408 is still mutation-covered both ways).
1409
1410 **Defects found and fixed en route:**
1411 - **`muxd start` panicked (exit 134) whenever its child died young** —
1412 the poll loop's second `waitpid(NOHANG)` after its own reap gets
1413 ECHILD, which the stdlib maps to `unreachable`. Fired on exactly the
1414 first-run mistake the failure line exists for (`--quic` before the key
1415 was placed); neither test layer covered a dying child (the unit stub
1416 slept forever, every e2e start succeeded). Fixed 9bd43fc: a pid owes
1417 exactly one reap; pinned at both layers by a stub that exits
1418 immediately.
1419 - **Three plan mutation gates could not fail.** The port tests asserted
1420 `expectEqual(quic.default_port, …)` — the constant against itself, a
1421 tautology that stayed green under mutation; the race scenario typed
1422 its marker *after* the race through the same socket path, so a stolen
1423 socket answered indistinguishably; and the key-resolution order,
1424 swapped at its CALL SITE (`envKey() orelse o.key` in `run()`),
1425 passed both layers while a live daemon authenticated with the wrong
1426 key — until 144f6af the daemon's env path had zero automated coverage
1427 anywhere. Two rules now standing. First: **assert the literal, never
1428 the constant the code under test reads** — the shape is a defect
1429 whenever the constant is a contract with something outside the
1430 process (a second binary, a user), fine when private. Second, and the
1431 likelier to recur in a codebase that rightly extracts for
1432 testability: **a pure function's unit test pins the function, not its
1433 call site** — extraction can move the risk into the argument list
1434 rather than removing it (`pickKey`'s three parameters are all
1435 `?[]const u8`, so a swap compiles silently), and the extraction needs
1436 its own end-to-end pin where it is called. Replaced with literal
1437 pins, a head-on stolen-socket e2e, and the call-site key scenario.
1438 - **Third instance of cross-binary drift**: two independent 15_000
1439 idle-ms constants, each self-asserted only. Single-sourced beside
1440 `default_port` and pinned by literal (abae058).
1441 - **Two silent-side-effect test bugs**: the keygen refusal test re-read
1442 through a kept fd (follows the inode — an unlink-and-replace of the
1443 credential passed); and the spawn test truncated the developer's real
1444 `muxd.log` (post-`muxd start`, that is a live daemon's log NUL-holed
1445 by `make test`). Both proven by control before and after fixing
1446 (ce7a921, 35ae0b2).
1447 - **`std.posix.exit` in a fork child under link_libc is exit(3)** —
1448 atexit handlers plus the *parent's* inherited stdio buffers flushed
1449 twice. `exit_group` is the only safe child exit (7b4208f).
1450 - **M9 burst flake, 1-in-3 under load (review runs at 715f71b)**:
1451 `confirmed 5` was exact-equality on a timing-dependent counter; the
1452 M9 WAN record itself shows the counter legitimately short (churn
1453 flush, `abandoned=1`). Now `>= 4` via a separate `want_stat_ge`
1454 helper — separate so weakening a *correctness* counter (contradicted,
1455 expired: still exact) never becomes a habit. Four consecutive green
1456 suites after (implementer's Task 10 runs at d587aa2), plus the
1457 reviewer's two at the same commit.
1458
1459 **Accepted, with rationale in place:** the e2e start scenarios leak a
1460 daemon if they fail inside the two assertions between spawn and pid
1461 capture (closing it would mean parsing the pid before asserting the
1462 lines that prove it is there); `muxd start --version` is an unknown
1463 argument (`--version` is recognized at argv[1] only — a command, not a
1464 flag, and with `keygen`/`start` present that stays a rule, not an
1465 exception).
1466
1467 **Banked by M10:** attach auto-start (`muxd proxy`/local `mux` call the
1468 spawn helper; unix-socket only; silence stays meaningful) and the
1469 ssh→QUIC handoff (`muxd endpoint` prints port+key over ssh; client-side
1470 cache with a ~2s attach deadline; the coordination ssh stays alive as
1471 the fallback proxy; key belongs to the host-user on disk, outliving
1472 daemons — restarts invalidate only cached ports; rotation = delete +
1473 restart; measure the wrong-key failure mode before trusting any
1474 deadline number) — both fully sketched in the spec. Also banked: an
1475 absolute-plus-relative bound pattern where a guard's threshold is
1476 derived from a path it does not bound (the `refuse` helper carries the
1477 worked example); a decision on `quic://[NAME]` — the portless-bracket
1478 branch now DNS-resolves any bracketed string where it was refused
1479 before M10, a behavior that fell out of the IPv6 handling rather than
1480 being chosen; and a Zig plan-writing trap that bit twice: an inferred
1481 error set silently loses a member when its only `return error.X` is
1482 removed (breaking distant switch arms), and an error *set* return like
1483 `execveZ`'s cannot be `_ =` discarded.
1484
1485 Provenance worth keeping honest: the 0700 key directory (b405645) and
1486 the cross-binary version-drift e2e pin (a33238a) were review findings
1487 that amended the spec mid-milestone, not design foresight.
1488
1489 Process note for the record: subagent pipeline as in M9 (implementer +
1490 reviewer in isolated worktrees, mutations written first). The reviewer
1491 re-fired mutations independently every round and found the waitpid HIGH
1492 the suite was structurally blind to; the implementer twice refused to
1493 tick a mutation checkbox that could not fire and said so instead of
1494 proceeding. One review round was NOT-APPROVED; everything else landed
1495 without rework cycles.
1496
1372 ## Open (owed by later milestones) 1497 ## Open (owed by later milestones)
1373 1498
1374 - Scrollback retention *tuning*. The policy itself was decided in M1 and 1499 - Scrollback retention *tuning*. The policy itself was decided in M1 and
docs/roadmap.md
Old New
@@ -5,9 +5,9 @@ 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:** `v0.0.1-1` tagged; M1–M9 complete; in field trial on real VMs. 8 **Now:** M1–M10 complete; `v0.0.1-2` published as a Linux tarball; in
9 Trial feedback outranks everything below — what actually hurts in use is 9 field trial on real VMs. Trial feedback outranks everything below —
10 better data than any of this ranking. 10 what actually hurts in use is better data than any of this ranking.
11 11
12 ## M9 — prediction (speculative local echo) — complete 12 ## M9 — prediction (speculative local echo) — complete
13 13
@@ -31,11 +31,33 @@ failure found ngtcp2 being re-entered on **every** QUIC exchange —
31 constant, with a rare consequence — which is fixed and, unusually, was 31 constant, with a rare consequence — which is fixed and, unusually, was
32 proven so by a mutation that fired immediately. 32 proven so by a mutation that fired immediately.
33 33
34 ## M10 — QUIC ergonomics — complete
35
36 **Verdict: cleared on the real box.** The whole QUIC story is four
37 commands — `muxd keygen`, one ssh line placing the key, `ssh HOST 'muxd
38 start --quic 0.0.0.0'`, `mux quic://HOST` — no port or key path typed
39 anywhere, trust posture unchanged. Kill criterion held on the LAN box:
40 first-attach success from a clean slate, a marker surviving full ssh
41 logout, ten attach/detach cycles with exactly one daemon counted by
42 tracked pid (decisions.md, M10). systemd is deleted, not deferred: the
43 contrib/ units and `LISTEN_FDS` activation are gone, with the removal
44 commit as the resurrection reference. En route: a panic on `muxd
45 start`'s own first-run-mistake path, two plan mutation gates that could
46 not fail, and a third cross-binary constant drift — all fixed, all with
47 the rule recorded ("assert the literal, never the constant the code
48 under test reads").
49
34 ## Trial friction, likely next — and still outranked by trial feedback 50 ## Trial friction, likely next — and still outranked by trial feedback
35 51
36 - **Daemon lifecycle**: attach does not auto-start `muxd run`; no install 52 - **Attach auto-start**: `muxd proxy` and local `mux` call the spawn
37 script; systemd user units exist in `contrib/` but are not the deployed 53 helper `muxd start` already uses, so `mux user@host` works on a box
38 recipe. First annoyance any trial hits. 54 where nothing is running. Sketched in the M10 spec; the helper is
55 built and shipping.
56 - **ssh→QUIC handoff**: `muxd endpoint` prints port+key over ssh;
57 client caches both, tries QUIC first under a ~2s deadline, and the
58 coordination ssh stays alive as the fallback proxy. Full sketch —
59 including the key-outlives-the-daemon model and the stale-cache
60 invariant — in the M10 spec's context section.
39 - **First-backoff tuning**: a reconnect after a tear can pay a 200ms–2s 61 - **First-backoff tuning**: a reconnect after a tear can pay a 200ms–2s
40 backoff step; measured ~4x win available on fast links, ~2x on slow 62 backoff step; measured ~4x win available on fast links, ~2x on slow
41 (decisions.md, M7). 63 (decisions.md, M7).