90487096
docs: M12 complete — the tty gate is open and graded
a73x 2026-08-10 13:32
Commit message
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 | ||