a73x

ef95d950

docs: M11 close — convergence harness held 10/10, campaign table recorded

a73x   2026-08-09 21:22

Commit message
docs: M11 close — convergence harness held 10/10, campaign table recorded

The campaign table moves into decisions.md in full: 18 product mutations,
old suite 7, new suite 13, and the six-row delta is all paint/overlay/clip.
Both kill-criterion legs recorded with their evidence, the three defects
found en route with the rules extracted (a timing margin is a number you
compute, not a sentence you write), and five both-miss rows closed as one
unit assertion and four banks with written reasons.

roadmap: M11 complete; the trial-friction tier rewritten around three
field findings from real usage (attach auto-start, the missing muxd stop,
an error-message audit); ASAN/valgrind and the unit-layer sweep stay
banked, each annotated with what the campaign taught about it.

The campaign scratch doc is deleted — this commit is the pointer.

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

docs/decisions.md
Old New
@@ -1494,6 +1494,275 @@ tick a mutation checkbox that could not fire and said so instead of
1494 proceeding. One review round was NOT-APPROVED; everything else landed 1494 proceeding. One review round was NOT-APPROVED; everything else landed
1495 without rework cycles. 1495 without rework cycles.
1496 1496
1497 ## 2026-08-09 (M11 — e2e hardening: convergence, pins, soak, mutation campaign)
1498
1499 **Verdict: cleared, both legs.** The e2e suite has stopped asking whether
1500 a marker string appeared and started asking whether the screen the client
1501 painted equals the screen the daemon holds — **22 convergence points
1502 across 10 scenario checkpoints**, both counts asserted as literals at
1503 runtime, every scenario ending in `assert_converged` with each declined
1504 exception named in a comment at its own site. `SOAK_N=10 make soak` is
1505 **10/10 green on the code that ships**, ~116s a run. The campaign that
1506 grades the harness broke the product **18 times**, one mutation at a
1507 time, and ran every break against both assertion sets out of the *same*
1508 mutated binaries: the old suite caught **7 of 18**, every one of them
1509 through a counter or a marker; the new suite caught **13 of 18**; and the
1510 six-row delta — rows 1, 2, 3 (paint path), 9, 10 (prediction overlay), 17
1511 (clip bound) — is entirely the byte-blind class the harness was built
1512 for, a wrong *picture* drawn from right *bytes*. Zero rows ended
1513 undecided: the five both-miss rows closed as one new unit assertion and
1514 four banked entries with written reasons. Spec:
1515 superpowers/specs/2026-08-09-m11-e2e-hardening-design.md.
1516
1517 ### The campaign table
1518
1519 The milestone's measurement, as M9's latency table was. Mutations lived
1520 only in a detached worktree, one at a time, restored and diff-verified
1521 between rows; both suites ran sequentially against the same build. The
1522 old suite is `test/e2e.sh` at `37ec262` — the last commit predating every
1523 M11 assertion — carried untracked as `e2e-old.sh`, with one deviation
1524 recorded below the table.
1525
1526 | # | site | mutation | old suite | new suite | disposition |
1527 |---|------|----------|-----------|-----------|-------------|
1528 | 0 | control | none (unmutated) | PASS | PASS | baseline |
1529 | 1 | paint path | delta paint drops each delta's last row | survived | caught: `base attach` — render diverges | demonstrated |
1530 | 2 | paint path | delta rows painted one row too low (CUP row+2) | survived | caught: `base attach` — render diverges | demonstrated |
1531 | 3 | paint path | full repaint preamble loses `\x1b[2J` | survived | caught: `quic epoch resync` — render diverges | demonstrated |
1532 | 4 | paint path | delta row's leading `\x1b[0m` stripped (SGR bleed) | survived | survived | banked: no grid can hold it (unit-pinned already) |
1533 | 5 | paint path | delta paint drops the `?2026` synchronized-update wrapper | survived | survived | new assertion: the `paintDeltaClipped` wrapper pin |
1534 | 6 | replica sync | delta painted and applied, but never fed to the replica | caught: `line mode: contradicted=1, want 0` | caught: `quic delta resume` — render diverges | both caught |
1535 | 7 | replica sync | snapshot's cols/rows prefix ignored (replica never resized) | survived | survived | banked: needs a resizing client fixture |
1536 | 8 | replica sync | resume attach quotes one seq MORE than the client holds | caught: `reconnect was served a snapshot (7 -> 8), not a delta` | caught: same line | caught by counters (both) |
1537 | 9 | prediction overlay | a CONFIRMED prediction keeps its underline painted | survived | caught: `line-mode prediction` — render diverges (styled leg only) | demonstrated |
1538 | 10 | prediction overlay | a made prediction is also fed into the replica engine | survived | caught: `raw mode` — render diverges | demonstrated |
1539 | 11 | delta production | delta payload omits its highest-numbered dirty row | caught: `line mode: contradicted=1, want 0` | caught: `base attach` — render diverges | both caught |
1540 | 12 | delta production | delta rows emitted in reverse row order | survived | survived | banked: order is not a state property |
1541 | 13 | delta production | `update()` never marks row 0 dirty | caught: `line mode: contradicted=1, want 0` | caught: `base attach` — render diverges | both caught |
1542 | 14 | seq/epoch | every second update reuses its predecessor's seq | caught: `raw mode: displayed=1, want 2` | caught: same line | caught by counters (both) |
1543 | 15 | seq/epoch | reconnect served a delta even when a snapshot is owed | caught: `restarted daemon served no snapshot (0); the stale seq was honoured` | caught: same line | caught by counters (both) |
1544 | 16 | seq/epoch | every snapshot prefix carries `epoch + 1` | caught: `reconnect was served a snapshot (7 -> 8), not a delta` | caught: same line | caught by counters (both) |
1545 | 17 | scrollback/clipping | client clip bound one row short: the last tty row is never painted | survived | caught: `reattach after kill` — render diverges (plain, `-sh-5.3$`) | demonstrated |
1546 | 18 | scrollback/clipping | resync no longer leaves scroll view (`scroll_pages = 0` deleted) | survived | survived | banked: needs a pty-driving client fixture |
1547 | 19 | paint path | every SGR run stripped from delta rows at the client paint | not run (scaffolding retired) | caught: `styled content` — render diverges (styled leg only) | gate for the styled scenario |
1548
1549 Every "render diverges" cell is the suite's own failure line, `e2e FAIL:
1550 NAME: client render diverges from daemon grid`, named by the scenario it
1551 fired in. Row 19 is not one of the 18 product mutations: it is the gate
1552 that justified the styled scenario Task 12 added, and its old-suite
1553 column is honestly blank — the scaffolding was retired with the worktree,
1554 and a marker grep cannot see a colour by construction.
1555
1556 **Tally: six demonstrated** (1, 2, 3, 9, 10, 17 — new caught, old
1557 survived), **three both-caught** (6, 11, 13), **four caught by counters in
1558 both columns** (8, 14, 15, 16), **five both-miss** (4, 5, 7, 12, 18), all
1559 five closed below. The control ran first and green on both suites at
1560 main's tip, 114s each: a campaign whose baseline is red measures nothing.
1561
1562 **The deviation in the old suite, recorded because it flatters nobody.**
1563 `e2e-old.sh` carries one line that is not the 37ec262 original — the
1564 burst scenario's keystroke interval, bumped 0.2 → 0.25, the same fix
1565 landed on main in 2bc64e8. Left unpatched, that defect (below) would have
1566 reported false "caught" verdicts on the old suite's burst line and
1567 inflated the old column. Nothing else was touched: the old suite's
1568 assertions are the object of comparison, so they stand exactly as they
1569 were.
1570
1571 **Where the delta actually is.** Rows 1-10 mutate the client and rows
1572 11-16 mutate the daemon, and the halves invert cleanly. Client-side
1573 breaks are mostly invisible to counters, because the counters live on the
1574 client and a client that lies to the screen still counts honestly.
1575 Daemon-side breaks are the opposite: the daemon is upstream of both the
1576 replica and the client's prediction bookkeeping, so five of those six
1577 trip a counter in *both* columns. That is not the convergence checks
1578 failing to earn their keep — it is the honest observation that the old
1579 suite's blind spot was the client, not the server, and the new checks are
1580 an instrument pointed at exactly that spot.
1581
1582 ### Defects found and fixed en route
1583
1584 **The burst knife-edge (2bc64e8) — a timing margin is a number you
1585 compute, not a sentence you write.** `delaypipe` drains one chunk per
1586 `DELAY_MS` serially in each direction, so burst keystrokes closer
1587 together than `DELAY_MS` queue, and each ages `(DELAY_MS - interval)`
1588 longer than the one before it:
1589
1590 ```
1591 age at which keystroke k is confirmed = 2*DELAY_MS + k*(DELAY_MS - interval)
1592 ```
1593
1594 At the old 200ms interval the fifth prediction reached judgment at
1595 `600 + 4*100 = 1000ms` — `predict.expire_after_ms` to the millisecond, a
1596 coin flip decided by which of two `milliTimestamp()` calls in the same
1597 poll iteration saw the boundary first. It failed 4 runs in 20 on an
1598 **idle** machine; no load required, and no prediction was ever judged
1599 wrong. The comment above the scenario had claimed margin on both sides,
1600 and that margin had never been computed. At 250ms the fifth lands at
1601 800ms, measured 798-799 across 12 runs. The same formula, applied
1602 backwards, retroactively explains the M9-era `DELAY_MS=400` collapse
1603 recorded above under "the burst/expiry ceiling": it puts the SECOND
1604 keystroke at 1000ms, which is the `confirmed=1` that was diagnosed then
1605 as a queueing effect and is now a closed-form one.
1606
1607 A second rule came out of the same bug: **`expired` increments
1608 `contradicted` through the shared `abandonAll` tail**, so
1609 `contradicted >= expired` is an invariant and a burst assertion that
1610 checks `contradicted` first reports an age-out as "a prediction judged
1611 wrong" — a different defect with a different cause, and the reason this
1612 flake was first investigated as a reconcile bug. The burst assertions now
1613 check `expired` first.
1614
1615 **The scroll-suppression reconnect bug (412f38f) — found by the campaign
1616 on UNMUTATED code.** Reading `client.zig` in order to grade row 18 turned
1617 up a real defect in HEAD: the resync path clears `scroll_pages` but never
1618 calls `overlay.setScrollMode(false)`, and `flush()` does not clear the
1619 mode — it drops pending and retired predictions and nothing else. A
1620 reconnect taken while scrolled therefore leaves the client painting live
1621 rows with the overlay still suppressing, and the "any other key leaves
1622 scroll mode" branch can never rescue it because that branch is guarded by
1623 `scroll_pages > 0`, which the reconnect has just made false. Local echo
1624 prediction is off for the rest of the session, silently. Fixed
1625 client-side with one call next to the existing `scroll_pages = 0` —
1626 deliberately not by making `flush()` clear the mode, because every
1627 snapshot flushes and a snapshot is not a reason to leave history; the
1628 overlay would start deciding where the viewport is, which is the client's
1629 business. Pinned where the property can be reached: `a flush leaves
1630 scroll mode exactly where it found it` (src/predict.zig) holds the
1631 contract in both directions. The end-to-end pin — "a reconnect while
1632 scrolled leaves prediction working" — rides the banked pty-fixture debt
1633 below and is **not** claimed here.
1634
1635 **Phase 1 landed with zero convergence failures, and one of those zeroes
1636 is evidence.** The plan named a candidate defect in advance: the
1637 raw-mode scenario's expired prediction (`j`), suspected since M9 of
1638 leaving a phantom underlined glyph no repaint ever removed. Twenty-two
1639 convergence points went green on first placement, styled leg included,
1640 which proves the client does repaint on expiry the way it already does on
1641 contradiction. A suspected gap that is checked and found clean is a
1642 result, not a non-event — the alternative is carrying the suspicion
1643 forward for another milestone.
1644
1645 ### Findings that reshaped what we thought the code was
1646
1647 - **The client has no seq-continuity logic at all** (row 8). The
1648 specified mutation — "remove the client's gap detection so a stale
1649 delta is applied" — could not be written, because there is no such
1650 code: the delta arm sets `last_seq` and applies unconditionally, and
1651 every gap decision is the daemon's, in `sendResync`. Expressed instead
1652 from the one client-side lever that exists (quote `last_seq +| 1`), the
1653 daemon's range guard noticed the lie and served a snapshot — the safe
1654 answer — so the screen rendered perfectly, all convergence points
1655 agreed, and only the counter spoke. A delta silently applied across a
1656 gap is **unreachable from the client**; the property is defended
1657 entirely by the daemon and is already mutation-proven there. This is
1658 the exact complement of rows 1-3, and a fourth proof of the standing
1659 entry "counters are asserted because markers are blind to *how* a
1660 resume was served".
1661 - **ghostty's formatter closes every styled row it emits** (row 4). The
1662 campaign's own prescription for row 4 was a styled scenario, on the
1663 reading that the styled leg was the right instrument handed no
1664 specimen. The scenario landed, the mutation was re-run, and it still
1665 survived — because `dumpVtRow`'s output both opens and closes with a
1666 reset ("style is reset before newline to prevent background colors
1667 from" bleeding, in ghostty's own test suite). Verified on the wire, not
1668 inferred: the delta row for `styled-red` carries two resets and the
1669 mutation strips only ours. So our leading reset is redundancy against a
1670 live pen that no byte source in the client can produce, and the only
1671 pen it could protect against belongs to the HOST terminal — which no
1672 rendered-grid comparison can ever hold, since the render helper replays
1673 into a fresh engine at the default pen by construction. The unit test
1674 at `src/client.zig:1837` is the real guard and fails under the mutation
1675 today; the bank names the dependency guarantee it rests on.
1676 - **Catch parity is not diagnosis parity** (rows 11 and 13). Both suites
1677 catch both mutations, so the disposition column reads "both caught" and
1678 says nothing interesting. What the columns hide: the old suite fails
1679 eleven scenarios in, at `line mode: contradicted=1, want 0` — a
1680 prediction was contradicted because the row that would have confirmed
1681 it never arrived, which is a symptom three inferential steps from the
1682 cause — while the new suite fails at `base attach`, the first scenario,
1683 and **prints the missing row**. Same verdict, eight scenarios of
1684 runtime and the entire distance between "a counter is off" and "this
1685 row is missing". Diagnosis cost is part of a suite's value even where
1686 catch parity is exact, and a table with a disposition column cannot
1687 show it, which is why this paragraph exists.
1688
1689 ### Accepted gaps, and banked by M11
1690
1691 The five both-miss rows closed as **one new assertion and four banks**,
1692 zero undecided. The banks fall in two classes and neither is a weak
1693 convergence check: rows 4, 5 and 12 are properties **no rendered grid can
1694 represent**, and rows 7 and 18 are branches **no fixture can reach**.
1695
1696 - **A pty-driving client fixture — now the suite's single largest
1697 coverage debt.** Rows 7 and 18 both wait on it. Every e2e client
1698 captures stdout to a file, so `ttySize` returns null and the client
1699 runs at the non-tty default of 80x24, which is also the daemon's: the
1700 snapshot-apply resize branch is dead code for the whole run, and no
1701 assertion of any kind could grade row 7. Scroll view is the same story
1702 from the other side — entering it requires stdin to deliver
1703 `\x1b[5;2~` in one read, which no suite contains, so `scroll_pages` is
1704 invariantly 0 and deleting an assignment of 0 to it is a semantic
1705 no-op. Neither is drivable from a unit test without a refactor this
1706 milestone should not make: the mutated statements sit inline in
1707 `session()`, which takes a live transport, two file descriptors and a
1708 run loop, has one caller and no test. One fixture — a client that owns
1709 a pty — makes both branches reachable and both mutations gradeable, and
1710 would also carry the end-to-end pin the 412f38f fix is missing.
1711 - **Row 12: row order is not a state property.** `composeDelta` emits
1712 `CUP(row+1);1H` + `EL(2)` + the row's bytes for each row it iterates,
1713 so the payload is a *set* by construction and permuting it is a no-op
1714 on the resulting grid. Asserting an emission order would forbid an
1715 equivalent implementation while pinning nothing a user can observe,
1716 which is the wrong trade for a wire format still moving; structural
1717 corruption is already rejected by the `seen != hdr.row_count` check. If
1718 row order ever becomes load-bearing — relative addressing, a
1719 scroll-region optimization — it becomes a protocol property and belongs
1720 to a `composeDelta` unit test on the composed byte string.
1721 - **The debugger's observation, unproven and owed a design pass.** The
1722 client calls `overlay.expire()` before reading frames that arrived in
1723 the same poll wakeup, so a prediction can be abandoned while its
1724 confirming evidence is sitting readable in the buffer. This came out of
1725 reading, not from a failing sample — **no spurious contradiction
1726 actually occurred** in this milestone, and the raw-mode expiry
1727 behaviour was proven correct by the styled leg. It touches the retired
1728 repaint list and the ordering of the poll loop, so it needs its own
1729 design pass rather than a patch; it is plausibly the same territory as
1730 M9's banked input-ack item, which remains the proper fix if spurious
1731 contradiction ever does appear.
1732 - **ASAN or valgrind over the QUIC tests** — carried from M9 unchanged,
1733 and unchanged is the point: this campaign was an e2e mutation campaign
1734 and says nothing about use-after-free. M9's two reasoned-not-pinned
1735 `deinit` orderings are still owed the only honest instrument for them.
1736 - **The unit-layer mutation sweep** — carried, and the campaign supplied
1737 evidence that it would pay. Two of this milestone's mutations were
1738 resolved by pins at the *unit* layer, not the e2e one (the `?2026`
1739 wrapper, which was pinned at no layer at all despite an assumption to
1740 the contrary; and the delta row's leading reset, where the existing
1741 pin turned out to be the whole guard). Both were found by aiming an
1742 e2e campaign at the product and discovering the unit layer's shape by
1743 accident. A sweep aimed at the unit layer directly would not need the
1744 accident.
1745
1746 ### Process note: the recording rule survived contact with a wrong premise
1747
1748 The rule was "every both-miss ends as a new assertion (mutation re-run to
1749 prove it fires) or a banked entry with a written reason — no third
1750 bucket, because 'noted' without a decision is the failure mode the
1751 campaign exists to prevent". It held, and the interesting part is *how*:
1752 one of the five rows had its prescribed disposition **disproved by
1753 attempting it**. Row 4 was filed as the single debt the convergence
1754 checks could pay, the fix being a styled scenario; the scenario landed,
1755 the mutation was re-run against it, and it survived — the premise was
1756 wrong, and the row moved to the "no grid can hold it" pile with a
1757 dependency guarantee named. A prescription that can be executed and found
1758 wrong is worth more than one that is merely plausible, and a rule that
1759 demands re-running the mutation is what forces the discovery instead of
1760 booking the fix as done. The scenario itself then had to justify its
1761 continued existence on its own terms and was gated by a new mutation
1762 (row 19) aimed at what it actually covers; before it, the byte-exact leg
1763 had exactly one specimen source in the entire corpus — the prediction
1764 overlay's own underline.
1765
1497 ## Open (owed by later milestones) 1766 ## Open (owed by later milestones)
1498 1767
1499 - Scrollback retention *tuning*. The policy itself was decided in M1 and 1768 - Scrollback retention *tuning*. The policy itself was decided in M1 and
docs/roadmap.md
Old New
@@ -5,9 +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–M10 complete; `v0.0.1-2` published as a Linux tarball; in 8 **Now:** M1–M11 complete; `v0.0.1-2` published as a Linux tarball; in
9 field trial on real VMs. Trial feedback outranks everything below — 9 field trial on real VMs, and the trial has started producing the queue.
10 what actually hurts in use is better data than any of this ranking. 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
12 the friction tier are there because they hurt this week, not because
13 they were predicted.
11 14
12 ## M9 — prediction (speculative local echo) — complete 15 ## M9 — prediction (speculative local echo) — complete
13 16
@@ -47,12 +50,50 @@ 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 50 the rule recorded ("assert the literal, never the constant the code
48 under test reads"). 51 under test reads").
49 52
53 ## M11 — e2e hardening — complete
54
55 **Verdict: cleared, both legs.** The suite has stopped asserting that a
56 marker string appeared and started asserting that the screen the client
57 painted equals the screen the daemon holds: **22 convergence points over
58 10 scenario checkpoints**, both pinned as literals, every scenario ending
59 in a convergence check with the declined exceptions named where they are
60 declined — and `SOAK_N=10 make soak` at **10/10 on the shipping code**,
61 ~116s a run. Then the harness was graded the only honest way, by breaking
62 the product 18 times and running each break against both assertion sets
63 out of the same binaries: old suite **7 of 18**, all via counters and
64 markers; new suite **13 of 18**; the six-row delta is all paint, overlay
65 and clip — the byte-blind class, a wrong picture drawn from right bytes
66 (decisions.md, M11, which carries the table in full). Zero rows ended
67 undecided. En route the campaign found a real bug in unmutated code — a
68 reconnect taken while scrolled left prediction silently off for the rest
69 of the session — and a computed timing knife-edge in the burst scenario
70 that had been sitting on the expiry bound exactly, whose formula also
71 explains the M9-era `DELAY_MS=400` collapse retroactively. The rule
72 extracted: **a timing margin is a number you compute, not a sentence you
73 write**.
74
50 ## Trial friction, likely next — and still outranked by trial feedback 75 ## Trial friction, likely next — and still outranked by trial feedback
51 76
52 - **Attach auto-start**: `muxd proxy` and local `mux` call the spawn 77 The first three are field findings from this week's real usage on a
53 helper `muxd start` already uses, so `mux user@host` works on a box 78 jump-host box, not predictions. They are one natural bundle and the
54 where nothing is running. Sketched in the M10 spec; the helper is 79 likely next cut; the order within it is the user's call.
55 built and shipping. 80
81 - **Attach auto-start** — now field-motivated, not merely sketched.
82 `mux user@host` against a box where no daemon is running fails with
83 `muxd proxy: cannot connect`, which is a correct message about the
84 wrong thing: the user asked for a session, not for a daemon. `muxd
85 proxy` and local `mux` call the spawn helper `muxd start` already
86 uses. Sketch banked in the M10 spec; the helper is built and shipping.
87 - **`muxd stop`** — the lifecycle verb is simply missing. `muxd start
88 --quic` against an already-running default daemon says to stop it
89 first, and there is no way to do that but find the pid yourself. Small
90 on its own; it stops being small the moment auto-start ships, because
91 auto-start makes socket-squatting common.
92 - **Error-message audit** — the `--via` failure hint "(is muxd installed
93 on the host?)" guessed wrong when ssh itself failed on a host-key
94 rejection, and sent the reading in the wrong direction. The rule to
95 apply across every failure path: say what happened, name the way out,
96 and do not guess a cause a lower layer has already named.
56 - **ssh→QUIC handoff**: `muxd endpoint` prints port+key over ssh; 97 - **ssh→QUIC handoff**: `muxd endpoint` prints port+key over ssh;
57 client caches both, tries QUIC first under a ~2s deadline, and the 98 client caches both, tries QUIC first under a ~2s deadline, and the
58 coordination ssh stays alive as the fallback proxy. Full sketch — 99 coordination ssh stays alive as the fallback proxy. Full sketch —
@@ -62,6 +103,29 @@ under test reads").
62 backoff step; measured ~4x win available on fast links, ~2x on slow 103 backoff step; measured ~4x win available on fast links, ~2x on slow
63 (decisions.md, M7). 104 (decisions.md, M7).
64 105
106 ## Test debt, banked by M11
107
108 - **A pty-driving client fixture** — the suite's single largest coverage
109 debt, and the campaign is what sized it. Every e2e client captures
110 stdout to a file, so it runs at the non-tty 80x24 default and can
111 never enter scroll mode; two mutations (snapshot resize prefix, scroll
112 view exit on resync) are therefore not merely unobserved but
113 *unreachable*, and no assertion of any kind could grade them. One
114 fixture makes both branches gradeable and carries the end-to-end pin
115 the scroll-suppression fix is currently missing.
116 - **The unit-layer mutation sweep** — still banked, now with evidence it
117 would pay. Two of M11's findings were resolved by pins at the unit
118 layer rather than the e2e one (the delta paint's synchronized-update
119 wrapper, which turned out to be pinned at no layer at all, and the
120 delta row's leading reset, where the existing unit pin turned out to
121 be the whole guard). Both were discovered by accident, while aiming an
122 e2e campaign at the product. A sweep aimed at the unit layer would not
123 need the accident.
124 - **ASAN or valgrind over the QUIC tests** — carried from M9 unchanged,
125 and unchanged is the finding: an e2e mutation campaign says nothing
126 about use-after-free. M9's two reasoned-not-pinned `deinit` orderings
127 are still owed the only honest instrument for them.
128
65 ## Banked, ranked by expected win 129 ## Banked, ranked by expected win
66 130
67 0. **Prediction polish, as a group** — backspace prediction (the 131 0. **Prediction polish, as a group** — backspace prediction (the
docs/superpowers/plans/2026-08-09-m11-campaign.md
Old New
@@ -1,781 +0,0 @@
1 # M11 Phase 2 — the mutation campaign
2
3 Phase 1 taught the e2e suite to assert render-vs-dump convergence. This
4 campaign measures what that bought: break the code deliberately, one
5 mutation at a time, and run each break against BOTH the suite as it
6 stood before Phase 1 (markers only) and the suite as it stands now
7 (markers + convergence + count pins). The catch-rate delta between the
8 two columns is the harness's measured value — the same way M9 measured
9 prediction against its own controls, rather than asserting it had
10 helped.
11
12 This document is the record. It lives in the MAIN checkout because
13 results are not mutations; the mutations themselves never touch main.
14
15 ## Method
16
17 **Where.** Every mutation is applied in the worktree at
18 `/home/xanderle/code/rad/mux-m11-campaign` (detached at main's tip —
19 `main` itself is checked out by the primary working copy, so the
20 worktree cannot hold the branch). The main checkout is never mutated.
21
22 **One at a time.** Apply a single edit, build, run both suites, restore
23 with `git checkout -- src/ test/`, and verify `git diff --stat` is
24 empty before the next mutation begins. A mutation that does not compile
25 is not a mutation — rework it into a different expression of the same
26 break until it builds.
27
28 **How built.** Each mutation is built in the worktree with the pinned
29 toolchain, `~/Downloads/zig-x86_64-linux-0.15.2/zig build`. The system
30 zig (0.17-dev) cannot build this repo. Note that `make test` does not
31 rebuild the `zig-out/` binaries; the build step is not optional.
32
33 **Binary provenance.** Both suites run the SAME mutated binaries, out
34 of the worktree's `zig-out/bin`. The old suite is not re-run against
35 some separately built tree — the only variable between the two columns
36 is the assertion set. The suites are run SEQUENTIALLY, never
37 concurrently; both rely on `$$`-isolated temp paths, which makes
38 sequential runs safe and concurrent ones not.
39
40 **The old suite.** `OLD_SUITE_SHA = 37ec262` ("test: render helper
41 replays a client capture into a dump-format grid") — the last commit
42 whose `test/e2e.sh` predates every M11 assertion. Task 1 landed the
43 render helper but touched only `build.zig` and `test/render.zig`, so
44 the e2e suite at that sha is the pre-convergence one; extracted to
45 `e2e-old.sh` in the worktree it has zero `assert_converged` calls and
46 takes FOUR args (no RENDER). It is scaffolding and stays uncommitted.
47
48 **One deviation from a verbatim extraction.** `e2e-old.sh` carries the
49 burst scenario's keystroke interval bumped from `0.2` to `0.25` — the
50 same one-line fix landed on main in 2bc64e8, which found that typing at
51 200ms places the burst's fifth prediction at exactly the 1000ms expiry
52 bound (measured 4/20 flake on an idle machine). Left unpatched, that
53 defect would report false "caught" verdicts on the old suite's burst
54 line and inflate the old column. Nothing else in the file was touched:
55 the old suite's assertions are the object of comparison, so they remain
56 exactly as they were.
57
58 **Recording rule.** Every mutation gets one row, and every row carries
59 an old-suite verdict, a new-suite verdict, and a disposition. A
60 mutation that BOTH suites miss is a FINDING, not a footnote: it is
61 resolved either as (a) a new assertion landed in this milestone, with
62 the mutation re-run to prove the assertion now fires, or (b) a banked
63 entry with a written reason the gap is accepted. There is no third
64 bucket — "noted" without a decision is the failure mode this campaign
65 exists to prevent.
66
67 **Hygiene.** Stranded daemons from a mutated build are killed by
68 tracked pid only, never by name or pattern.
69
70 ## Results
71
72 | # | site | mutation | old suite | new suite | disposition |
73 |---|------|----------|-----------|-----------|-------------|
74 | 0 | control | none (unmutated) | PASS | PASS | baseline |
75 | 1 | paint path | delta paint drops each delta's last row | survived | caught: `e2e FAIL: base attach: client render diverges from daemon grid` | demonstrated |
76 | 2 | paint path | delta rows painted one row too low (CUP row+2) | survived | caught: `e2e FAIL: base attach: client render diverges from daemon grid` | demonstrated |
77 | 3 | paint path | full repaint preamble loses `\x1b[2J` | survived | caught: `e2e FAIL: quic epoch resync: client render diverges from daemon grid` | demonstrated |
78 | 4 | paint path | delta row's leading `\x1b[0m` stripped (SGR bleed) | survived | survived | banked: no grid can hold it (unit-pinned already) |
79 | 5 | paint path | delta paint drops the `?2026` synchronized-update wrapper | survived | survived | new assertion: the `paintDeltaClipped` wrapper pin |
80 | 6 | replica sync | delta painted and applied, but never fed to the replica | caught: `e2e FAIL: line mode: contradicted=1, want 0` | caught: `e2e FAIL: quic delta resume: client render diverges from daemon grid` | both caught |
81 | 7 | replica sync | snapshot's cols/rows prefix ignored (replica never resized) | survived | survived | banked: needs a resizing client fixture |
82 | 8 | replica sync | resume attach quotes one seq MORE than the client holds | caught: `e2e FAIL: reconnect was served a snapshot (7 -> 8), not a delta` | caught: `e2e FAIL: reconnect was served a snapshot (7 -> 8), not a delta` | caught by counters (both) |
83 | 9 | prediction overlay | a CONFIRMED prediction keeps its underline painted | survived | caught: `e2e FAIL: line-mode prediction: client render diverges from daemon grid` (styled leg only) | demonstrated |
84 | 10 | prediction overlay | a made prediction is also fed into the replica engine | survived | caught: `e2e FAIL: raw mode: client render diverges from daemon grid` | demonstrated |
85 | 11 | delta production | delta payload omits its highest-numbered dirty row | caught: `e2e FAIL: line mode: contradicted=1, want 0` | caught: `e2e FAIL: base attach: client render diverges from daemon grid` | both caught |
86 | 12 | delta production | delta rows emitted in reverse row order | survived | survived | banked: order is not a state property |
87 | 13 | delta production | `update()` never marks row 0 dirty | caught: `e2e FAIL: line mode: contradicted=1, want 0` | caught: `e2e FAIL: base attach: client render diverges from daemon grid` | both caught |
88 | 14 | seq/epoch | every second update reuses its predecessor's seq | caught: `e2e FAIL: raw mode: displayed=1, want 2` | caught: `e2e FAIL: raw mode: displayed=1, want 2` | caught by counters (both) |
89 | 15 | seq/epoch | reconnect served a delta even when a snapshot is owed | caught: `e2e FAIL: restarted daemon served no snapshot (0); the stale seq was honoured` | caught: `e2e FAIL: restarted daemon served no snapshot (0); the stale seq was honoured` | caught by counters (both) |
90 | 16 | seq/epoch | every snapshot prefix carries `epoch + 1` | caught: `e2e FAIL: reconnect was served a snapshot (7 -> 8), not a delta` | caught: `e2e FAIL: reconnect was served a snapshot (7 -> 8), not a delta` | caught by counters (both) |
91 | 17 | scrollback/clipping | client clip bound one row short: the last tty row is never painted | survived | caught: `e2e FAIL: reattach after kill: client render diverges from daemon grid` (plain, `-sh-5.3$`) | demonstrated |
92 | 18 | scrollback/clipping | resync no longer leaves scroll view (`scroll_pages = 0` deleted) | survived | survived | banked: needs a pty-driving client fixture |
93 | 19 | paint path | every SGR run stripped from delta rows at the client paint | not run (scaffolding retired) | caught: `e2e FAIL: styled content: client render diverges from daemon grid` (styled leg only) | gate for the styled scenario |
94
95 **Control run (row 0).** Both suites green on unmutated code at main's
96 tip (2bc64e8), first attempt, no re-runs. Old suite: `e2e OK`, 114s.
97 New suite: `e2e OK (9 scenarios, 21 convergence points)`, 114s. A
98 campaign whose baseline is red measures nothing; this one measures.
99
100 **Rows 1-5 (paint path).** All five built on the first attempt with the
101 pinned toolchain; no compile reworks were needed, though row 1 had to be
102 written as a two-pass count-then-skip over `proto.deltaRowIterator`
103 rather than an `rows[0..n-1]` slice, because the delta rows arrive from
104 a streaming iterator and there is no slice to trim. No suite run was
105 re-run: every verdict below is a first-attempt result, and no scenario
106 failed in a place unrelated to its mutation, so there are no flake
107 attributions to make.
108
109 **Rows 1 and 2 — the structural-blindness demonstration.** Both are
110 caught by the new suite at the FIRST convergence point, `base attach`,
111 and both are plain-diff catches (the normalized grid differs, so the
112 styled leg never has to speak). The two diffs are mirror images and
113 worth keeping: dropping the last row leaves the client's grid missing
114 `sh-5.3$ printf ... / e2e-works`, while painting one row low leaves it
115 with an extra leading `sh-5.3$` and everything shifted down. The old
116 suite passes both, and passes them for exactly the reason the milestone
117 predicted: `e2e-works` is present in the byte stream either way, so a
118 marker grep sees a healthy session. Same bytes, wrong picture.
119
120 **Row 3 — the missing clear survives longer than expected.** Dropping
121 `\x1b[2J` does NOT fire at `base attach`; a full repaint over an empty
122 or already-correct screen is indistinguishable from a correct one. It
123 fires at `quic epoch resync`, the first scenario where a repaint lands
124 on a screen that already holds different content: the client's grid
125 keeps `quic-attach-ok`, `quic-reattached`, `quic-pre-tear` and
126 `quic-pre-restart` under the post-restart lines the daemon's grid ends
127 at. Plain diff again. The lesson for the harness is that convergence
128 coverage is only as good as the state the scenario leaves on screen
129 before the repaint — the check that caught this is the one whose
130 fixture had something to clear.
131
132 **Row 4 — both suites missed, and the reason is fixture data, not the
133 check.** The mutation does fire: delta rows really are built with a
134 leading `\x1b[0m` (pinned by the `paintDeltaClipped` unit test, which
135 asserts `\x1b[4;1H\x1b[2K\x1b[0mfits`), so the client really did strip
136 four bytes off every row it painted. But no scenario in either suite
137 ever emits a styled byte — no `tput`, no colour SGR, nothing that
138 leaves a live attribute at end of row. With no live style upstream, a
139 suppressed reset resets nothing and the styled leg has no divergence to
140 find. This is a gap in the CORPUS, not in the assertion: the styled
141 (`--vt`, byte-exact) comparison is the right instrument and is never
142 handed a specimen. Resolution per the recording rule: land a scenario
143 that emits styled output spanning two delta rows and re-run this
144 mutation to prove the styled leg fires. Until that lands the row stays
145 `pending`.
146
147 **Row 5 — both suites missed, and this one is honest to accept.** The
148 `?2026` wrapper is a flicker property: it changes WHEN the terminal
149 shows the paint, never WHAT it shows. The convergence harness replays
150 the client's stream into a grid, and a grid has no concept of tearing,
151 so no diff can exist by construction. Recorded expectation confirmed.
152 One correction to the going assumption, though: this is NOT unit-pinned
153 on the delta path. `src/client.zig` pins `?2026h` in two unit tests, at
154 `renderClipped` and at the prediction overlay, but the only
155 `paintDeltaClipped` unit test asserts row clipping and cursor clamping
156 and says nothing about the wrapper. So the delta path's synchronized
157 update is currently unpinned at every layer. Cheapest resolution is a
158 unit assertion at `paintDeltaClipped`, not an e2e one — e2e is the
159 wrong instrument for a property no grid can observe.
160
161 **Rows 6-10 (replica sync and the prediction overlay).** All five built
162 on the first attempt. No suite run was re-run: every verdict is a
163 first-attempt result, and each failure landed in a scenario the
164 mutation plausibly explains, so there are no flake attributions. One
165 mutation (row 8) had to be reworked before it could be written at all;
166 that is recorded under its own heading below rather than as a footnote,
167 because the reason it could not be written as specified is itself the
168 finding.
169
170 **Row 6 — the two suites catch the same break at different places, by
171 different organs, and the new one gets there first.** Removing
172 `replica.feed(composed.bytes)` from the delta arm leaves the *paint*
173 entirely correct: the screen is drawn from the delta payload, which the
174 mutation does not touch, so every scenario that only ever paints looks
175 healthy. What rots is the replica, and it surfaces the moment something
176 *reads* it. The new suite fails at `quic delta resume` (test/e2e.sh:850)
177 — a resume repaints from the replica via `renderClipped`, and the
178 replica has not seen a delta since the last snapshot, so the client's
179 grid is missing precisely the delta-only lines:
180
181 ```
182 sh-5.3$ printf "quic-%s\n" reattached
183 quic-reattached
184 -sh-5.3$ printf "quic-%s\n" pre-tear
185 -quic-pre-tear
186 ```
187
188 Plain leg, and a clean statement of what the mutation deleted. The old
189 suite gets there too, but 372 lines later and by a different route: at
190 `line mode` (test/e2e.sh:1222) it reports `contradicted=1, want 0`,
191 because reconcile judges the overlay against a replica that never
192 advances, so a correctly predicted keystroke goes unanswered until the
193 expiry bound abandons the queue. Both columns say "caught", and the row
194 is honest about that — but the mechanisms are not interchangeable. The
195 counter catch depends on a prediction scenario existing downstream of
196 the damage; the convergence catch reads the corrupted state directly,
197 at the first scenario that repaints from it.
198
199 **Row 7 — pending, and the branch is not merely unobserved but
200 unreachable.** The mutation deletes `replica.resize(prefix.cols,
201 prefix.rows)` from the snapshot arm. Both suites pass, which was the
202 recorded expectation, and the reason is stronger than "no scenario
203 resizes": the mutated statement is never executed at all. It sits under
204 `if (prefix.cols != grid.cols or prefix.rows != grid.rows)`, and in the
205 suite that condition is never true. Every e2e client captures stdout to
206 a file, so `ttySize` returns null (`src/client.zig:917` — `isatty`
207 fails) and the client runs at its non-tty default of 80x24
208 (`src/client.zig:478`), which is also the daemon's. `test/rawmode.zig`
209 does not change this: it is a shell stand-in on the daemon side, not a
210 pty allocated for the client. So `grid` starts at 80x24, every snapshot
211 prefix says 80x24, and the branch is dead code for the whole run.
212 Resolution per the recording rule: this is a banked gap with a written
213 reason — there is no path to a non-80x24 grid anywhere in the corpus,
214 and closing it needs a scenario that actually resizes (a client on a pty
215 plus a `resize` frame), not a new assertion. Until such a scenario
216 exists, no assertion of any kind could catch this mutation, so it is not
217 evidence about the convergence checks either way.
218
219 **Row 8 — reworked, because the check the mutation was supposed to
220 delete does not exist.** The specification was "remove the client's
221 gap-detection so a stale delta is applied rather than triggering
222 resync". There is no such code. The client has no seq-continuity check
223 anywhere: the delta arm sets `last_seq = composed.header.seq` and
224 applies the frame unconditionally, and every gap decision is made by the
225 DAEMON, in `sendResync` (`src/server.zig:1436`), which serves a delta
226 only when the client's `have_seq` names this epoch and falls inside
227 `[reset_seq, seq]`. Removing a check that is not there is not a
228 mutation, so the same break was expressed from the one client-side lever
229 that exists: the resume attach quotes `last_seq +| 1`, claiming one seq
230 more than the client actually holds.
231
232 What that produced is worth more than what was asked for. The intended
233 desync never happened, because the daemon's range guard noticed the lie
234 (`have_seq <= self.tracker.seq` fails when the daemon has not yet moved
235 past the claimed seq) and served a SNAPSHOT — the safe answer. The
236 screen therefore renders perfectly, all 21 convergence points agree, and
237 the only witness is the counter: both suites fail identically at
238 `reconnect was served a snapshot (7 -> 8), not a delta`. This is the
239 exact complement of rows 1-3. There, convergence saw what counters were
240 blind to; here, a resume served the wrong WAY renders identically
241 either way, and the counter is the only organ that can tell. Recorded in
242 `docs/decisions.md` already, under "counters are asserted because
243 markers are blind to *how* a resume was served" — this run is a fourth
244 proof of it, arrived at from the client side.
245
246 The residual finding: a delta silently applied across a gap is not
247 reachable from the client at all. Understating `have_seq` yields a
248 superset delta (correct); overstating it yields a snapshot (correct);
249 and the malformed-delta arm, which is the client's only resync REQUEST,
250 never fires because no e2e payload is ever malformed. The property is
251 defended entirely by the daemon, and it is already mutation-proven
252 there. No new assertion is owed.
253
254 **Row 9 — the styled leg fires, alone, and this is the check earning its
255 keep.** Confirmed predictions are retired into `overlay.retired` so the
256 authoritative row paint can take their underline off; the mutation
257 repaints `\x1b[4m` over every retired cell instead, leaving confirmation
258 visible forever. Counters are deliberately untouched — `markPainted` is
259 not called for retired cells, so `made`, `displayed` and `confirmed` all
260 read exactly as they do unmutated — and the old suite duly passes.
261
262 The new suite fails at `line-mode prediction`, and the attribution is as
263 sharp as this campaign gets. The normalized grids are byte-identical;
264 the styled grids are two bytes against fourteen:
265
266 ```
267 daemon (.dvt): z
268 client (.rvt): ^[[0m^[[4mz^[[0m
269 ```
270
271 Same character, same cell, same plain grid — the client is simply still
272 calling it a prediction. This is the live test of M9's
273 overlay-never-becomes-state invariant, and it is caught by exactly one
274 of the four legs the harness owns. Note the contrast with row 4, which
275 also needed the styled leg and got nothing: there the style had to come
276 from the fixture, and no scenario emits one. Here the client's own
277 overlay supplies the specimen, so the styled comparison finally has
278 something to compare. The lesson is not "styled diffing is unreliable" —
279 it is that a byte-exact check needs a source of bytes, and prediction is
280 the one part of the client that generates its own.
281
282 **Row 10 — the plain leg, at raw mode, and the counters cannot see it.**
283 Feeding the predicted byte into the replica is the violation M9's design
284 names outright, and it fails at `raw mode`: daemon grid `abc`, client
285 grid `abcj`. Plain diff, so the styled leg never ran (the harness stops
286 at the first divergence and leaves no `.dvt`/`.rvt` behind).
287
288 Why the old suite survives is the instructive half. Raw mode's swallow
289 phase is chosen precisely because the application answers nothing, so
290 the daemon sends no frames — which means `reconcile` is never called and
291 the self-confirmation the mutation would otherwise cause never gets the
292 chance to happen. The idle `expire` path fires instead, exactly as it
293 does unmutated, and every counter reads normally. The overlay's own
294 glyph is then rolled back by the expiry repaint, as designed. What
295 cannot be rolled back is the byte that went into the replica: an overlay
296 is erasable and state is not, which is the entire reason the invariant
297 exists. The counters see a textbook expiry; the grid sees a permanent
298 `j`.
299
300 **What rows 6-10 measured.** Two demonstrated (9, 10 — new caught, old
301 survived), one both-caught by different organs at different scenarios
302 (6), one caught by counters in both columns (8, and only because the
303 daemon defended the break the client tried to cause), one pending on an
304 unreachable branch (7). Combined with rows 1-5, the convergence checks
305 have now caught five mutations the old suite passes (1, 2, 3, 9, 10),
306 and the three both suites miss separate cleanly by cause: row 4 is the
307 right instrument handed no specimen and is resolvable by landing a
308 styled scenario; row 5 is a property no grid can represent and belongs
309 to a unit test; row 7 is a branch no scenario executes and needs a
310 resizing client before any assertion could speak. Only the first of the
311 three is a debt the convergence checks themselves can pay.
312
313 **Rows 11-16 (daemon side: delta production, seq/epoch).** All six built
314 on the first attempt with the pinned toolchain. No suite run was re-run:
315 every verdict below is a first-attempt result, and each failure landed
316 in a scenario the mutation plainly reaches, so there are no flake
317 attributions to make. One planned mutation had to be widened to reach
318 its target and is recorded under row 15.
319
320 **The shape of this half.** Rows 1-10 mutated the client; these six
321 mutate the daemon, and the difference shows up as a change in which
322 organ speaks first. Client-side breaks were mostly invisible to the
323 counters, because the counters live on the client and a client that
324 lies to the screen still counts honestly. Daemon-side breaks are the
325 opposite: the daemon is upstream of both the client's replica AND the
326 client's prediction bookkeeping, so a broken daemon usually trips a
327 counter somewhere even when nothing was built to watch for it. Five of
328 these six are caught by the old suite. That is not the convergence
329 checks failing to earn their keep — it is the honest observation that
330 the old suite's blind spot was the client, not the server.
331
332 **Rows 11 and 13 — the same catch from both suites, but only one of
333 them says what is wrong.** Both mutations withhold a row that really
334 changed: row 11 drops the highest-numbered dirty row from every payload
335 (from the header count and the row list together, so the payload stays
336 valid and `composeDelta`'s count check has nothing to object to), row 13
337 swallows every change to row 0 by never stamping it in `update()`.
338
339 Both suites fail on both. But the old suite fails at `line mode:
340 contradicted=1, want 0` — the eleventh scenario in, and by a prediction
341 counter — while the new suite fails at `base attach`, the very first
342 scenario, and prints the missing content:
343
344 ```
345 row 11: -sh-5.3$ printf "e2e-%s\n" works
346 -e2e-works
347 sh-5.3$
348 +e2e-works
349
350 row 13: -sh-5.3$ printf "e2e-%s\n" works
351 +sh-5.3$
352 ```
353
354 Row 13's diff is the whole diagnosis in two lines: one row, the first
355 one, holding the prompt and the echoed command on the daemon and only
356 the bare prompt on the client. The old suite's verdict is true but
357 oblique — a prediction was contradicted because the row that would have
358 confirmed it never arrived, which is a symptom three inferential steps
359 from the cause. Same disposition, very different debugging cost, and
360 the gap is eight scenarios of runtime plus the entire distance between
361 "a counter is off" and "this row is missing".
362
363 Worth naming why the prediction counter is the old suite's organ here
364 at all: the shell prompt is where typing happens, so the row a
365 prediction is judged against is exactly the row these mutations
366 withhold. Row 11 withholds it because the prompt is the highest dirty
367 row; row 13 because a fresh shell's prompt is row 0. The counter is not
368 watching for lost rows. It is watching for lost predictions, and lost
369 rows happen to eat predictions.
370
371 **Row 12 — pending, and the reason is that row order is not a state
372 property.** Reversing the row order inside each delta payload changes
373 nothing either suite can see, including all 21 convergence points.
374 This was the honest expectation written before the run, and it is
375 confirmed twice over — once by both suites passing, and once by
376 reading the code that would have to object.
377
378 Nothing objects. `composeDelta` emits `CUP(row+1);1H` + `EL(2)` + the
379 row's bytes for every row it iterates, and its only validation is
380 `seen != hdr.row_count`. Every row therefore paints at its own absolute
381 address, independent of every other row, so the payload is a set and
382 not a sequence: permuting it is a no-op on the resulting grid by
383 construction. The final CUP to the header's cursor position is emitted
384 after the loop and is unaffected by the loop's order.
385
386 This is a genuine gap in the harness, but it is not a gap worth
387 closing with a grid check, because no grid can hold the evidence. If
388 row order ever becomes load-bearing — a relative-addressing delta
389 format, a scroll-region optimization — it becomes a protocol property
390 and belongs to a `composeDelta` unit test asserting on the composed
391 byte string, not to an e2e convergence point. Filed alongside row 5 as
392 "correctly unmeasurable here", as distinct from row 4's "measurable but
393 unmeasured".
394
395 **Row 14 — the grid converges perfectly and the seq is still wrong.**
396 Making every second update reuse its predecessor's seq (a `reuse_seq`
397 toggle flipped on each advance) leaves the delta CONTENT correct: with
398 the seq unadvanced, `buildDeltaSince(seq - 1)` selects the rows stamped
399 at the current seq, which is this update's rows plus the previous
400 update's, so every client receives a superset of what it needed. All 21
401 convergence points pass. Both suites then fail at exactly the same
402 place, `raw mode: displayed=1, want 2`, with the full counter line:
403
404 ```
405 predict made=4 displayed=1 confirmed=3 contradicted=1 expired=1 abandoned=1 suppressed=1
406 ```
407
408 The mechanism runs through `reconcile`'s first guard, `if (p.made_seq
409 >= applied_seq) continue` — "too new to be evidence about". A
410 prediction made while the client holds seq N is stamped `made_seq = N`;
411 the next frame legitimately carries N+1 and judges it. Under the
412 mutation every second frame carries N again, so the guard defers a
413 prediction the frame could in fact have judged. Deferred judgement
414 means a deferred `confirmed`, which means a streak that lengthens a
415 frame late, which means `.adaptive` promotion to `confident` arrives
416 late — and `displayed` only increments for predictions painted while
417 confident. The lost display is not a lost prediction; it is a
418 prediction that was still invisible when its moment passed.
419
420 This is the cleanest instance in the campaign of a break the
421 convergence checks structurally cannot see. A seq is not a cell. The
422 replica ends in exactly the right state and the screen shows exactly
423 the right bytes; what is wrong is the numbering that tells the client
424 WHEN it is allowed to believe things. Row 12 and row 14 are the same
425 lesson from opposite ends: row 12 is a wrong wire ordering with no
426 consequence, row 14 is a wrong wire numbering with a consequence no
427 grid can express.
428
429 **Row 15 — the control, and the old suite catches precisely what it was
430 built to catch.** Both suites fail identically, at the M7b restart
431 scenario, with the message the campaign brief predicted verbatim:
432 `restarted daemon served no snapshot (0); the stale seq was honoured`.
433
434 The mutation had to be widened, and the discrepancy is worth recording.
435 The plan asked for a delta served "when the client's quoted have_seq is
436 below the tracker's floor (or from a dead epoch)", which implies
437 dropping two of `sendResync`'s four guards — `have_epoch == self.epoch`
438 and `have_seq >= self.tracker.reset_seq`. Dropping only those does not
439 reach the restart scenario at all, because a RESTARTED daemon's tracker
440 sits at a very low seq while the client quotes a high one from the dead
441 instance, so the surviving upper bound `have_seq <= self.tracker.seq`
442 sends it to a snapshot anyway and the mutation is inert. Expressing the
443 intended break therefore meant reducing the guard to `have_seq != 0 and
444 self.tracker.rows != 0` — "serve a delta to anyone quoting any nonzero
445 seq". The daemon then answers the reconnect with a delta whose
446 `since` exceeds every row seq it holds: an empty, structurally valid,
447 completely useless payload.
448
449 What the two suites see is identical, and the scenario's own comment
450 explains why the markers stay silent:
451
452 > Markers are blind to the kind of resume — a broken epoch fence handing
453 > back a garbage delta would render "m7b-post-restart" just as well.
454 > Only the counter sees it.
455
456 The grid-content assertion in the same scenario greps `muxd dump`, i.e.
457 the DAEMON's grid, which never held the pre-restart marker no matter how
458 the resume was served. It cannot fire. The snapshot counter is the sole
459 witness, it was written to be the sole witness, and it is. This row is
460 the campaign's evidence that the old suite is not being graded on a
461 curve: where it has teeth, they close on the first attempt.
462
463 **Row 16 — the same fence from the other side, and the same verdict.**
464 Stamping `epoch + 1` into every snapshot prefix (the daemon's real
465 `self.epoch` untouched) poisons the identity the client echoes back:
466 the client stores `prefix.epoch`, quotes it on reattach, and
467 `sendResync` compares it against the true `self.epoch`, which now never
468 matches. Every resume that should have been a delta becomes a snapshot.
469 Both suites fail identically at the M7 delta-resume scenario:
470 `reconnect was served a snapshot (7 -> 8), not a delta`.
471
472 Rows 15 and 16 are exact mirror images and it is worth stating the pair
473 together, because between them they pin the fence from both directions.
474 Row 15 removes the fence, so a resync that is owed a snapshot gets a
475 delta; row 16 corrupts the identity the fence checks, so a resync that
476 is owed a delta gets a snapshot. Neither disturbs a single pixel — row
477 16's snapshots are perfectly correct snapshots, merely unearned — which
478 is why all 21 convergence points pass under both and the counters are
479 the only organ that speaks. It also explains the identical failure
480 string shared with row 8: row 8 broke the same fence from the client's
481 side by quoting one seq too many. Three different mutations, one
482 mechanism, one assertion, and the assertion holds each time.
483
484 **What rows 11-16 measured.** Zero demonstrated, two both-caught (11,
485 13), three caught by counters in both columns (14, 15, 16), one pending
486 (12). Read against rows 1-10 this is a clean inversion and the reason is
487 structural, not accidental: the convergence checks compare a client's
488 rendering against a daemon's grid, so they are an instrument pointed at
489 the client. Aim a mutation at the daemon and the daemon's own
490 counters — snapshots, deltas, and the M9 prediction stats the client
491 derives from the daemon's seq stream — are already watching, in both
492 suites equally. The new suite still wins rows 11 and 13 decisively on
493 diagnosis rather than detection: same disposition, eight scenarios
494 earlier, with the offending row printed instead of inferred.
495
496 **Campaign totals across all 16 rows.** Five demonstrated (1, 2, 3, 9,
497 10 — new caught, old survived), three both-caught (6, 11, 13), four
498 caught by counters in both columns (8, 14, 15, 16), four pending (4, 5,
499 7, 12). The four pending separate cleanly by cause, and only one is a
500 debt the convergence checks themselves can pay: row 4 is the right
501 instrument handed no specimen and is fixed by landing a styled
502 scenario. Row 5 (synchronized-update wrapper), row 7 (a resize branch
503 no scenario executes) and row 12 (row order under absolute addressing)
504 are not convergence-check debts at all — they are properties a rendered
505 grid cannot represent, and each belongs to a unit test on the byte
506 string or to a scenario that does not yet exist.
507
508 **Row 17 — the clip bound, one row short.** The client paints through
509 two bounded loops: `renderClipped`'s `limit = @min(grid_rows,
510 tty.rows)` for a full repaint, and `paintDeltaClipped`'s `if (row.row
511 >= tty.rows) continue` for a delta. Both were clamped one row short
512 (`tty.rows -| 1`), so on the suites' 80x24 clients row 24 is structurally
513 unpaintable while the replica and the daemon both still hold it. Two
514 lines, one file, one idea — a single-line edit would have been half a
515 mutation, because either loop alone leaves the other painting the row.
516
517 The old suite survives it and the new suite fails at the FIRST scenario
518 after the control, `reattach after kill`, on the plain arm of the
519 convergence check, with the missing row printed:
520
521 ```
522 --- .../re.dump.n
523 +++ .../re.render.n
524 @@ -21,4 +21,3 @@
525 58
526 59
527 60
528 -sh-5.3$
529 ```
530
531 That diff is the whole argument for the new suite in four lines. The
532 scenario is the same one in both suites — `seq 1 60` into a scrolled
533 shell, kill the client, reattach — and the old suite runs it too. What
534 the old suite asks is whether the marker text came back, and the markers
535 are the numbers, which live in rows 1..23 and came back perfectly. The
536 one thing that ends up on row 24 of a screen that has just scrolled is
537 the prompt, and no marker grep was ever written against a prompt. So the
538 old suite watched the exact byte stream that carried the bug and had
539 nothing to say about it. Worth recording as the general shape: a marker
540 grep is a test of the rows a fixture happens to write text into, and the
541 bottom row is precisely where a scrolled session puts the thing the
542 fixture did not write.
543
544 The unit layer is a near miss, and instructively so. The clip tests at
545 `src/client.zig` "renderClipped paints only rows that fit and clamps the
546 cursor" assert the two NEGATIVE facts — no `\x1b[25;`, no "bottom row"
547 from a 30-row grid — and one cursor fact, `\x1b[24;80H`, which survives
548 untouched because `clampCursor` was not mutated. Nothing there asserts
549 that row 24 IS painted. The tests pin the ceiling and leave the floor
550 open, so this mutation walks under them. Both of the campaign's
551 instruments for this bug therefore agree on the same lesson from
552 opposite directions: the new suite catches it only because a convergence
553 check compares whole grids rather than chosen rows.
554
555 **Row 18 — the resync scroll reset, unreachable and unpinned.** The
556 mutation deletes `scroll_pages = 0` from the reconnect path in
557 `fn session` (`src/client.zig` ~609), so a client that reconnects while
558 paged into history stays in scroll view instead of returning to live.
559 Both suites survive, and the reason is not that the assertions are weak
560 but that no e2e client can enter the state at all.
561
562 The evidence is exhaustive rather than argued. `scroll_pages` is a local
563 in `session()`, initialised to 0, and the only site that raises it is the
564 Shift+PageUp branch, which requires stdin to deliver exactly `\x1b[5;2~`
565 in one read AND `history_rows > 0`. Neither suite contains that byte
566 sequence anywhere: `grep -c $'5;2~' test/e2e.sh e2e-old.sh` returns 0 and
567 0. So at the mutation site `scroll_pages` is invariantly 0 in every e2e
568 run ever executed, and deleting an assignment of 0 to a variable already
569 0 is a semantic no-op. This is not "the suites missed it"; there is no
570 behavioural difference to miss. Disposition `pending`, cause: unreachable
571 fixture, not absent assertion.
572
573 The unit layer does not cover it either, which is the part worth acting
574 on. `session()` has exactly one caller and no test drives it — the run
575 loop is exercised only end-to-end — so the reset is pinned nowhere in the
576 tree. The nearest pins are adjacent but distinct: `src/predict.zig`
577 (~1128-1140) pins that `setScrollMode` flushes the overlay and that
578 `predictAt` suppresses while scrolled, and `src/client.zig` pins
579 `renderScrollback`'s paint and `scrollPageRange`'s arithmetic. All three
580 are about scroll mode; none is about leaving it. Closing this needs a
581 scenario that pages back and then forces a reconnect, which in turn needs
582 a pty-driving client fixture (`rawmode` is the existing precedent) — the
583 same missing-fixture debt as row 5, not a convergence-check debt.
584
585 One genuine finding fell out of reading for this row, and it is in
586 unmutated HEAD, not in the mutant. The resync path clears `scroll_pages`
587 but never calls `overlay.setScrollMode(false)`, and `overlay.flush()`
588 does not clear `scroll_mode` — it only drops pending and retired
589 predictions. So after a reconnect taken while scrolled, the client is
590 back on live rows with `scroll_mode` still true, and since the "any other
591 key exits scroll mode" branch is guarded by `scroll_pages > 0` it can
592 never run again, `predictAt` suppresses unconditionally, and local echo
593 prediction is dead for the remainder of the session. It is invisible to
594 both suites for exactly the reason row 18 is: no e2e client can page
595 back. Recorded here, deliberately unfixed — this campaign commits no code
596 — and it belongs with the fixture debt above, since the same pty fixture
597 that would pin the reset would expose this.
598
599 **Campaign totals across all 18 rows.** Six demonstrated (1, 2, 3, 9,
600 10, 17 — new caught, old survived), three both-caught (6, 11, 13), four
601 caught by counters in both columns (8, 14, 15, 16), five pending (4, 5,
602 7, 12, 18). The last row of the table is also the campaign's cleanest
603 single result: row 17 is a real client-side rendering bug that the old
604 suite runs the fixture for and cannot see, and the new suite reports at
605 the first scenario with the offending line printed. The five pending
606 rows now separate into three causes, none of which is a weak convergence
607 check: one specimen the checks want and do not have (row 4, styled
608 scenario), two properties a rendered grid cannot represent (rows 5 and
609 12), and two branches no fixture can reach (rows 7 and 18), both of which
610 want a pty-driving client fixture rather than a new assertion.
611
612 **Worktree retired.** The campaign worktree
613 `/home/xanderle/code/rad/mux-m11-campaign` (detached at 2bc64e8, tracked
614 diff empty at teardown) was removed after row 18. Its one piece of
615 scaffolding, `e2e-old.sh` — the pre-M11 suite, carried untracked so that
616 each row could be graded against both — was deleted with it; sha256
617 `aa04ecbebfa2f2dbc425cb651b543ebb995fe30dc45393fc5b797bd004ea1c27`, for
618 anyone reconstructing an "old suite" column later.
619
620 ## Dispositions
621
622 The recording rule allows two endings and no third: a NEW assertion
623 landed now, with the mutation re-run to prove it fires, or a banked
624 entry with a written reason. All five pending rows are closed below.
625 The mutations were re-applied in the MAIN checkout, one at a time,
626 apply-run-revert, with `git diff` verified between each — the campaign
627 worktree is gone, and an in-checkout gate is the repo's established
628 pattern for proving a new assertion fires.
629
630 **Row 4 — banked, and the campaign's own prescription was wrong.** The
631 prescription was a styled scenario, on the reading that the styled leg
632 was the right instrument handed no specimen. The scenario landed
633 (`styled content`, test/e2e.sh, a session printing two bold-red rows
634 followed by a plain prompt row) and the mutation was re-run against it.
635 It still survives. The reading was wrong, and the reason is a contract
636 nobody had checked: ghostty's VT formatter closes every styled row it
637 emits. `dumpVtRow` returns `\x1b[0m` (the prefix `formatSelection` adds)
638 plus the formatter's own output, and for a styled row that output both
639 opens with a reset and ends with one — ghostty's test suite spells out
640 why, "style is reset before newline to prevent background colors from"
641 bleeding. Verified on the wire, not inferred: the delta row for
642 `styled-red` paints as `\x1b[2K\x1b[0m` `\x1b[0m\x1b[1m\x1b[38;5;1m`
643 `styled-red\x1b[0m`, two resets, and the mutation strips only ours. A
644 full-width background fill and a styled line wrapped across two rows
645 were both tried; both close their pen the same way.
646
647 So the leading reset is redundancy against a live pen, and no live pen
648 exists. Every byte source the client paints from closes itself: the
649 formatter's rows, `paintOverlay`'s per-cell `\x1b[4m{c}\x1b[0m`,
650 `bannerText`'s `\x1b[7m{s}\x1b[0m`. The only pen the reset could
651 protect against is one the HOST terminal had already set before mux
652 started, which is exactly the thing no rendered-grid comparison can
653 ever hold — the render helper replays into a fresh engine at the
654 default pen, by construction. This is not a corpus gap; it is a
655 property that is invisible to the harness for the same structural
656 reason as rows 5 and 12, and it moves to that pile.
657
658 It is also already pinned where it lives. Re-running mutation 4 against
659 `make test` fails on the assertion that has been there since the paint
660 landed:
661
662 ```
663 error: 'client.test.paintDeltaClipped skips rows beyond the tty and clamps the cursor' failed
664 src/client.zig:1837: try std.testing.expect(std.mem.indexOf(u8, out[0..n], "\x1b[4;1H\x1b[2K\x1b[0mfits") != null);
665 Build Summary: 196/197 tests passed; 1 failed
666 ```
667
668 No new assertion is owed, and adding one that says what line 1837
669 already says would be theatre.
670
671 **Row 19 — the styled scenario stays, and here is what it is worth.** A
672 scenario landed on a disproved hypothesis has to justify itself on its
673 own terms or come back out, so it was gated by a mutation aimed at what
674 it actually covers: strip EVERY SGR run from delta rows at the client
675 paint, not merely the leading reset. Glyphs unchanged, colours gone.
676 The new scenario is the first convergence point to fail, on the styled
677 leg alone:
678
679 ```
680 e2e FAIL: styled content: client render diverges from daemon grid (-daemon +client):
681 (plain diff empty — 0 bytes)
682 daemon (.dvt): ^[[0m^[[1m^[[38;5;1mstyled-red^[[0m
683 client (.rvt): styled-red
684 ```
685
686 Before this, the byte-exact leg had exactly one source of specimens in
687 the whole corpus — the prediction overlay's own underline (row 9). Now
688 the fixture supplies one too, which is what puts daemon-side style
689 emission and the delta path's style transport under the check at all.
690 The old-suite column is honestly blank: `e2e-old.sh` went with the
691 worktree, and a marker grep cannot see a colour by construction — the
692 text is identical under this mutation.
693
694 **Row 5 — new assertion, at the unit layer, gated.** `paintDeltaClipped
695 brackets the whole paint in one synchronized update` (src/client.zig)
696 asserts the delta paint STARTS with `\x1b[?2026h\x1b[?25l` and ENDS with
697 `\x1b[?25h\x1b[?2026l` — on the ends rather than by substring search,
698 because the property is that every row lands inside the brackets, which
699 only position can show. e2e is the wrong instrument here and always was:
700 a synchronized update changes when the terminal shows a paint, never
701 what it shows. Re-applying mutation 5 (wrapper dropped from the delta
702 path) fails exactly this test and nothing else:
703
704 ```
705 error: 'client.test.paintDeltaClipped brackets the whole paint in one synchronized update' failed
706 src/client.zig:1868
707 Build Summary: 197/198 tests passed; 1 failed
708 ```
709
710 **Row 7 — banked: needs a resizing client fixture.** The branch is
711 unreachable, as row 7's note established, and the question left open was
712 whether the snapshot-apply resize is drivable from a unit test instead.
713 It is not, without a refactor this milestone should not make. The
714 mutated statement sits inline in `session()`'s frame switch — read the
715 prefix, compare against `grid`, `replica.resize`, restamp `grid` — and
716 `session()` takes a live transport, two file descriptors and a run loop.
717 There is no `applySnapshot` seam to call, `session()` has one caller and
718 no test drives it, and `Engine.resize` is already tested on its own, so
719 a test of the callee could not catch a deleted call. Banked with row 18:
720 same debt class, a fixture that can drive a client at a size other than
721 80x24, which today means a pty. Note the asymmetry with row 4 — that one
722 is invisible to the instrument; this one is invisible to the corpus.
723
724 **Row 12 — banked: order is not a state property.** Rows are absolutely
725 addressed — `composeDelta` emits `CUP(row+1);1H` + `EL(2)` + the row's
726 bytes for each row it iterates — so the payload is a set by
727 construction and permuting it is a no-op on the resulting grid.
728 Asserting an emission order would forbid an equivalent implementation
729 while pinning nothing a user can observe, which is the wrong trade for
730 a wire format that is still moving. Structural corruption is already
731 rejected: `composeDelta` validates `seen != hdr.row_count`. If row order
732 ever becomes load-bearing — relative addressing, a scroll-region
733 optimization — it becomes a protocol property and belongs to a
734 `composeDelta` unit test on the composed byte string.
735
736 **Row 18 — banked: needs a pty-driving client fixture.** No e2e client
737 can enter scroll mode, because entering it requires stdin to deliver
738 `\x1b[5;2~` in one read and no suite contains those bytes; with
739 `scroll_pages` invariantly 0 the mutation is a semantic no-op. Closing
740 it needs a scenario that pages back and then forces a reconnect, which
741 needs a client on a pty (`rawmode` is the precedent, but it stands in
742 for the SHELL, on the daemon side). Banked together with row 7 as one
743 debt: a client fixture that owns a pty, which would make both branches
744 reachable and both mutations gradeable.
745
746 **The bug the campaign found, now fixed.** Row 18's note recorded a
747 genuine defect in unmutated HEAD and deliberately left it alone, since a
748 campaign commits no code. It is fixed here. The reconnect path in
749 `session()` cleared `scroll_pages` but never told the overlay, and
750 `flush()` does not clear `scroll_mode` — it drops pending and retired
751 predictions and nothing else. A reconnect taken while scrolled therefore
752 left the client painting live rows with the overlay still suppressing,
753 and the "any other key leaves scroll mode" branch could never rescue it
754 because that branch is guarded by `scroll_pages > 0`, which the
755 reconnect had just made false. Local echo prediction was off for the
756 rest of the session, silently. The fix is one call,
757 `overlay.setScrollMode(false)`, next to the existing `scroll_pages = 0`.
758
759 Client-side deliberately, rather than making `flush()` clear the mode
760 itself: every snapshot flushes, and a snapshot is not a reason to leave
761 history — the overlay would start deciding where the viewport is, which
762 is the client's business. The counters do not move either way, because
763 `setScrollMode` is a no-op when the bit is already false and its flush
764 would otherwise abandon exactly the predictions the flush ten lines
765 later abandons.
766
767 Tested where the property can be reached: `a flush leaves scroll mode
768 exactly where it found it` (src/predict.zig) pins the contract the fix
769 depends on in both directions — a flush taken while scrolled still
770 suppresses, a flush taken live still predicts. The end-to-end pin, "a
771 reconnect while scrolled leaves prediction working", rides the banked
772 pty-fixture debt above and is not claimed here.
773
774 **Where the five rows ended.** One new assertion (row 5, unit), four
775 banked (4, 7, 12, 18), zero undecided. The banks fall in two classes and
776 neither is a weak convergence check: rows 4, 5 and 12 are properties no
777 rendered grid can represent, and rows 7 and 18 are branches no fixture
778 can reach. Row 4 changed class during resolution — it was filed as the
779 one debt the convergence checks could pay, and paying it disproved the
780 premise. One product bug fixed, one scenario added, pins moved to 10
781 scenario checkpoints and 22 convergence points.
docs/superpowers/plans/2026-08-09-m11-e2e-hardening.md
Old New
@@ -1,5 +1,7 @@
1 # M11: e2e Hardening Implementation Plan 1 # M11: e2e Hardening Implementation Plan
2 2
3 **Executed in full; verdicts and deviations recorded in decisions.md M11.**
4
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. 5 > **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 6
5 **Goal:** the e2e suite asserts "the screen the client painted equals the screen the daemon holds" (render-vs-dump convergence), pins its own scenario count, gains a soak mode — then a mutation campaign breaks the code deliberately and measures what the new suite catches that the old one could not. 7 **Goal:** the e2e suite asserts "the screen the client painted equals the screen the daemon holds" (render-vs-dump convergence), pins its own scenario count, gains a soak mode — then a mutation campaign breaks the code deliberately and measures what the new suite catches that the old one could not.
@@ -42,7 +44,7 @@
42 - Create: `test/render.zig` 44 - Create: `test/render.zig`
43 - Modify: `build.zig` (module + exe + test loop + e2e arg) 45 - Modify: `build.zig` (module + exe + test loop + e2e arg)
44 46
45 - [ ] **Step 1: Write the file — core function, main, and failing tests together** (the tests fail because the file is new; TDD's red step here is the whole file compiling and its tests being exercised by the build) 47 - [x] **Step 1: Write the file — core function, main, and failing tests together** (the tests fail because the file is new; TDD's red step here is the whole file compiling and its tests being exercised by the build)
46 48
47 ```zig 49 ```zig
48 //! e2e render helper: replays a captured mux-client stdout stream through 50 //! e2e render helper: replays a captured mux-client stdout stream through
@@ -190,7 +192,7 @@ test "render: styled state survives replay identically to a direct feed" {
190 } 192 }
191 ``` 193 ```
192 194
193 - [ ] **Step 2: Wire into build.zig.** Four edits: 195 - [x] **Step 2: Wire into build.zig.** Four edits:
194 196
195 After the `delaypipe_mod` block (~line 208): 197 After the `delaypipe_mod` block (~line 208):
196 198
@@ -224,14 +226,14 @@ Add the 5th e2e artifact arg after `e2e.addArtifactArg(delaypipe_exe);` (~line 2
224 e2e.addArtifactArg(render_exe); 226 e2e.addArtifactArg(render_exe);
225 ``` 227 ```
226 228
227 - [ ] **Step 3: Run the tests.** `make test` — expect the four render tests to pass among the total (193 → 197). If ghostty-vt import fails, compare against how `engine_mod` gets its import — render_mod itself must NOT import ghostty-vt directly; it reaches it through `engine`. 229 - [x] **Step 3: Run the tests.** `make test` — expect the four render tests to pass among the total (193 → 197). If ghostty-vt import fails, compare against how `engine_mod` gets its import — render_mod itself must NOT import ghostty-vt directly; it reaches it through `engine`.
228 230
229 - [ ] **Step 4: Fire the mutations (M9 rule — a test whose mutation doesn't fire doesn't count).** Two, one at a time, in the main checkout, reverted immediately after each: 231 - [x] **Step 4: Fire the mutations (M9 rule — a test whose mutation doesn't fire doesn't count).** Two, one at a time, in the main checkout, reverted immediately after each:
230 1. In `feedForFinalGrid`, replace the body with `eng.feed(stream);` (drop the boundary). Run `make test`: tests 2 and 3 MUST fail ("primary junk"/"trailing" pollute the grid). Revert. 232 1. In `feedForFinalGrid`, replace the body with `eng.feed(stream);` (drop the boundary). Run `make test`: tests 2 and 3 MUST fail ("primary junk"/"trailing" pollute the grid). Revert.
231 2. Replace `lastIndexOf` with `indexOf`. Run `make test`: test 3 MUST fail (grid says "first"). Revert. 233 2. Replace `lastIndexOf` with `indexOf`. Run `make test`: test 3 MUST fail (grid says "first"). Revert.
232 Verify `git diff` is empty after the reverts. 234 Verify `git diff` is empty after the reverts.
233 235
234 - [ ] **Step 5: Commit.** 236 - [x] **Step 5: Commit.**
235 237
236 ```bash 238 ```bash
237 git add test/render.zig build.zig 239 git add test/render.zig build.zig
@@ -243,14 +245,14 @@ git commit -m "test: render helper replays a client capture into a dump-format g
243 **Files:** 245 **Files:**
244 - Modify: `test/e2e.sh` (header, helpers, base-flow placement) 246 - Modify: `test/e2e.sh` (header, helpers, base-flow placement)
245 247
246 - [ ] **Step 1: Take the 5th argument.** After `DELAYPIPE="$4"` (line ~10): 248 - [x] **Step 1: Take the 5th argument.** After `DELAYPIPE="$4"` (line ~10):
247 249
248 ```sh 250 ```sh
249 # M11 convergence: replays a client capture into a grid (test/render.zig). 251 # M11 convergence: replays a client capture into a grid (test/render.zig).
250 RENDER="$5" 252 RENDER="$5"
251 ``` 253 ```
252 254
253 - [ ] **Step 2: Add the helpers**, after the `proxy_pid()` function and before `cleanup()`: 255 - [x] **Step 2: Add the helpers**, after the `proxy_pid()` function and before `cleanup()`:
254 256
255 ```sh 257 ```sh
256 # --- M11: render-vs-dump convergence ----------------------------------- 258 # --- M11: render-vs-dump convergence -----------------------------------
@@ -297,7 +299,7 @@ assert_converged() {
297 } 299 }
298 ``` 300 ```
299 301
300 - [ ] **Step 3: First placement + the control.** After the base scenario's three checks (immediately after the `kill -0 "$DPID"` detach check, line ~216): 302 - [x] **Step 3: First placement + the control.** After the base scenario's three checks (immediately after the `kill -0 "$DPID"` detach check, line ~216):
301 303
302 ```sh 304 ```sh
303 # 4. The M11 claim itself: the screen the client painted equals the 305 # 4. The M11 claim itself: the screen the client painted equals the
@@ -319,9 +321,9 @@ echo "e2e OK: convergence control fires on a doctored stream"
319 321
320 Add `"$OUT.doctored"` and its derived names to the `cleanup()` rm list (a run failing between cp and rm must not leak them). Derived convergence files from FAILING assert_converged calls are deliberately left behind as evidence — cleanup does not chase `*.render` etc. for every capture. 322 Add `"$OUT.doctored"` and its derived names to the `cleanup()` rm list (a run failing between cp and rm must not leak them). Derived convergence files from FAILING assert_converged calls are deliberately left behind as evidence — cleanup does not chase `*.render` etc. for every capture.
321 323
322 - [ ] **Step 4: Run it.** `make e2e` — expect pass, with the new OK line. If the base convergence itself fails, STOP and diagnose from `$OUT.render`/`$OUT.dump` (see the context note: a landing failure is a candidate real defect; likely causes are a format mismatch in render.zig's trailing newline, or a genuine paint bug). Do not weaken the diff. 324 - [x] **Step 4: Run it.** `make e2e` — expect pass, with the new OK line. If the base convergence itself fails, STOP and diagnose from `$OUT.render`/`$OUT.dump` (see the context note: a landing failure is a candidate real defect; likely causes are a format mismatch in render.zig's trailing newline, or a genuine paint bug). Do not weaken the diff.
323 325
324 - [ ] **Step 5: Commit.** 326 - [x] **Step 5: Commit.**
325 327
326 ```bash 328 ```bash
327 git add test/e2e.sh 329 git add test/e2e.sh
@@ -335,7 +337,7 @@ git commit -m "test: e2e asserts render-vs-dump convergence, with a control that
335 337
336 Every capture that today mixes stderr into the client stream gets split (`2>&1` → `2> "FILE.err"`), because the replay engine would otherwise paint exit messages and predict-stats lines into the grid. The stats-reading helpers (`want_stat`, `want_stat_ge`, `predict_stat` call sites) then read the `.err` sibling. Marker greps stay on the stdout file — session output is stdout. Failure diagnostics that `cat` a capture should cat both files. 338 Every capture that today mixes stderr into the client stream gets split (`2>&1` → `2> "FILE.err"`), because the replay engine would otherwise paint exit messages and predict-stats lines into the grid. The stats-reading helpers (`want_stat`, `want_stat_ge`, `predict_stat` call sites) then read the `.err` sibling. Marker greps stay on the stdout file — session output is stdout. Failure diagnostics that `cat` a capture should cat both files.
337 339
338 - [ ] **Step 1: Transport/M10 scenarios — split and place.** Work top-to-bottom; line numbers are pre-edit anchors, verify each against content: 340 - [x] **Step 1: Transport/M10 scenarios — split and place.** Work top-to-bottom; line numbers are pre-edit anchors, verify each against content:
339 341
340 | # | Scenario (anchor) | Split | Placement (after) | 342 | # | Scenario (anchor) | Split | Placement (after) |
341 |---|---|---|---| 343 |---|---|---|---|
@@ -358,16 +360,16 @@ Every capture that today mixes stderr into the client stream gets split (`2>&1`
358 360
359 Declined, each with a one-line comment AT the site (the spec requires exceptions named in place): `--version`/keygen (no daemon), the `refuse` block (no daemon survives), `--via` death + dead-first-transport (no session), abort-during-reconnect + abort-during-handshake (never attached / reconnecting), wrong-key (refusal), doomed-child (no daemon), `$OUT.kill` (stream truncated by `kill -9` mid-paint — possibly mid-escape-sequence), `$OUT.a` (capture ends before B's marker landed; B's convergence covers this grid). 361 Declined, each with a one-line comment AT the site (the spec requires exceptions named in place): `--version`/keygen (no daemon), the `refuse` block (no daemon survives), `--via` death + dead-first-transport (no session), abort-during-reconnect + abort-during-handshake (never attached / reconnecting), wrong-key (refusal), doomed-child (no daemon), `$OUT.kill` (stream truncated by `kill -9` mid-paint — possibly mid-escape-sequence), `$OUT.a` (capture ends before B's marker landed; B's convergence covers this grid).
360 362
361 - [ ] **Step 2: Run.** `make e2e` — green before touching the prediction scenarios. Any divergence: diagnose from the evidence files; a real paint/replica defect gets its own fix commit before proceeding. 363 - [x] **Step 2: Run.** `make e2e` — green before touching the prediction scenarios. Any divergence: diagnose from the evidence files; a real paint/replica defect gets its own fix commit before proceeding.
362 364
363 - [ ] **Step 3: Commit.** 365 - [x] **Step 3: Commit.**
364 366
365 ```bash 367 ```bash
366 git add test/e2e.sh 368 git add test/e2e.sh
367 git commit -m "test: convergence on every transport scenario that ends with a live daemon" 369 git commit -m "test: convergence on every transport scenario that ends with a live daemon"
368 ``` 370 ```
369 371
370 - [ ] **Step 4: Prediction scenarios — split, retarget stats, place.** 372 - [x] **Step 4: Prediction scenarios — split, retarget stats, place.**
371 373
372 | # | Scenario | Split | Stats reads move to | Placement (after) | 374 | # | Scenario | Split | Stats reads move to | Placement (after) |
373 |---|---|---|---|---| 375 |---|---|---|---|---|
@@ -385,9 +387,9 @@ Details that must not be missed:
385 - Add every new `.err` file and `$OUT.race` to the `cleanup()` rm list. 387 - Add every new `.err` file and `$OUT.race` to the `cleanup()` rm list.
386 - Raw mode (#20) is the scenario most likely to FAIL on landing — the expired `j` phantom-glyph candidate from the context notes. If it does: that is a finding, handled as a client fix (its own commit, with the convergence check as the regression test), not a declined scenario. 388 - Raw mode (#20) is the scenario most likely to FAIL on landing — the expired `j` phantom-glyph candidate from the context notes. If it does: that is a finding, handled as a client fix (its own commit, with the convergence check as the regression test), not a declined scenario.
387 389
388 - [ ] **Step 5: Run.** `make e2e` green. 390 - [x] **Step 5: Run.** `make e2e` green.
389 391
390 - [ ] **Step 6: Commit.** 392 - [x] **Step 6: Commit.**
391 393
392 ```bash 394 ```bash
393 git add test/e2e.sh 395 git add test/e2e.sh
@@ -399,7 +401,7 @@ git commit -m "test: convergence on the prediction scenarios; stats reads move t
399 **Files:** 401 **Files:**
400 - Modify: `test/e2e.sh` 402 - Modify: `test/e2e.sh`
401 403
402 - [ ] **Step 1: Add `ok` next to the convergence helpers:** 404 - [x] **Step 1: Add `ok` next to the convergence helpers:**
403 405
404 ```sh 406 ```sh
405 # Scenario checkpoints. The suite's final line asserts the COUNT of these 407 # Scenario checkpoints. The suite's final line asserts the COUNT of these
@@ -413,9 +415,9 @@ ok() {
413 } 415 }
414 ``` 416 ```
415 417
416 - [ ] **Step 2: Convert every `echo "e2e OK: ..."` line to `ok "..."`.** There are 9 after Task 2 (the original 8 plus the convergence control). `grep -n 'e2e OK' test/e2e.sh` to find them; none may remain as bare echoes except the final summary line (next step). 418 - [x] **Step 2: Convert every `echo "e2e OK: ..."` line to `ok "..."`.** There are 9 after Task 2 (the original 8 plus the convergence control). `grep -n 'e2e OK' test/e2e.sh` to find them; none may remain as bare echoes except the final summary line (next step).
417 419
418 - [ ] **Step 3: Pin both counts at the end.** Replace the final `echo "e2e OK"` with: 420 - [x] **Step 3: Pin both counts at the end.** Replace the final `echo "e2e OK"` with:
419 421
420 ```sh 422 ```sh
421 # The pins. Literals, not variables set from counting something else — 423 # The pins. Literals, not variables set from counting something else —
@@ -436,9 +438,9 @@ echo "e2e OK (9 scenarios, 21 convergence points)"
436 438
437 The literals 9 and 21 are the expected values from Tasks 2–3; if the sweep landed a different count, set the literal to what actually runs (count the call sites) — the pin's job is to freeze reality, not this plan's guess. Record the final numbers in the commit message. 439 The literals 9 and 21 are the expected values from Tasks 2–3; if the sweep landed a different count, set the literal to what actually runs (count the call sites) — the pin's job is to freeze reality, not this plan's guess. Record the final numbers in the commit message.
438 440
439 - [ ] **Step 4: Fire the mutation.** Comment out one `ok` call (the keygen one), run `make e2e`: MUST fail with the checkpoint-pin message. Restore, comment out one `assert_converged` call, run: MUST fail with the convergence-pin message. Restore, `git diff` shows only the intended Task 4 edits. 441 - [x] **Step 4: Fire the mutation.** Comment out one `ok` call (the keygen one), run `make e2e`: MUST fail with the checkpoint-pin message. Restore, comment out one `assert_converged` call, run: MUST fail with the convergence-pin message. Restore, `git diff` shows only the intended Task 4 edits.
440 442
441 - [ ] **Step 5: Run + commit.** `make e2e` green. 443 - [x] **Step 5: Run + commit.** `make e2e` green.
442 444
443 ```bash 445 ```bash
444 git add test/e2e.sh 446 git add test/e2e.sh
@@ -451,7 +453,7 @@ git commit -m "test: pin scenario and convergence counts to literals"
451 - Create: `test/soak.sh` (mode 755) 453 - Create: `test/soak.sh` (mode 755)
452 - Modify: `build.zig`, `Makefile` 454 - Modify: `build.zig`, `Makefile`
453 455
454 - [ ] **Step 1: Write `test/soak.sh`:** 456 - [x] **Step 1: Write `test/soak.sh`:**
455 457
456 ```sh 458 ```sh
457 #!/bin/sh 459 #!/bin/sh
@@ -514,7 +516,7 @@ cat "$SUMMARY"
514 exit 1 516 exit 1
515 ``` 517 ```
516 518
517 - [ ] **Step 2: Wire the build.** In `build.zig`, after the e2e step: 519 - [x] **Step 2: Wire the build.** In `build.zig`, after the e2e step:
518 520
519 ```zig 521 ```zig
520 const soak = b.addSystemCommand(&.{"test/soak.sh"}); 522 const soak = b.addSystemCommand(&.{"test/soak.sh"});
@@ -534,9 +536,9 @@ soak:
534 $(ZIG) build soak 536 $(ZIG) build soak
535 ``` 537 ```
536 538
537 - [ ] **Step 3: Smoke it.** `chmod +x test/soak.sh`, then `SOAK_N=1 make soak` — one green run, "soak OK: 1/1". Then verify the failure path fires: `SOAK_N=1` with a deliberately broken pin (temporarily set the OK literal to 99), expect "soak FAIL: 1 of 1" plus the table naming the pin line. Restore the pin, `git diff` clean apart from Task 5 files. 539 - [x] **Step 3: Smoke it.** `chmod +x test/soak.sh`, then `SOAK_N=1 make soak` — one green run, "soak OK: 1/1". Then verify the failure path fires: `SOAK_N=1` with a deliberately broken pin (temporarily set the OK literal to 99), expect "soak FAIL: 1 of 1" plus the table naming the pin line. Restore the pin, `git diff` clean apart from Task 5 files.
538 540
539 - [ ] **Step 4: Commit.** 541 - [x] **Step 4: Commit.**
540 542
541 ```bash 543 ```bash
542 git add test/soak.sh build.zig Makefile 544 git add test/soak.sh build.zig Makefile
@@ -545,10 +547,10 @@ git commit -m "test: make soak repeats the suite and attributes failures to runs
545 547
546 ### Task 6: kill-criterion leg 1 — the soak run 548 ### Task 6: kill-criterion leg 1 — the soak run
547 549
548 - [ ] **Step 1:** `make test && make e2e` green (full, fresh). 550 - [x] **Step 1:** `make test && make e2e` green (full, fresh).
549 - [ ] **Step 2:** `SOAK_N=10 make soak` (~25 min — run it in the background and wait; do not shorten N: the criterion says NOT EXERCISED below 10). 551 - [x] **Step 2:** `SOAK_N=10 make soak` (~25 min — run it in the background and wait; do not shorten N: the criterion says NOT EXERCISED below 10).
550 - [ ] **Step 3:** If any run fails: diagnose per the quiesce rule (lengthen that scenario's pre-detach settle; never loosen a diff), fix, commit the fix, and RESTART the count at run 1 — 10/10 means ten consecutive greens on the final code. 552 - [x] **Step 3:** If any run fails: diagnose per the quiesce rule (lengthen that scenario's pre-detach settle; never loosen a diff), fix, commit the fix, and RESTART the count at run 1 — 10/10 means ten consecutive greens on the final code.
551 - [ ] **Step 4:** Record the result (N/N, wall clock per run, any fixes made) in the commit message of a docs touch or in the final close notes — this is kill-criterion evidence. 553 - [x] **Step 4:** Record the result (N/N, wall clock per run, any fixes made) in the commit message of a docs touch or in the final close notes — this is kill-criterion evidence.
552 554
553 --- 555 ---
554 556
@@ -558,11 +560,11 @@ Not code: a recorded experiment. Read the spec's Phase 2 section before starting
558 560
559 ### Task 7: campaign infrastructure + the control run 561 ### Task 7: campaign infrastructure + the control run
560 562
561 - [ ] **Step 1: Worktree.** `git worktree add ../mux-m11-campaign main` (or the repo's worktree convention). Copy the QUIC dep outputs so the first build doesn't refetch: `cp -r deps/quic/out deps/quic/work ../mux-m11-campaign/deps/quic/`. 563 - [x] **Step 1: Worktree.** `git worktree add ../mux-m11-campaign main` (or the repo's worktree convention). Copy the QUIC dep outputs so the first build doesn't refetch: `cp -r deps/quic/out deps/quic/work ../mux-m11-campaign/deps/quic/`.
562 - [ ] **Step 2: The old suite.** Record OLD_SUITE_SHA = the commit BEFORE Task 2's first e2e edit (find it: `git log --oneline -- test/e2e.sh`, the commit before "render-vs-dump convergence"). Extract: `git show OLD_SUITE_SHA:test/e2e.sh > ../mux-m11-campaign/e2e-old.sh && chmod +x ../mux-m11-campaign/e2e-old.sh`. It takes FOUR args (no RENDER). 564 - [x] **Step 2: The old suite.** Record OLD_SUITE_SHA = the commit BEFORE Task 2's first e2e edit (find it: `git log --oneline -- test/e2e.sh`, the commit before "render-vs-dump convergence"). Extract: `git show OLD_SUITE_SHA:test/e2e.sh > ../mux-m11-campaign/e2e-old.sh && chmod +x ../mux-m11-campaign/e2e-old.sh`. It takes FOUR args (no RENDER).
563 - [ ] **Step 3: The campaign doc.** Create `docs/superpowers/plans/2026-08-09-m11-campaign.md` (in the MAIN checkout — results are not mutations) with the methodology header (old-suite sha, binary provenance, the per-mutation procedure below) and an empty table: `| # | site | mutation | old suite | new suite | disposition |`. 565 - [x] **Step 3: The campaign doc.** Create `docs/superpowers/plans/2026-08-09-m11-campaign.md` (in the MAIN checkout — results are not mutations) with the methodology header (old-suite sha, binary provenance, the per-mutation procedure below) and an empty table: `| # | site | mutation | old suite | new suite | disposition |`.
564 - [ ] **Step 4: The control run — both suites green on UNMUTATED code.** In the worktree: `~/Downloads/zig-x86_64-linux-0.15.2/zig build`, then `./e2e-old.sh zig-out/bin/muxd zig-out/bin/mux zig-out/bin/rawmode zig-out/bin/delaypipe` and `test/e2e.sh <same four> zig-out/bin/render`. Both must pass — a campaign whose baseline is red measures nothing. Record the control row in the table. 566 - [x] **Step 4: The control run — both suites green on UNMUTATED code.** In the worktree: `~/Downloads/zig-x86_64-linux-0.15.2/zig build`, then `./e2e-old.sh zig-out/bin/muxd zig-out/bin/mux zig-out/bin/rawmode zig-out/bin/delaypipe` and `test/e2e.sh <same four> zig-out/bin/render`. Both must pass — a campaign whose baseline is red measures nothing. Record the control row in the table.
565 - [ ] **Step 5: Commit the campaign doc.** `git add docs/superpowers/plans/2026-08-09-m11-campaign.md && git commit -m "docs: M11 campaign methodology and control run"` 567 - [x] **Step 5: Commit the campaign doc.** `git add docs/superpowers/plans/2026-08-09-m11-campaign.md && git commit -m "docs: M11 campaign methodology and control run"`
566 568
567 **Per-mutation procedure (referenced by Tasks 8–11):** in the worktree — (1) apply the single edit; (2) rebuild with the pinned zig (`zig build` — remember `make test` does not rebuild binaries); (3) run the OLD suite, record caught (which FAIL line) or survived; (4) run the NEW suite, record the same; (5) `git checkout -- src/ test/` and verify `git diff --stat` empty; (6) append the row to the campaign doc in the main checkout and commit it (`docs: campaign row N`, batched per task is fine). A mutation that fails to COMPILE is not a mutation — rework it until it builds (the M10 lesson: inferred error sets can make an edit uncompilable; pick a different expression of the same break). 569 **Per-mutation procedure (referenced by Tasks 8–11):** in the worktree — (1) apply the single edit; (2) rebuild with the pinned zig (`zig build` — remember `make test` does not rebuild binaries); (3) run the OLD suite, record caught (which FAIL line) or survived; (4) run the NEW suite, record the same; (5) `git checkout -- src/ test/` and verify `git diff --stat` empty; (6) append the row to the campaign doc in the main checkout and commit it (`docs: campaign row N`, batched per task is fine). A mutation that fails to COMPILE is not a mutation — rework it until it builds (the M10 lesson: inferred error sets can make an edit uncompilable; pick a different expression of the same break).
568 570
@@ -570,53 +572,53 @@ Not code: a recorded experiment. Read the spec's Phase 2 section before starting
570 572
571 These are the mutations the old suite is structurally blind to (markers grep the byte stream; a wrong paint of a right byte passes). The kill criterion requires caught-new/survived-old to be demonstrated here. 573 These are the mutations the old suite is structurally blind to (markers grep the byte stream; a wrong paint of a right byte passes). The kill criterion requires caught-new/survived-old to be demonstrated here.
572 574
573 - [ ] **Mutation 1 — drop the last row of a delta paint.** In `client.zig`'s delta-paint loop (anchor: the `\x1b[{d};1H\x1b[2K` row paint, ~line 973): skip the final row of each delta's row set. 575 - [x] **Mutation 1 — drop the last row of a delta paint.** In `client.zig`'s delta-paint loop (anchor: the `\x1b[{d};1H\x1b[2K` row paint, ~line 973): skip the final row of each delta's row set.
574 - [ ] **Mutation 2 — paint one row off.** Same loop: paint at `row.row + 2` instead of `row.row + 1` (CUP is 1-based; +2 shifts every delta row down one). 576 - [x] **Mutation 2 — paint one row off.** Same loop: paint at `row.row + 2` instead of `row.row + 1` (CUP is 1-based; +2 shifts every delta row down one).
575 - [ ] **Mutation 3 — skip the clear on a full repaint.** In `paintFull` (anchor: `\x1b[?2026h\x1b[?25l\x1b[H\x1b[2J`, ~line 941): drop the `\x1b[2J`. Stale glyphs survive under new content. 577 - [x] **Mutation 3 — skip the clear on a full repaint.** In `paintFull` (anchor: `\x1b[?2026h\x1b[?25l\x1b[H\x1b[2J`, ~line 941): drop the `\x1b[2J`. Stale glyphs survive under new content.
576 - [ ] **Mutation 4 — SGR bleed.** In the delta row paint: if the row's bytes start with `\x1b[0m`, strip those 4 bytes before writing, so the previous row's style bleeds in. Expect: plain diff may pass, STYLED diff catches — record which. 578 - [x] **Mutation 4 — SGR bleed.** In the delta row paint: if the row's bytes start with `\x1b[0m`, strip those 4 bytes before writing, so the previous row's style bleeds in. Expect: plain diff may pass, STYLED diff catches — record which.
577 - [ ] **Mutation 5 — omit the synchronized-update wrapper.** Drop `\x1b[?2026h`/`\x1b[?2026l` from the delta paint. Expected honestly: BOTH suites may miss (2026 is a flicker property, not a state property, and it is pinned at the unit layer — client tests assert its presence). If both miss, that is a finding for Task 12: the likely disposition is banked-with-reason, but write it down, don't presume it. 579 - [x] **Mutation 5 — omit the synchronized-update wrapper.** Drop `\x1b[?2026h`/`\x1b[?2026l` from the delta paint. Expected honestly: BOTH suites may miss (2026 is a flicker property, not a state property, and it is pinned at the unit layer — client tests assert its presence). If both miss, that is a finding for Task 12: the likely disposition is banked-with-reason, but write it down, don't presume it.
578 - [ ] Run the per-mutation procedure for each; commit the rows. 580 - [x] Run the per-mutation procedure for each; commit the rows.
579 581
580 ### Task 9: replica-sync + prediction-overlay mutations (client) 582 ### Task 9: replica-sync + prediction-overlay mutations (client)
581 583
582 - [ ] **Mutation 6 — apply a delta but skip feeding the replica engine.** Screen paints from the payload; the replica goes stale; reconcile and any replica-sourced repaint diverge. 584 - [x] **Mutation 6 — apply a delta but skip feeding the replica engine.** Screen paints from the payload; the replica goes stale; reconcile and any replica-sourced repaint diverge.
583 - [ ] **Mutation 7 — ignore a snapshot's cols/rows prefix.** Skip the resize-to-prefix on snapshot apply. Expected honestly: likely BOTH miss (every e2e grid is 80×24 and there is no path to another size in the suite). A finding either way — the disposition question for Task 12 is whether a non-default-size scenario is buildable or the gap is banked with that reason. 585 - [x] **Mutation 7 — ignore a snapshot's cols/rows prefix.** Skip the resize-to-prefix on snapshot apply. Expected honestly: likely BOTH miss (every e2e grid is 80×24 and there is no path to another size in the suite). A finding either way — the disposition question for Task 12 is whether a non-default-size scenario is buildable or the gap is banked with that reason.
584 - [ ] **Mutation 8 — accept a delta for a stale seq without requesting resync.** In the client's delta arm (anchor: `last_seq = composed.header.seq`, ~line 772): remove the gap-detection/resync request so an out-of-sequence delta is applied anyway. 586 - [x] **Mutation 8 — accept a delta for a stale seq without requesting resync.** In the client's delta arm (anchor: `last_seq = composed.header.seq`, ~line 772): remove the gap-detection/resync request so an out-of-sequence delta is applied anyway.
585 - [ ] **Mutation 9 — leave a confirmed prediction's underline on.** In the overlay's confirm path: keep the confirmed cell in the painted set. The styled diff is the only e2e eye that can see this; counters stay correct. 587 - [x] **Mutation 9 — leave a confirmed prediction's underline on.** In the overlay's confirm path: keep the confirmed cell in the painted set. The styled diff is the only e2e eye that can see this; counters stay correct.
586 - [ ] **Mutation 10 — feed a prediction into the replica engine (the M9 invariant).** Where the overlay records a prediction, also `replica.feed(...)` the predicted byte. 588 - [x] **Mutation 10 — feed a prediction into the replica engine (the M9 invariant).** Where the overlay records a prediction, also `replica.feed(...)` the predicted byte.
587 - [ ] Run the procedure; commit the rows. 589 - [x] Run the procedure; commit the rows.
588 590
589 ### Task 10: daemon-side mutations — delta production and seq/epoch (server) 591 ### Task 10: daemon-side mutations — delta production and seq/epoch (server)
590 592
591 - [ ] **Mutation 11 — skip one dirty row.** In `DeltaTracker.buildDeltaSince` (~line 196) or `update` (~149): omit the highest-numbered dirty row from the payload (the prompt row, typically — marker greps live higher). 593 - [x] **Mutation 11 — skip one dirty row.** In `DeltaTracker.buildDeltaSince` (~line 196) or `update` (~149): omit the highest-numbered dirty row from the payload (the prompt row, typically — marker greps live higher).
592 - [ ] **Mutation 12 — emit rows in the wrong order.** Reverse the row order in the delta payload. Expected honestly: rows are absolutely addressed by CUP, so the final grid may be identical and BOTH suites may miss — a disposition-by-reason candidate. 594 - [x] **Mutation 12 — emit rows in the wrong order.** Reverse the row order in the delta payload. Expected honestly: rows are absolutely addressed by CUP, so the final grid may be identical and BOTH suites may miss — a disposition-by-reason candidate.
593 - [ ] **Mutation 13 — mark a changed row clean.** In the tracker's change detection: never mark row 0 dirty. 595 - [x] **Mutation 13 — mark a changed row clean.** In the tracker's change detection: never mark row 0 dirty.
594 - [ ] **Mutation 14 — reuse a seq.** Don't increment the seq for one update in each pair (every second delta repeats its predecessor's seq). 596 - [x] **Mutation 14 — reuse a seq.** Don't increment the seq for one update in each pair (every second delta repeats its predecessor's seq).
595 - [ ] **Mutation 15 — answer a reconnect with a delta when a snapshot is owed.** In the attach/reconnect handler (~line 1093 area): serve `buildDeltaSince` even when the client's `have_seq` is below the tracker's floor. Note: the old suite has real teeth here (the restart scenarios assert snapshot counters) — this row is the control showing the old suite catching what it was built to catch. 597 - [x] **Mutation 15 — answer a reconnect with a delta when a snapshot is owed.** In the attach/reconnect handler (~line 1093 area): serve `buildDeltaSince` even when the client's `have_seq` is below the tracker's floor. Note: the old suite has real teeth here (the restart scenarios assert snapshot counters) — this row is the control showing the old suite catching what it was built to catch.
596 - [ ] **Mutation 16 — wrong epoch in the snapshot prefix.** Send `epoch + 1` in the snapshot prefix the daemon writes. 598 - [x] **Mutation 16 — wrong epoch in the snapshot prefix.** Send `epoch + 1` in the snapshot prefix the daemon writes.
597 - [ ] Run the procedure; commit the rows. 599 - [x] Run the procedure; commit the rows.
598 600
599 ### Task 11: scrollback/clipping mutations (client) 601 ### Task 11: scrollback/clipping mutations (client)
600 602
601 - [ ] **Mutation 17 — clip bound off-by-one.** In the client's paint clipping (anchor: the unit test asserting `\x1b[24;80H` reachable and `\x1b[25;` never emitted, ~line 1780): clamp one row short, so the last grid row is never painted. 603 - [x] **Mutation 17 — clip bound off-by-one.** In the client's paint clipping (anchor: the unit test asserting `\x1b[24;80H` reachable and `\x1b[25;` never emitted, ~line 1780): clamp one row short, so the last grid row is never painted.
602 - [ ] **Mutation 18 — scroll position not reset on resync.** In the resync path (anchor: `repaint_after_resync`, ~line 585–632): skip leaving the scroll view on resync. Expected honestly: scroll view is tty-only (Shift+PageUp), so e2e likely can't see it under EITHER suite — unit tests at ~line 1653 pin it. Record and disposition. 604 - [x] **Mutation 18 — scroll position not reset on resync.** In the resync path (anchor: `repaint_after_resync`, ~line 585–632): skip leaving the scroll view on resync. Expected honestly: scroll view is tty-only (Shift+PageUp), so e2e likely can't see it under EITHER suite — unit tests at ~line 1653 pin it. Record and disposition.
603 - [ ] Run the procedure; commit the rows. Then remove the campaign worktree: `git worktree remove ../mux-m11-campaign` (after confirming `git -C ../mux-m11-campaign diff --stat` is empty). 605 - [x] Run the procedure; commit the rows. Then remove the campaign worktree: `git worktree remove ../mux-m11-campaign` (after confirming `git -C ../mux-m11-campaign diff --stat` is empty).
604 606
605 ### Task 12: findings resolution — the recording rule 607 ### Task 12: findings resolution — the recording rule
606 608
607 - [ ] **Step 1:** For every row where BOTH suites missed, decide per the recording rule — no third bucket: 609 - [x] **Step 1:** For every row where BOTH suites missed, decide per the recording rule — no third bucket:
608 - (a) **new assertion**, landed now in `test/e2e.sh` (or the unit layer if that is where the property lives), with the mutation RE-RUN in a fresh worktree to prove the new assertion fires — the checkbox for each such fix is that demonstrated failure; or 610 - (a) **new assertion**, landed now in `test/e2e.sh` (or the unit layer if that is where the property lives), with the mutation RE-RUN in a fresh worktree to prove the new assertion fires — the checkbox for each such fix is that demonstrated failure; or
609 - (b) **banked**, as a written entry (goes into decisions.md at close) stating exactly why the gap is accepted and what would reopen it. 611 - (b) **banked**, as a written entry (goes into decisions.md at close) stating exactly why the gap is accepted and what would reopen it.
610 - [ ] **Step 2:** Update the campaign table's disposition column for every row; no row may read "noted". 612 - [x] **Step 2:** Update the campaign table's disposition column for every row; no row may read "noted".
611 - [ ] **Step 3:** If new assertions changed the convergence or checkpoint counts, update the Task 4 literals and say so in the commit. 613 - [x] **Step 3:** If new assertions changed the convergence or checkpoint counts, update the Task 4 literals and say so in the commit.
612 - [ ] **Step 4:** Re-run `make test && make e2e` green; if e2e assertions were added, re-run `SOAK_N=10 make soak` for the final code (leg 1 must hold on what ships). 614 - [x] **Step 4:** Re-run `make test && make e2e` green; if e2e assertions were added, re-run `SOAK_N=10 make soak` for the final code (leg 1 must hold on what ships).
613 - [ ] **Step 5:** Commit: `git commit -m "test: assertions landed from campaign findings; campaign table complete"` 615 - [x] **Step 5:** Commit: `git commit -m "test: assertions landed from campaign findings; campaign table complete"`
614 616
615 ### Task 13: milestone close 617 ### Task 13: milestone close
616 618
617 - [ ] **Step 1:** `docs/decisions.md` — new M11 section: the campaign table IN FULL (it is the milestone's measurement, as M9's latency table was), the kill-criterion evidence for both legs (soak 10/10 with wall clocks; paint-path caught-new/survived-old rows called out), any defects found en route (the raw-mode phantom-glyph candidate, if it fired), and the banked entries from Task 12 with their reasons. 619 - [x] **Step 1:** `docs/decisions.md` — new M11 section: the campaign table IN FULL (it is the milestone's measurement, as M9's latency table was), the kill-criterion evidence for both legs (soak 10/10 with wall clocks; paint-path caught-new/survived-old rows called out), any defects found en route (the raw-mode phantom-glyph candidate, if it fired), and the banked entries from Task 12 with their reasons.
618 - [ ] **Step 2:** `docs/roadmap.md` — M11 section marked complete with a verdict paragraph; ASAN/valgrind and the unit-layer mutation sweep stay banked, each annotated with what the campaign taught about them; delete the campaign scratch doc (`git rm docs/superpowers/plans/2026-08-09-m11-campaign.md`) once its table lives in decisions.md. 620 - [x] **Step 2:** `docs/roadmap.md` — M11 section marked complete with a verdict paragraph; ASAN/valgrind and the unit-layer mutation sweep stay banked, each annotated with what the campaign taught about them; delete the campaign scratch doc (`git rm docs/superpowers/plans/2026-08-09-m11-campaign.md`) once its table lives in decisions.md.
619 - [ ] **Step 3:** Tick this plan's checkboxes; commit: `git commit -m "docs: M11 close — convergence harness, pins, soak, campaign table"` 621 - [x] **Step 3:** Tick this plan's checkboxes; commit: `git commit -m "docs: M11 close — convergence harness, pins, soak, campaign table"`
620 622
621 --- 623 ---
622 624