a73x

90487096

docs: M12 complete — the tty gate is open and graded

a73x   2026-08-10 13:32

Commit message
docs: M12 complete — the tty gate is open and graded

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

docs/decisions.md
Old New
@@ -1767,6 +1767,256 @@ continued existence on its own terms and was gated by a new mutation
1767 had exactly one specimen source in the entire corpus — the prediction 1767 had exactly one specimen source in the entire corpus — the prediction
1768 overlay's own underline. 1768 overlay's own underline.
1769 1769
1770 ## 2026-08-10 (M12 — ptyclient: a pty-driving e2e client fixture)
1771
1772 **Verdict: cleared, both legs.** The e2e client is no longer blind to its
1773 own terminal. `test/ptyclient.zig` hands it a real pty, and the branches
1774 that were unreachable for the whole of M11 — raw mode, the alternate
1775 screen, true dimensions, SIGWINCH, scroll mode — are now driven and
1776 graded. Leg 1: the suite stayed green and grew, **13 scenario checkpoints
1777 over 25 convergence points**, both pinned as literals, every new scenario
1778 ending in `assert_converged`, and `SOAK_N=10 make soak` **10/10 on the
1779 code that ships** (3f27759/3690e5b). Leg 2: the three defects M11 could
1780 not score are **three resurrections, three catches**, each dying at
1781 exactly the check it was predicted to die at — rows 7 and 18, both filed
1782 as "survived M11, ungradeable — code unreachable", and the 412f38f
1783 revert, which had no e2e pin of any kind. The rule that came out of the
1784 milestone came out of three scenario shapes that failed under load: **an
1785 assertion about order must be an assertion the bytes can actually
1786 carry.** Spec: superpowers/specs/2026-08-10-m12-ptyclient-design.md.
1787
1788 ### The regrade table
1789
1790 The milestone's measurement, as M11's campaign table was. One sitting,
1791 one tree — `3f27759` — one set of binaries, each defect resurrected in a
1792 detached worktree at `/tmp/mux-m12-regrade` (since removed) and the
1793 suite's own words written down. Mutations were applied to the worktree
1794 alone and reverted with `git checkout -- src/client.zig` between rows;
1795 the worktree was verified clean before removal, and no product or test
1796 code on `main` was changed by the exercise. The baseline control ran
1797 first and green — `e2e OK (13 scenarios, 25 convergence points)` — and
1798 the budget was four full-suite runs in total, because no resurrection
1799 needed a re-run.
1800
1801 | Resurrection | Prior score (M11) | The FAIL line now | Verdict |
1802 | --- | --- | --- | --- |
1803 | Row 7 — snapshot cols/rows prefix ignored (`replica.resize(prefix.cols, prefix.rows)` commented out, `src/client.zig:739`) | survived M11, ungradeable — code unreachable | `e2e FAIL: tp2b ptyclient exited 3` (`verb 7: expect "0"×91 did not arrive within 10000ms`) | **CAUGHT** |
1804 | Row 18 — resync no longer leaves scroll view (`scroll_pages = 0;` deleted, `src/client.zig:609`) | survived M11, ungradeable — code unreachable | `e2e FAIL: tp1 ptyclient exited 3` (`verb 5: expect "100" did not arrive within 20000ms`) | **CAUGHT** |
1805 | The 412f38f revert — scroll-mode suppression left set across reconnect (`overlay.setScrollMode(false);` deleted, `src/client.zig:621`) | no e2e pin existed | `e2e FAIL: pty scroll reconnect: made=0, want 1` | **CAUGHT** |
1806
1807 **Row 7 — the width witness fires.** Commenting out the replica resize
1808 means the client parses a snapshot's `cols`/`rows` prefix and then paints
1809 into a replica still sized to the old geometry. tp2b's verb 7 demands a
1810 full row of 91 zeros, a string that cannot appear at the old width:
1811
1812 ```
1813 e2e FAIL: tp2b ptyclient exited 3:
1814 ptyclient: verb 7: expect "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" did not arrive within 10000ms
1815 ptyclient: last 200 bytes received: "095d\n' 7\x1b[26;1H\x1b[0m000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\x1b[27;1H\x1b[0m00007\x1b[28;1H\x1b[0mtp2rdy@\x1b[28;5H\x1b[?25h\x1b[?2026l\x1b[?2026h\x1b[?25l\x1b[30;8H\x1b[?25h\x1b[?2026l"
1816 ```
1817
1818 The check that fired is the fixture's witness timeout, and the tail is
1819 the diagnosis in miniature: the zeros arrive wrapped — 88 on row 26 and
1820 the remainder spilling onto row 27 — because the replica is still the
1821 narrower grid. The witness is precisely the assertion that they arrive
1822 *unwrapped*.
1823
1824 **Row 18 — the live repaint never lands.** Deleting `scroll_pages = 0;`
1825 from the resync path leaves the client believing it is still scrolled
1826 after a reconnect, so the authoritative repaint of live state is
1827 suppressed behind a history page. tp1's verb 5 waits for the live content
1828 and never sees it:
1829
1830 ```
1831 e2e FAIL: tp1 ptyclient exited 3:
1832 ptyclient: verb 5: expect "100" did not arrive within 20000ms
1833 ptyclient: last 200 bytes received: "\x1b[?25l\x1b[H\x1b[2J\x1b[0m54\r\r\n55\r\r\n56\r\r\n...\r\r\n77\x1b[1;72H\x1b[7m[scroll]\x1b[0m\x1b[?2026l\x1b[s\x1b[1;66H\x1b[7m[reconnecting]\x1b[0m\x1b[u"
1834 ```
1835
1836 The tail shows the screen frozen on the history page (rows 54–77) with
1837 the `[scroll]` badge still lit and `[reconnecting]` painted over it: the
1838 client sat in scroll mode through the reconnect, exactly as the deleted
1839 line was there to prevent.
1840
1841 **The 412f38f revert — the exact counter.** Deleting only
1842 `overlay.setScrollMode(false);`, with `scroll_pages = 0;` left in place
1843 so this is 412f38f's defect and not row 18's, leaves the overlay
1844 suppressing predictions with no page left to suppress for. The screen
1845 looks right; prediction is silently dead. The scenario catches it on
1846 counters, not on content:
1847
1848 ```
1849 e2e FAIL: pty scroll reconnect: made=0, want 1
1850 predict made=0 displayed=0 confirmed=0 contradicted=0 expired=0 abandoned=0 suppressed=1
1851 ```
1852
1853 The check that fired is the exact counter assertion — `want 1`, not
1854 `want >= 1` — and the stats line names the mechanism outright:
1855 `suppressed=1` with `made=0`, the keystroke swallowed by a scroll mode
1856 that outlived the scroll. This is the fix that was previously guarded
1857 only by reading and by a unit-level contract test.
1858
1859 **Independent corroboration.** None of the three rests on the single
1860 sitting. tp2b's width witness was seen red 10/10 by the implementer and
1861 independently confirmed 4/4 on the full suite and 10/10 targeted by the
1862 reviewer; tp1's two were each seen red during development, and the spec
1863 reviewer independently re-ran the 412f38f revert. Three for three: two
1864 rows M11 had to bank as "needs a pty-driving client fixture" are graded
1865 CAUGHT by that fixture, and the 412f38f fix has an end-to-end pin for the
1866 first time.
1867
1868 ### What shipped
1869
1870 `Pty.spawnArgv` (`src/pty.zig`) — one child-setup path for both the
1871 daemon's shell spawn and the fixture's argv spawn, with an optional
1872 stderr redirect off the pty and parent-side `EmptyArgv` validation. It
1873 also completed 7b4208f's `exit_group`-only sweep, which had missed
1874 `pty.zig` entirely; two further instances were caught in review.
1875
1876 `test/ptyclient.zig` (~490 lines) — a script engine over that pty:
1877 `send`, `expect`, `resize`, `settle`, `waitexit`, an `Expecter` whose
1878 cursor *consumes* the match rather than the buffer, distinct exit codes
1879 (2 usage, 3 timeout, 4 child died), escaped-tail diagnostics on failure,
1880 and a bounded final drain. Five fixture controls guard the fixture
1881 itself: the cat roundtrip, an impossible expect that must fail at exit 3
1882 *exactly*, and the stderr-split leg proving predict stats land in the
1883 `.err` sibling and never in the capture the convergence machinery diffs.
1884
1885 Three scenarios: tp2a (first attach at a real 100x30), tp2b (resize
1886 mid-session — the row-7 catcher), tp1 (reconnect while scrolled — row 18
1887 and the 412f38f pin). `converged_quiet`/`assert_converged` grew optional
1888 size arguments so a non-80x24 grid can be compared against the size it
1889 actually is.
1890
1891 ### The finding that reshaped the scenarios: delta rows never touch the replica
1892
1893 Load-bearing, and it inverted the plan. Delta rows do **not** pass
1894 through the replica on their way to the tty — `paintDeltaClipped` paints
1895 them straight out, absolutely addressed, clipped to the tty. The replica
1896 is read back only at a **full repaint** (`renderClipped`, clipped to
1897 `min(replica, tty)`). The consequence is that a stale replica is
1898 *invisible* on the delta path: it is observable only when damage is on
1899 screen at a full repaint **and** outside the reach of the stale geometry.
1900
1901 Three things follow, and all three are now in the file at their sites.
1902 The plan's original resize-DOWN scenario could never have caught row 7,
1903 because a too-wide replica clips identically to a correct one. tp2b
1904 therefore resizes **up**, with the 95-wide row printed *before* the
1905 winch, so the stale 90x28 replica has somewhere to show. And tp2a is
1906 documented as **not** exercising the prefix at all: an attach applies the
1907 attacher's size to the grid server-side before the answering resync, so
1908 the prefix always equals the size the replica already holds and the
1909 resize-on-prefix guard never fires on a first attach. That last one is a
1910 negative result, verified by probe rather than by reading, and it is
1911 recorded in the scenario's own comment so the next reader does not
1912 mistake tp2a for coverage it does not provide.
1913
1914 ### The choreography war, and the rule it produced
1915
1916 Prompt-row sentinel occurrences have **no order guarantee** relative to
1917 output rows once a scrolling repaint splits across deltas. Two
1918 sentinel-counting shapes of tp2b failed under a 16-way nice'd load gate —
1919 9 failures in 150 and 15 in 150 — and the worst specimen is the one worth
1920 remembering: a **clean** build failing with a diff byte-identical to row
1921 7's mutation signature, missing rows and a still-wrapped wide row. A
1922 harness that cannot tell a load flake from the bug it exists to catch is
1923 worse than no harness.
1924
1925 The cures are in the file as comments and are the general form: sync only
1926 on (a) content unique to its phase, (b) a **structural witness** — bytes
1927 that are impossible before the event, such as 91 contiguous zeros at a
1928 width of 90 — or (c) `settle`, silence, **after** an arrival proof and
1929 never alone, because settle's quiet window starts at the verb and never
1930 requires a byte to have arrived. Witness and settle each cover the
1931 other's blind side: the witness permits a detach mid-repaint, and settle
1932 alone passes having observed nothing at all. Hence the rule: **an
1933 assertion about order must be an assertion the bytes can actually
1934 carry.**
1935
1936 The loaded-gate scoreboard, since the numbers are the argument: sentinel
1937 shapes 9/150 and 15/150 failures; the witness shape 150/150 pass;
1938 witness+settle 150/150; tp1 20/20 with a byte-identical counter line
1939 every run. And the reason the gate had to be loaded at all: idle runs
1940 measured **0 failures in 420** on a shape that failed 6% of the time
1941 under load. Idle statistics cannot gate this flake class.
1942
1943 ### The prediction counters are exact, and that took exactly one keystroke
1944
1945 `offerKeystroke` refuses any stdin chunk that is not one byte, counting
1946 it suppressed, so a multi-byte send predicts nothing — and consecutive
1947 sends have no barrier between them, so the client's next read can pick up
1948 two at once. Measured on a three-keystroke draft of tp1: `made=3` on one
1949 run and `made=2` on the next, identical scripts. Under load a full
1950 coalesce reads `made=0`, which is the mutation's own signature.
1951
1952 tp1 therefore types **one** character and asserts five counters exactly:
1953 `made=1 displayed=1 confirmed=1 contradicted=0 suppressed=0`. That pins
1954 all seven, because `expired <= contradicted` (they share the `abandonAll`
1955 tail — the M11 invariant again) and `made = confirmed + abandoned +
1956 pending` forces the remainder. The arrival needle is structural for the
1957 same reason the witness is: in canonical mode the only thing one
1958 keystroke produces is the line discipline's echo of the very glyph the
1959 prediction just painted, so no *content* needle can tell the daemon's
1960 answer from the client's guess. `\x1b[2K` can — it reaches the client's
1961 stdout from exactly one paint path (`paintDeltaClipped`), predictions
1962 paint with no erase, and both full repaints use `2J`.
1963
1964 ### Smaller findings, each with its rule
1965
1966 - **`spawnArgv` execve's `argv[0]` with no PATH search.** An
1967 `env VAR=x cmd` prefix therefore dies at 127 before the first verb.
1968 Harness-side environment export is the form, and the comment says so.
1969 - **The doctored-stream control was a no-op on pty captures.** `render`
1970 replays only up to the last alt-screen exit, and a tty client's capture
1971 *ends* with one, so appended bytes landed after the grid under test and
1972 changed nothing — a control that could not fire. Fixed by dropping the
1973 trailing 8-byte `\x1b[?1049l` first; that tail is now asserted by `od`
1974 rather than assumed, because if teardown ever stops ending there the
1975 control would silently go back to being a no-op.
1976 - **`kill -0` cannot detect the death of the shell's own unreaped
1977 child** — signalling a zombie succeeds. tp1's daemon guard now proves
1978 the daemon is *serving*, via `muxd dump`, not that it exists.
1979 - **A Zig mutation that orphans a capture fails the BUILD**, which reads
1980 as a kill and proves nothing. Discard the capture in the same edit.
1981 - **`waitexit` drains to a bounded quiet after observing the exit.**
1982 Bytes arriving between the last poll and the `waitpid` were silently
1983 lost; provable to 0-byte captures with a widened window.
1984
1985 ### Method note
1986
1987 The reviews were adversarial and load-bearing, which is the only reason
1988 several of the above are recorded as findings rather than shipped as
1989 bugs. The Task 4 spec reviewer personally ran the row-7 resurrection and
1990 proved the first committed version of the scenario graded nothing. The
1991 implementer measured the doubled-sentinel cure at one red in ten before
1992 it could land, and proposed the witness instead. Two inaccuracies in
1993 implementer reports were caught by reviewers re-verifying the claims
1994 rather than reading them — a "verbatim transcription" that was not one,
1995 and a "reaps by pid" that was actually the tty's SIGHUP. One process
1996 lesson, without naming infrastructure: instructions issued against a
1997 moving tree can be stale by the time they arrive, so instructions should
1998 address content, not commit SHAs.
1999
2000 ### Banked by M12
2001
2002 - **The outer `timeout` on the pty scenarios sits below the sum of their
2003 verb deadlines** — 40 on tp2 against ~65s of verbs, 60 on tp1 against
2004 ~120s. House-consistent with every other scenario's outer bound, and
2005 noted rather than changed: the verb deadlines are the diagnosis and the
2006 outer bound is only the backstop, but a slow machine would report the
2007 backstop's silence instead of the verb's message.
2008 - **tp2b's `fill-done` expect retains a survivable one-sided race
2009 class** — survivable only while the content it precedes is what it is
2010 today; a new kind of content typed after it would need the same
2011 witness-or-settle treatment the post-resize verbs got.
2012 - **Loaded gating exists only as session tooling** — a scratchpad driver
2013 that runs the suite N times under a 16-way load, not a repo target.
2014 Every number in the choreography section came from it, and the idle-vs-
2015 loaded gap (0/420 against 6%) is the argument for a future `make gate`.
2016 - The M11 banks are unchanged: prediction polish, the unit-layer mutation
2017 sweep, ASAN/valgrind over the QUIC tests, and rows 4, 5 and 12 as
2018 properties no rendered grid can represent.
2019
1770 ## Open (owed by later milestones) 2020 ## Open (owed by later milestones)
1771 2021
1772 - Scrollback retention *tuning*. The policy itself was decided in M1 and 2022 - Scrollback retention *tuning*. The policy itself was decided in M1 and
docs/roadmap.md
Old New
@@ -5,7 +5,7 @@ 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–M11 complete; `v0.0.1-2` published as a Linux tarball; in 8 **Now:** M1–M12 complete; `v0.0.1-2` published as a Linux tarball; in
9 field trial on real VMs, and the trial has started producing the queue. 9 field trial on real VMs, and the trial has started producing the queue.
10 Trial feedback outranks everything below — what actually hurts in use is 10 Trial feedback outranks everything below — what actually hurts in use is
11 better data than any of this ranking, and the three items at the top of 11 better data than any of this ranking, and the three items at the top of
@@ -72,7 +72,26 @@ 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 72 extracted: **a timing margin is a number you compute, not a sentence you
73 write**. 73 write**.
74 74
75 ## Trial friction, likely next — and still outranked by trial feedback 75 ## M12 — ptyclient — complete
76
77 **Verdict: cleared, both legs.** The tty gate is open: the e2e client
78 now runs on a real pty, so the branches that were unreachable for the
79 whole of M11 — raw mode, the alternate screen, true dimensions,
80 SIGWINCH, scroll mode — are driven and graded. Leg 1 held at **13
81 scenario checkpoints over 25 convergence points**, both pinned as
82 literals, every new scenario ending in a convergence check, with
83 `SOAK_N=10 make soak` at **10/10 on the code that ships**. Leg 2 is
84 three for three: campaign rows 7 and 18, which M11 had to bank as
85 ungradeable because the mutated code never ran, and the 412f38f revert,
86 which had no e2e pin of any kind, were each resurrected and each caught
87 at exactly the check that predicted them (decisions.md, M12, which
88 carries the regrade table and the failure lines). En route, three
89 scenario shapes died under a loaded gate — one of them a clean build
90 failing with the mutation's own signature — and the rule extracted is
91 **an assertion about order must be an assertion the bytes can actually
92 carry**.
93
94 ## M13 candidates — trial friction — and still outranked by trial feedback
76 95
77 The first three are field findings from this week's real usage on a 96 The first three are field findings from this week's real usage on a
78 jump-host box, not predictions. They are one natural bundle and the 97 jump-host box, not predictions. They are one natural bundle and the
@@ -105,14 +124,8 @@ likely next cut; the order within it is the user's call.
105 124
106 ## Test debt, banked by M11 125 ## Test debt, banked by M11
107 126
108 - **A pty-driving client fixture** — the suite's single largest coverage 127 - ~~A pty-driving client fixture~~ — paid by M12; rows 7/18 and the
109 debt, and the campaign is what sized it. Every e2e client captures 128 412f38f pin all caught (decisions.md, M12).
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 129 - **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 130 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 131 layer rather than the e2e one (the delta paint's synchronized-update
docs/superpowers/plans/2026-08-10-m12-ptyclient.md
Old New
@@ -1,6 +1,7 @@
1 # M12 ptyclient Implementation Plan 1 # M12 ptyclient Implementation Plan
2 2
3 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. 3 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4 > Executed in full; verdicts and deviations recorded in decisions.md M12.
4 5
5 **Goal:** A pty-driving e2e client fixture (`test/ptyclient.zig`) that opens the client's tty-gated branches to the suite, plus the two scenarios that pay M11's named debts, graded by resurrecting campaign rows 7 and 18 and the 412f38f revert — all three must go from unscorable to caught. 6 **Goal:** A pty-driving e2e client fixture (`test/ptyclient.zig`) that opens the client's tty-gated branches to the suite, plus the two scenarios that pay M11's named debts, graded by resurrecting campaign rows 7 and 18 and the 412f38f revert — all three must go from unscorable to caught.
6 7
docs/superpowers/plans/2026-08-10-m12-regrade.md
Old New
@@ -1,112 +0,0 @@
1 # M12 Leg 2 regrade — the formal scoring
2
3 **What this is.** M12 built a pty-driving client fixture (`ptyclient`) and two
4 scenarios (tp2 resize, tp1 reconnect-while-scrolled) for one reason: three
5 defect classes that the M11 mutation campaign could not grade — two because the
6 mutated code was unreachable from a pipe-driven client, one because no e2e pin
7 existed at all — should become CAUGHT. This is the on-the-record grading of
8 that claim: one sitting, one tree, one set of binaries, each defect
9 resurrected in a detached worktree and the suite's own words written down.
10
11 - **Tree:** `3f27759` ("fix: tp1's daemon guard proves serving, not existing"),
12 graded in a detached worktree at `/tmp/mux-m12-regrade` (since removed).
13 - **Date:** 2026-08-10.
14 - **Toolchain:** pinned Zig via the Makefile (`make build` / `make e2e`).
15 - **Baseline control, run first, unmutated:** GREEN —
16 `e2e OK (13 scenarios, 25 convergence points)`.
17 - **Budget:** four full-suite runs (baseline + three resurrections). No
18 re-runs were needed; every resurrection died at the check it was predicted
19 to die at.
20
21 No product code and no test code was changed on `main` by this exercise. Each
22 mutation was applied to the worktree alone and reverted with
23 `git checkout -- src/client.zig` before the next; the worktree was verified
24 clean before removal.
25
26 | Resurrection | Prior score (M11) | The FAIL line now | Verdict |
27 | --- | --- | --- | --- |
28 | Row 7 — snapshot cols/rows prefix ignored (`replica.resize(prefix.cols, prefix.rows)` commented out, `src/client.zig:739`) | survived M11, ungradeable — code unreachable | `e2e FAIL: tp2b ptyclient exited 3` (`verb 7: expect "0"×91 did not arrive within 10000ms`) | **CAUGHT** |
29 | Row 18 — resync no longer leaves scroll view (`scroll_pages = 0;` deleted, `src/client.zig:609`) | survived M11, ungradeable — code unreachable | `e2e FAIL: tp1 ptyclient exited 3` (`verb 5: expect "100" did not arrive within 20000ms`) | **CAUGHT** |
30 | The 412f38f revert — scroll-mode suppression left set across reconnect (`overlay.setScrollMode(false);` deleted, `src/client.zig:621`) | no e2e pin existed | `e2e FAIL: pty scroll reconnect: made=0, want 1` | **CAUGHT** |
31
32 ## Row 7 — the width witness fires
33
34 Commenting out the replica resize means the client parses a snapshot's
35 `cols`/`rows` prefix and then paints into a replica still sized to the old
36 geometry. tp2b's verb 7 is the **width witness**: after the resize it demands a
37 full row of 91 zeros, a string that simply cannot appear at the old width. The
38 fixture timed out waiting for it:
39
40 ```
41 e2e FAIL: tp2b ptyclient exited 3:
42 ptyclient: done 1
43 ptyclient: done 2
44 ptyclient: done 3
45 ptyclient: done 4
46 ptyclient: done 5
47 ptyclient: done 6
48 ptyclient: verb 7: expect "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" did not arrive within 10000ms
49 ptyclient: last 200 bytes received: "095d\n' 7\x1b[26;1H\x1b[0m000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\x1b[27;1H\x1b[0m00007\x1b[28;1H\x1b[0mtp2rdy@\x1b[28;5H\x1b[?25h\x1b[?2026l\x1b[?2026h\x1b[?25l\x1b[30;8H\x1b[?25h\x1b[?2026l"
50 ```
51
52 The check that fired is the **fixture witness timeout**, and the tail is the
53 diagnosis in miniature: the zeros arrive wrapped — 88 on row 26 and the
54 remainder spilling onto row 27 — because the replica is still the narrower
55 grid. The witness is precisely the assertion that they arrive *unwrapped*.
56
57 ## Row 18 — the live repaint never lands
58
59 Deleting `scroll_pages = 0;` from the resync path leaves the client believing
60 it is still scrolled after a reconnect, so the authoritative repaint of live
61 state is suppressed behind a history page. tp1's verb 5 waits for the live
62 content (`"100"`) to appear after the reconnect and never sees it:
63
64 ```
65 e2e FAIL: tp1 ptyclient exited 3:
66 ptyclient: done 1
67 ptyclient: done 2
68 ptyclient: done 3
69 ptyclient: done 4
70 ptyclient: verb 5: expect "100" did not arrive within 20000ms
71 ptyclient: last 200 bytes received: "\x1b[?25l\x1b[H\x1b[2J\x1b[0m54\r\r\n55\r\r\n56\r\r\n57\r\r\n58\r\r\n59\r\r\n60\r\r\n61\r\r\n62\r\r\n63\r\r\n64\r\r\n65\r\r\n66\r\r\n67\r\r\n68\r\r\n69\r\r\n70\r\r\n71\r\r\n72\r\r\n73\r\r\n74\r\r\n75\r\r\n76\r\r\n77\x1b[1;72H\x1b[7m[scroll]\x1b[0m\x1b[?2026l\x1b[s\x1b[1;66H\x1b[7m[reconnecting]\x1b[0m\x1b[u"
72 ```
73
74 The check that fired is the **fixture expect timeout**. The tail shows the
75 screen frozen on the history page (rows 54–77) with the `[scroll]` badge still
76 lit and `[reconnecting]` painted over it — the client sat in scroll mode
77 through the reconnect exactly as the deleted line was there to prevent.
78
79 ## The 412f38f revert — the exact counter
80
81 Deleting only `overlay.setScrollMode(false);` (with `scroll_pages = 0;` left in
82 place, so this is 412f38f's defect and not row 18's) leaves the overlay
83 suppressing predictions with no page left to suppress for. The screen looks
84 right; prediction is silently dead. The scenario catches it on counters, not
85 on content:
86
87 ```
88 e2e FAIL: pty scroll reconnect: made=0, want 1
89 predict made=0 displayed=0 confirmed=0 contradicted=0 expired=0 abandoned=0 suppressed=1
90 ```
91
92 The check that fired is the **exact counter assertion** — `want 1`, not
93 `want >= 1`. The stats line names the mechanism outright: `suppressed=1` with
94 `made=0`, the keystroke swallowed by a scroll mode that outlived the scroll.
95 This is the resurrection that had no e2e pin of any kind before M12; the fix
96 was previously guarded only by reading.
97
98 ## Independent corroboration
99
100 None of these three catches rests on this single sitting. Each was observed
101 red during development, before this formal grading:
102
103 - **tp2b's width witness (row 7):** the implementer saw it 10/10, and the
104 reviewer independently confirmed 4/4 on the full suite and 10/10 targeted.
105 - **tp1's two (row 18 and the 412f38f revert):** the implementer saw each
106 once, and the spec reviewer independently re-ran the 412f38f revert (C).
107
108 ## Result
109
110 Three for three. Two rows the M11 campaign had to bank as "survived — needs a
111 pty-driving client fixture" are now graded CAUGHT by that fixture, and the
112 412f38f fix has an e2e pin for the first time.
docs/superpowers/specs/2026-08-10-m12-ptyclient-design.md
Old New
@@ -1,6 +1,6 @@
1 # M12 — ptyclient: a pty-driving e2e client fixture 1 # M12 — ptyclient: a pty-driving e2e client fixture
2 2
3 **Status:** approved 2026-08-10. Scope: pay the named debts, nothing more. 3 **Status:** executed 2026-08-10 — verdicts and deviations in decisions.md M12.
4 **Queue:** this is M12; the trial-friction bundle (auto-start + `muxd stop` + 4 **Queue:** this is M12; the trial-friction bundle (auto-start + `muxd stop` +
5 error audit) moves to M13. 5 error audit) moves to M13.
6 6