Commit Graph

1351 Commits

Author SHA1 Message Date
Levi Neuwirth 18b74d7a97
docs(evidence): narrow the D0a conclusion; retire D0b as a precondition
Three findings, all upheld.

1. The causal conclusion overreached, in the same way this lane has
   overreached before. Uniform-red at both endpoints today proves only
   that the two commits DO NOT DISCRIMINATE UNDER CURRENT CONDITIONS.
   "Source hypothesis eliminated", "the interval cannot contain the
   transition" and "unreachable by source" are withdrawn from the
   framing, the manifest and the ledger: a historical regression could
   be masked by a later environmental effect, or by a source/environment
   interaction under which both commits now fail. Failing to
   discriminate is not the same as not differing. "No bisect is
   justified under current conditions" is what survives, and the
   approved endpoint table's two uniform-same rows are corrected to say
   the same thing.

2. D0b was still mandatory, and going to D1/D2 would have skipped an
   approved step. It is now RETIRED AS A PRECONDITION with the reason
   recorded: it existed to make the reduction matrix trustworthy so the
   subset-vs-full comparison could locate the mechanism indirectly,
   and D0a has since produced a reliable direct reproduction that D1/D2
   measure against. Re-running ten reduction rows to sharpen an
   indirect instrument while a direct one is in hand is the wrong order
   of work. The obligation is NOT discharged: A3 still binds, so if
   D1/D2 fail to account for why every subset passed, D0b runs before
   this lane closes.

3. Provenance is now portable. The exact per-run command and a
   transcribed ten-row table --- start time, class, red bins, load,
   freeMB, daemon count, log digest --- are committed, rather than
   delegated to a machine-local results.tsv. Raw logs stay local by
   design. The transcription also surfaces something the delegation hid:
   the leaked-daemon count climbs 72 -> 108, four per run, monotonically
   while every run classifies identically. Recorded, not implicated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 13:23:19 +02:00
Levi Neuwirth 24a84b5381
docs(evidence): D0a executed --- the source hypothesis is eliminated
Ten runs under the approved contract: counterbalanced A B B A A B B A A
B, N = 5 per endpoint, clean detached worktrees at 7599661 and 724b785,
isolated target directories, the gate's build-crdt precondition then its
sweep-crdt command, dirty=0 verified per run. Zero voids, zero splits.

A (7599661) uniform-red. B (724b785) uniform-red. By the approved
endpoint table that is the both-endpoints-uniform-same row: the
difference is NOT captured by those two commits.

What it settles:

  - No bisect of 7599661..724b785 is justified, and none will run.
    7599661 passed inside sweep-crdt on 08-15 and fails 5/5 clean today,
    so the interval cannot contain the transition.
  - The onset window is demoted --- still a true observation, but not
    reachable by source.
  - A RELIABLE REPRODUCTION now exists: 10/10 today across two commits
    at ~4 minutes per run. This is D0a's most useful product, because
    D1/D2 no longer depend on catching a rare event.

What it does not settle: anything about the mechanism. One cheap
negative on "what else changed" --- no package activity in the window per
pacman.log, nearest on 08-18 --- and it is not pursued further, because
with a reproduction in hand direct measurement dominates archaeology.

A's three extra failing binaries are recorded rather than swept up:
a54_real_daemon_real_pty_and_headless_gpu_render..., a v21/v20 row
expected to differ at that older commit, and m6_1_pty_mode_lifecycle.
Two of the three are process/PTY-spawn rows, the same family as the
target. None affect classification, which reads only the two target
copies.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 13:04:59 +02:00
Levi Neuwirth bdef05cd02
docs(framing): record revision 9 approval
Revision 9 is approved at 15c25ec after the portable manifest and compact
ledger summary preserve the endpoint direction required by D0a.

Record that approval durably before diagnostic implementation begins. The
mechanism remains unknown, no fix is proposed, and panel-mapping-generation
remains held until this teardown lane closes.
2026-08-19 12:14:21 +02:00
Levi Neuwirth 15c25ecaad
docs(evidence): preserve endpoint direction in D0 summary
The portable manifest collapsed the two clean-split directions even though
the governing endpoint table permits a bisect only when 7599661 is uniform
green and 724b785 is uniform red. Preserve that direction explicitly, and
carry the same distinction in the compact active-work summary.

The inverted split remains a real difference, but it contradicts the onset
reading and therefore requires that reading to be re-examined before any
bisect.
2026-08-19 12:02:21 +02:00
Levi Neuwirth f99250e8cd
docs(evidence): the manifest fix in revision 9 never landed
74dbd34 corrected the framing so that "the difference is not captured"
attaches to the uniform-same endpoint verdicts rather than to mixed ---
mixed means intermittency --- but the manifest kept the wrong pairing.
The edit's anchor did not match and the script reported success anyway,
so the commit touched only active-work.md and the framing.

The manifest now defers to §7 D0a for the governing contract and states
the three verdicts with the conclusions that actually belong to them:
clean split permits the bisect, mixed means intermittency under fixed
source, both-uniform-same means the difference is not captured by those
two commits.

Process note, since this is the third silent no-op of its kind in this
lane: an unasserted string replace that misses its anchor is
indistinguishable from one that succeeded. Every edit here now asserts
the anchor before writing and re-asserts the result afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 10:41:13 +02:00
Levi Neuwirth 74dbd342a6
docs(framing): revision 9 --- a total classifier, and honest counterbalancing
Two D0a findings on revision 8, both upheld.

1. The classifier was not total. "Clean split" and "mixed" left five
   outcomes unprescribed, and two of them are in the historical logs
   already: 20260815T182846Z-708693 died compiling pmacs so neither
   copy executed, and ...-2839374 / ...-830195 were red on unrelated
   rows while both ctrl_c copies passed.

   A run is now classified from THE TWO COPIES OF THE TARGET TEST and
   nothing else --- green (both ok), red (both FAILED), split (copies
   disagree), void (either did not execute). A sweep red only on
   unrelated tests is therefore a green run, with the unrelated
   failures recorded as evidence about environment stability. A split
   STOPS the procedure, since two copies of one source disagreeing
   within a run is its own defect. Voids are discarded and re-run on a
   budget of 3, after which the environment is too unstable to classify
   anything and D0a stops.

   Endpoint verdicts are uniform green, uniform red, or mixed, and a
   six-row table prescribes every combination: clean split permits the
   bisect; an inverted split is a real difference that falsifies which
   endpoint was believed good; both-uniform-green and both-uniform-red
   each mean the difference is not captured by those commits; mixed at
   either endpoint means intermittency under fixed source and forbids a
   bisect. The manifest had attached "difference is not captured" to
   the mixed case --- that conclusion belongs to the uniform-same rows,
   and is moved.

2. Strict A/B/A/B does not make drift "hit both arms equally": B always
   follows A and owns the final time point. Runs are now counterbalanced
   AB BA AB BA AB, which removes systematic order confounding; the
   residual last-slot asymmetry is accepted and stated rather than
   claimed away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 10:23:43 +02:00
Levi Neuwirth 4e84ff0050
docs(framing): revision 8 --- the superseded one-run rule was still in force
Four findings on revision 7, all upheld.

1. The old one-run D0 rule survived in three durable places --- the
   manifest, this branch's ledger, and the framing's own §4a --- each
   still permitting a bisect when the endpoints merely "differ". That
   contradicts the N = 5 clean-split contract added in revision 7. All
   three now defer to that contract, and §4a's "needs only that the two
   clean endpoints differ now" is marked as the superseded rule it is.

2. D0a still overstated its evidence, in three ways now fixed:
     - "context-sensitive by construction, appearing only in the full
       sweep" is downgraded to what has been OBSERVED so far;
     - the historical 7/7 and 13/13 are stated as NOT endpoint-specific
       rates --- of seven reds only F6 ran at 724b785, of the greens only
       the last at 7599661, both with unknown cleanliness;
     - five runs are named a PREDEFINED EVIDENTIARY THRESHOLD chosen so
       the outcome cannot be argued after the fact, not something that
       mathematically separates intermittency.
   And the bisect now specifies its own classifier: every intermediate
   commit uses the identical N = 5 protocol, and a mixed classification
   ABORTS the bisect rather than being guessed, skipped, or rerun until
   it agrees. A bisect with cheaper steps than its endpoints would
   inherit the weakness the contract exists to remove.

3. The artifacts column is now exact per run, read from each log:
   R1/R2 UNKNOWN (no log preserved), R3 -5d9105cb/-d4dae4f0, R4 and R5
   -6b4b8223 only, R6 -91f51d0b/-6b4b8223. R8's citation was half2.log:1;
   the executable lines are 438 and 459. The framing's last "not same
   binaries" is now "not the same compilations".

4. (Held ledger, 5274d6b.) It named a stale ledger tip and two different
   framing revisions on consecutive lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-19 10:08:11 +02:00
Levi Neuwirth 7110256956
docs(framing): revision 7 --- the ancestry supports no causal claim at all
Five findings on revision 6, all upheld.

1. The ancestry pair supports nothing causal. Revision 6 had already
   retreated to "outcome is not determined by commit alone"; that is
   withdrawn too, because different commits CAN deterministically
   produce different outcomes --- this document's own fix-then-regression
   scenario is an example. The two observations differ in commit AND
   environment AND time, so they are simply NON-COMPARABLE. The held
   ledger's "no source-monotonic cause does that" goes with it.

2. D0a was not a valid decision procedure: one unspecified run per
   endpoint cannot establish a regression for a failure that only
   appears in the full sweep. Now specified --- N = 5 full sweep-crdt
   runs per endpoint, INTERLEAVED A/B/A/B so session drift hits both
   arms, identical captured conditions including uptime/free//tmp/
   leaked-daemon count, and a bisect permitted ONLY on a clean split.
   A mixed result means intermittency under fixed source, and no bisect
   is justified at all.

3. "Neither binary contains signal-handling code" is FALSE. The pmacs
   binary does: install_signal_handlers (src/daemon.rs:628) registers
   SIGINT and SIGTERM; it is simply not on run_gpu's path. A grep of
   project sources also cannot exclude a runtime or dependency
   installing a disposition. The established fact is narrow --- no
   explicit installation on run_gpu's path --- and "whatever disposition
   they hold was inherited" is restored to a HYPOTHESIS that D2 must
   measure.

4. Artifact wording finished: no "artifact family", "reduction/
   workspace artifacts" or "different binaries" remain. Every manifest
   row now carries its exact Cargo suffixes read from its log, with a
   stated caveat that those logs are machine-local and this manifest is
   the portable transcription of them.

5. Held ledger pointed at revision 5; it now points at revision 7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-18 18:03:41 +02:00
Levi Neuwirth e2084fbba5
docs(framing): revision 6 --- the ancestry argument shows less than claimed
Three findings on revision 5, all upheld.

1. The ancestry argument overreached. 72da24a failing today while its
   descendant 7599661 passed on 08-15 shows exactly one thing: outcome
   is not determined by commit alone, since the observations come from
   different environments at different times. Revision 5 said a source
   cause was "positively discouraged", that the ancestry "says to
   expect" equal endpoints, and that the change was environmental.
   None follows. It cannot discriminate an environmental change, a
   source/environment interaction, or a fix before 7599661 with a
   regression before 724b785 --- and an ancestor OUTSIDE the interval
   is irrelevant to whether the interval regressed, since a bisect over
   7599661..724b785 needs only that the clean endpoints differ now.

   D0a is unchanged as an action but is now stated as a decision
   procedure with NO predicted outcome: endpoints differ -> bisect that
   interval; endpoints agree -> ask what else changed across the window.

2. The byte-identity withdrawal was incomplete in both ledgers. This
   branch's said the artifacts "are byte-different" and then withdrew
   it two lines later, still said R9 ran "different binaries", and
   still promised an "artifact family". The held ledger still said
   "byte-different" and still called the window a bisect target with
   revision 4's onset conclusion. Both now say "different Cargo
   suffixes/compilations" throughout; historical byte identity is
   UNKNOWN and is never claimed.

3. Provenance slips: R9's observation-table row listed only -6b4b8223
   although it executed both -91f51d0b and -6b4b8223; R10's suffixes
   are at log lines 3 and 24, not 3 and 4; R9's are at 3066 and 3087,
   not 3066 alone. All corrected against the logs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-18 17:33:23 +02:00
Levi Neuwirth 55053c601a
docs(framing): revision 5 --- the onset is not a source boundary
Four findings on revision 4, all upheld. The third changes what the
lane should do next.

1. Section summaries still carried revision-3 language while the
   manifest carried revision 4's. Framing and ledger now agree: seven
   red runs (F1-F7), not five; the observation table is keyed on
   compilation set rather than an invented "workspace artifact family";
   and it is labelled an observation, not an isolated interaction.

2. The onset count was wrong. Per test copy across the 17 sweep-crdt
   logs: 13 with both copies ok, 1 where NEITHER executed because the
   stage died compiling pmacs (error[E0308]), and 3 with both failed.
   Revision 4's "14 runs, 11 green, 3 red on other tests" mis-stated
   both the count and the kind --- one of those runs never reached the
   test. The two genuinely red-on-other-tests sweeps did execute
   ctrl_c, and it passed.

3. D0a cannot be a source bisect, and the evidence argues against one.
   Reflog and commit times put HEAD at 7599661 during the last green
   (3c06176 landed 40s after it finished) and at 724b785 during the
   first red (5174f73 landed 08:45:41, after that run ended 08:42:01;
   the manifest had recorded F6 at 5174f73, which was wrong).
   Cleanliness was captured at neither endpoint. And 72da24a is an
   ANCESTOR of the passing 7599661 yet fails today --- no
   source-monotonic cause produces that. D0a now reproduces the two
   endpoints CLEAN, in isolated target directories, and a bisect is
   justified only if they differ.

4. Manifest completed: R9 carries full argv rather than a recipe; R7
   lists only gpu_invocation-6b4b8223, since R7 does not select
   gpu_initial_target; R10 lists both -5d9105cb and -d4dae4f0.

Also withdraws "byte-different" everywhere. The bytes a historical run
executed are not knowable --- target dirs have been overwritten, and a
hash computed today is the current occupant's. Three levels are now kept
apart in the manifest: suffix (known), today's bytes at a path (known),
and the bytes a past run executed (UNKNOWN). Differing suffixes mean
differing Cargo metadata hashes, which is enough to void the comparison
and is all that is claimed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-18 16:06:46 +02:00
Levi Neuwirth 9332d5a616
docs(framing): revision 4 --- and the failure has a datable onset
Four findings on revision 3, all upheld. Answering finding 1 turned up
something that reframes the lane.

THE ONSET. sweep-crdt appears SEVENTEEN times in this target directory's
gate logs. The ctrl_c failure appears in exactly the LAST THREE, and the
test passed --- both copies, "... ok" --- inside the stage before them.
Last green 20260815T185708Z, first red 20260816T063330Z, no reboot
between. The three earlier red sweeps failed on unrelated rows. So
"pre-existing on main" holds (F1 at 72da24a reproduces it) but "always
broken" was never established and is now contradicted. D0 gains a first
part: bisect that window. A test that passed fourteen times in this
stage and then failed three times running has a change behind it, and
that is worth more than further reduction --- which has isolated
nothing.

1. Both ledgers still carried the falsified R9 conclusions. This branch
   listed --workspace unification and preceding tests as ruled out
   while the section above described an interaction; said "five call
   sites" immediately before correcting to six; and labelled the
   framing revision 2. The held branch was worse: --workspace refuted,
   R9 "same binaries", later packages not implicable, cause cumulative
   across 37 binaries. All corrected and pushed (5b9abd8). §11 no
   longer asserts the held lane is clean; it records a re-verified
   checklist, since asserting that prematurely is what went wrong.

2. Manifest now carries complete argv for R7-R9 and F5 --- abbreviations
   are not reconstructable invocations. F5 is disambiguated: the
   framing cited gate ...-2144707 while the manifest cited ...-2375685,
   two distinct real runs. Enumerating them gives F1-F7: the red count
   is SEVEN, not five, each with its own log digest. F5 also carries an
   extra failing binary the others do not.

3. "Workspace artifact family" conflated Cargo suffix with byte
   identity and is withdrawn as a grouping. Demonstrated: F1 in the
   main worktree executed the same suffixes -5d9105cb and -d4dae4f0,
   but the bytes there are e0578039/00f06aeb versus the panel
   worktree's 1b3cc86c/ede0c07d. Each run now records the suffix its
   log shows and byte identity as UNKNOWN, since target dirs have been
   overwritten and a hash computed today is not the hash that ran.

4. The interaction table is demoted to a description of what was
   observed. Revision 3 disclaimed its inputs and then asserted a
   finding from them, which cannot both hold. A3 no longer speaks of an
   established "R9 paradox" --- there is none to explain, because the
   comparison was never made; it requires D0 to recreate it first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-18 15:50:53 +02:00
Levi Neuwirth 4e1ca68b4c
docs(framing): revision 3 --- R9 did not run the same binaries
Five findings on revision 2, all upheld. The first invalidates its
strongest claim.

1. R9 executed gpu_initial_target_acceptance-91f51d0b and
   gpu_invocation_acceptance-6b4b8223; the failing sweeps executed
   -5d9105cb and -d4dae4f0. Verified byte-different by sha256. Cargo's
   target selection changes the fingerprint, so command shape changes
   the executable. "Same binaries" is now "same target names and
   order". What the evidence supports is an INTERACTION --- prior
   targets alone green (R9), workspace artifacts alone green (R10),
   both together red (F1-F5) --- so --workspace selection is not
   sufficient by itself and NOT ruled out. The claim that other
   packages "cannot be implicated" because their targets run after the
   failure is withdrawn: later-selected packages can affect the build
   graph and fingerprints before their tests ever run.

2. Both ledgers made internally consistent and portable. This branch's
   asserted default-disposition death and then withdrew it further
   down; the assertion is gone. panel-mapping-generation still carried
   "119 binaries green one red", the >=8s arithmetic, the default-action
   claim and the >6s selector --- corrected on its own branch and pushed
   at 779a6bd.

3. Provenance is now a pushed document, docs/probe-sigint-evidence.md:
   exact command, worktree, HEAD, cleanliness, artifact family, result
   and log digest per physical run. R1 and R2 have no preserved log,
   and revision 2 double-counted one log as both R2 and R6. Cleanliness
   is UNKNOWN for every pre-manifest run and is not inferred. R1-R10
   ran in the panel-mapping-generation worktree, not at main. D0 now
   precedes every other diagnostic: re-run the matrix at main under a
   harness capturing provenance AND the artifact hashes executed.

4. "The probe never blocks indefinitely" narrowed to "the event loop
   wakes at least every 50ms". The stdin reader blocks in read_to_end
   (:1109) and, once ready, the loop leaves only when stdin closes
   (:1212), so the process is not bounded.

5. Launcher call sites: six under --features crdt (:509 :534 :544 :574
   :725 :1097, inside #[cfg(feature = "crdt")] mod crdt). The other two
   --gpu arguments are under #[cfg(not(...))] and compiled out.
   Revision 2 said five while citing eight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-18 15:36:27 +02:00
Levi Neuwirth 9988e974af
docs(framing): revision 2 --- five findings, two of them my own retractions
Revision 1 rejected on five findings, all upheld.

1. The >6s selector could not have captured the failure. Both
   reproducing binaries finish in ~5.19s INCLUDING the 5s timeout
   (:3097, :3131), so the failing launcher lives about 5.1s. This also
   falsifies my earlier retraction, which had argued the instance "must
   live >=8s" --- so the "mechanism located" claim is NOT refuted by
   that argument. It stays unproven for a different reason: the suite
   spawns launchers from five call sites, so command line alone cannot
   attribute one to this test. Key on the PID the test records.

2. Diagnostics rewritten to DISCRIMINATE blocked delivery, inherited
   ignore, and an escaped process group: before-and-after snapshots for
   test parent / launcher / probe, per-thread SigBlk from
   /proc/<pid>/task/*/status, SigPnd/ShdPnd, and PID/PPID/PGID/SID.
   Relatedly, "two processes with default disposition" is withdrawn ---
   SIG_IGN is inherited across fork and survives exec, so absence of
   handler code says nothing about runtime disposition, and inherited
   ignore is the leading hypothesis precisely because the source is
   silent. Revision 1 contradicted its own hypothesis.

3. Counts corrected: 119 green result summaries and TWO red binaries,
   not "119 binaries green, one red". Reductions are now enumerated
   R1-R10 and F1-F5 with command, run count and log each, preserved off
   the tmpfs --- /tmp is a tmpfs and these were nearly lost mid-lane.

4. Acceptance contract corrected: A2 now requires three consecutive
   green runs on the reviewed fixed head of this branch, not on main,
   which is unobtainable before approval and merge; journey step 12(a)
   "closing is clean" is named, since revision 1 reasoned from grade
   movement which §20 warns against; and A5 is explicitly conditional
   on D4, with bet 1 restated as a bet --- the witness uses a wrapper
   and headless probe, not the real GUI path.

5. Portability closed: this branch now tracks
   githubsucks/gpu-probe-sigint-teardown, and panel-mapping-generation
   was pushed to 16cf3a2 so its retraction travels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-18 14:47:48 +02:00
Levi Neuwirth f1992a65d9
docs(framing): open the GPU probe SIGINT teardown lane
`ctrl_c_on_launcher_group_does_not_reach_spawned_daemon` fails in gate
stage `sweep-crdt` with "child did not exit within 5s". It is
PRE-EXISTING on main --- 72da24a fails it in a clean worktree with its
own target dir --- so while it reds, no branch can present a green
sixteen-stage gate, main included. §5b is held behind this lane.

Framing revision 1, and it proposes NO FIX, because the mechanism is not
known. What it does instead is fix the shape of the problem so the next
attempt is not another guess:

  - Ground truth, cited: neither binary handles signals. `run_gpu`
    (src/main.rs:324) blocks in `command.status()` with no handler, and
    grepping all of pmacs-gpu/src for signal machinery returns nothing.
    The probe polls at 50ms. Two processes with default SIGINT
    disposition should both die at once --- this deepens the puzzle
    rather than explaining it, and the framing says so.
  - Ruled out by measurement, with the method for each: load, tmpfs
    (tested by experiment, not argument), leaked daemons, inotify,
    --workspace feature unification, and any specific preceding test.
  - The reduction paradox stated as the problem's real shape: 5/5 in
    the full sweep, 0/N in every reduction, including all 37 preceding
    targets plus the suite.
  - One retracted claim kept as a warning, because it was mine: the
    "mechanism located" report described a healthy teardown. The
    sampler behind it caught 394 launchers with a 5s maximum lifetime
    while the failing instance must live 8s or more.

The first step is diagnostic only: an instrument keyed on the FAILING
instance --- launchers outliving ~6s --- capturing /proc/<pid>/status
signal masks, since SigIgn survives fork and exec while handlers do not.

Acceptance criteria are written now so the fix cannot quietly become
"make the test pass": a demonstrated mechanism with a mutation-tested
witness, sweep-crdt green three consecutive times, the reduction paradox
explained or recorded as unexplained, and no deadline raised or test
skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-18 14:22:32 +02:00
Levi Neuwirth 72da24adea
Merge pull request #240 from levineuwirth/gate-tmpdir-isolation
gate: isolate TMPDIR per invocation — no more override, and a SUN_LEN guard
2026-08-13 20:12:58 +00:00
Levi Neuwirth cf09f5ad3a
docs: correct a misattributed review round and an unmeasured claim
Records only. No behaviour change; the focused suite stays 27/27 and
the full gate stays green.

**THE WITHDRAWN CAUSAL CLAIM WAS ASSIGNED TO THE WRONG ROUND.** The
framing credited it to round 4. It was round 3 --- whose subject line
says so plainly, "canonical ancestry, guard witnesses, and a withdrawn
claim". Round 4 was the four discriminators: it added the
canonical-traversal WITNESS (round 3 had fixed the traversal and
verified it by hand, which is the evidence that does not survive the
next edit), moved the guard rows onto the exact boundary, and covered
both managed areas on cleanup.

Rounds 1 and 2 were re-checked against their commits in the same pass,
and round 1 was also wrong: it carried ONE reserve finding, not two.
The nested-reserve ruling is round 2's. Each round is now described
from what its commit actually did.

**AND THE GATE CLAIMED A TMPFS QUOTA THAT WAS NEVER ESTABLISHED.**
`scripts/gate` said a build here "hit tmpfs quota mid-compile". Nothing
measured that, and it contradicts what was measured later: `/tmp` was
at 24G of a 30G tmpfs with 6G free --- NOT out of space. What actually
happened is worth keeping precisely because it is subtler than a quota:
24G of tmpfs is 24G of RAM, free memory fell to ~4G of 61G, process
spawning became unreliable, and eleven rows failed with EMPTY output.
That reads like a code defect. The comment records the measurement and
the symptom now, and drops the quota story.

The independent reason for a disk-backed TMPDIR is unchanged and is
still the measured one: tmpfs fixtures compete for memory.

Gates: all nine green under `env -u TMPDIR`, log 20260813T190456Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-13 21:10:10 +02:00
Levi Neuwirth da74ae645b
fix(test): the multibyte witness had the wrong axis, and CI proved it
CI went red on three Test jobs with the helper's own panic. That is the
loud failure working as designed --- and the design was wrong.

`${#x}` counting characters is a property of the SHELL first and the
locale second. `bash` counts characters under a UTF-8 locale; `dash`
counts bytes under EVERY locale, having no multibyte handling at all.
`/bin/sh` is `bash` on Arch and `dash` on the Ubuntu runners, so
probing `/bin/sh` and naming a locale established nothing there: no
pair could qualify, and the row could not run.

The helper now probes `(shell, locale)` pairs and the row invokes the
gate THROUGH the qualifying shell. `/bin/sh` is tried first so the real
interpreter is used wherever it qualifies. This is not a contrivance to
get green: `#!/bin/sh` resolves to `bash` on Arch and on macOS, which
is precisely where a `${#VAR}` guard would miscount, and it is the
configuration the guard exists for.

Renumbered, because `M-G-8` was taken. Round 3 assigned it to the
canonical-traversal mutation and the ledger never recorded it, so the
locale exercise reusing the ID was a collision. Canonical `M-G-8` is
restored to the ledger; the locale legs are `M-G-9a-c`. Nine total.

  9a  mutant gate, probed pair -> row fails, boundary row still passes.
      Re-run with /bin/sh EXCLUDED, covering the dash/CI fallback
      path -> still fails.
  9b  SAME mutant gate, pair forced byte-counting -> row passes.
      The defect reproduced rather than argued.
  9c  no pair qualifies -> panic naming shells and locales tried

Record corrections review asked for:

- framing said three rounds and revisions 6a-6c; history is rounds 1-4
  plus this follow-up, and each round is now named for what it fixed
- framing SS2a claimed `${#var}` counts characters under UTF-8 with no
  qualifier --- the same error as the helper's. It now states the shell
  dependence and why the guard measures bytes explicitly.
- the helper's prose said every candidate comes from `locale -a` while
  the code also tried two hardcoded spellings; the doc comment now
  describes what the code does

Gates: all nine green under `env -u TMPDIR`, log 20260813T183646Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-13 20:41:18 +02:00
Levi Neuwirth 3a7e3790a1
test(gate): establish the locale precondition instead of naming one
Review found the byte-versus-character witness asserting something
adjacent to its contract. It set `LC_ALL=C.UTF-8` and assumed the
locale took effect. Locale names beyond `C` and `POSIX` are
implementation-defined, so where that one is absent the shell falls
back to byte semantics --- and then the character-counting mutant
counts bytes too, agrees with the fix, and the row passes while
proving nothing. M-G-6 was killable here and unkillable elsewhere,
which is the same as not having it.

The locale is now chosen by BEHAVIOUR. Candidates come from `locale -a`
so the set reflects what is installed, and each is probed through the
same `/bin/sh` the gate runs under, asking `${#x}` on a two-byte
character and requiring `1`. No qualifying locale is a loud panic
naming what was tried, never a skip: a skip would be indistinguishable
from a pass, which is the failure mode this replaces.

M-G-8 proves the fix in three legs, because the hazard lives in the
environment rather than the code:

  8a  mutant gate, probed locale  -> the row fails, and the
      exact-boundary row still passes
  8b  SAME mutant gate, locale forced to `C` -> the row passes.
      The defect reproduced rather than argued.
  8c  no candidate can qualify -> panic naming the candidates

Also marks framing revision 6 approved and records M-G-8 in the ledger.

Gates: all nine green under `env -u TMPDIR`, log 20260813T182020Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-13 20:25:00 +02:00
Levi Neuwirth 8e1f03a4a0
fix(gate): review round 4 --- four discriminators that constrained nothing
Every item here is the same failure in a different place: a property
fixed or claimed, and a suite that would stay green without it.

**CANONICAL ANCESTRY WAS UNWITNESSED.** Round 3 fixed word-splitting
and lexical traversal and verified both BY HAND, which is exactly the
evidence that does not survive the next edit --- reverting to the
`for _anc in $(... dirname ...)` loop left all 26 rows green. There is
now a row driving a **space-containing root** (an unquoted expansion
tears it into fragments and the real ancestor is never tested, so the
guard passes on the path it must reject) and a **symlinked root** (a
marker visible only after `pwd -P`, which is what `detect_project`
sees). `M-G-8` restores the old loop and fails it. The traversal is
encoded in framing §2a rather than left as an implementation detail.

**THE SOCKET GUARD MISSED ITS OWN BOUNDARY.** The rows generated ~51-
and ~71-byte paths against a 55-byte cutoff, so they constrained the
guard's EXISTENCE and not its VALUE: raising the budget from 103 to 118
would have kept both green. They now hit **exactly 55 accepted and 56
rejected**, assert the measured byte lengths, and check that the
refusal reports precisely one byte over.

**REJECTION-CLEANUP CHECKED ONE AREA OF TWO.** Only `<root>/tmp` was
inspected, so leaking AMBIENT alone would have passed --- and AMBIENT
is created before the guard can measure anything, which is the whole
reason the trap moved earlier. Both areas are inspected now, the
ambient one under the derived per-worktree target whose hashed name the
test does not compute.

**THE MULTIBYTE ROW DEPENDED ON THE INHERITED LOCALE.** Under
`LC_ALL=C`, `${#var}` already counts bytes, so the character-counting
mutant passed and the row's verdict was a property of the environment
rather than of the code. It sets `LC_ALL=C.UTF-8` explicitly;
`M-G-6` now fails even when the harness itself runs under `LC_ALL=C`.

Stale test prose corrected, including one claim that was the very
inference the ancestor check exists to refute: **placement under a
managed root does not make a path marker-free**. Also, the tmp parent
is SHARED between worktrees and untouched by `--prune` (unlike the
ambient root, which is per-worktree), and the module header named
framing revision 4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-13 19:33:51 +02:00
Levi Neuwirth 0a04d55a35
fix(gate): review round 3 --- canonical ancestry, guard witnesses, and a withdrawn claim
**THE ANCESTOR WALK WAS WRONG TWICE OVER.** `for _anc in $(...)`
word-splits on IFS, so a gate root containing a SPACE was torn into
fragments and the real ancestor never tested --- the check passed on
exactly the path it should reject. And `dirname` walks LEXICAL
ancestry while `detect_project` canonicalizes, so a symlinked root hid
a marker the editor plainly sees. The walk resolves with `pwd -P` first
and iterates a quoted `while`; both shapes are verified by hand
(space-containing root refused, symlinked root refused at its real
path).

**THE 103-BYTE GUARD HAD NO WITNESS AT ALL** --- every other row runs
with a short root, so the guard is silent and a broken one looked
identical. Three rows now aim at it deliberately: boundary rejection
and acceptance, a MULTIBYTE root (each `é` is one character and two
bytes, so it is rejected only if the guard measures bytes), and
**rejection must reap both created areas**, which is the leak the early
trap exists to prevent.

**The `Cargo.toml`-DIRECTORY case was claimed and not covered**, and
the consequence is exactly as review predicted: reverting only the
language-marker arm to `[ -e ]` stayed green. The marker-type row now
drives all three shapes, and `M-G-5` --- that precise revert --- fails
it.

**Prose brought level with the implementation.** The framing, the
handoff and the ledger all said 108; the supported floor is **103
usable bytes**, Darwin's 104-byte array minus its NUL. The ledger also
still said `<pid>`, the superseded 21/30 reserve, and `M-G-1`.

**And the ruling said nested gates "do not pay" the reserve, which is
false and would have licensed exempting them.** They pay it in full;
the short layout merely gives them the headroom to satisfy an unchanged
production guard. Reworded, because the wrong version is the one a
future reader would act on.

**THE btrfs CAUSAL CLAIM IS WITHDRAWN.** The draft argued that a
one-second deadline plus a slower filesystem was a plausible new
mechanism for the fourth `managed_retry` occurrence. It does not
survive inspection: the deadline bounds the connection RETRY loop, not
the socketpair handshake that returned `BrokenPipe`, and the filesystem
work happens before it is armed --- the tempdir is created and never
bound. The environmental change is still recorded, as a CHANGE rather
than a mechanism, so a later occurrence can compare like with like.
Recording a mechanism the code does not support is worse than
recording none: the next occurrence gets measured against a story
instead of the evidence. TMPDIR stays disk-backed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-13 17:07:55 +02:00
Levi Neuwirth 465acae547
fix(gate): review round 2 --- the platform floor, the nested reserve, and marker types
**108 IS LINUX'S NUMBER, NOT THE FLOOR.** Darwin's `sun_path` is 104
(xnu `bsd/sys/un.h`) and pmacs supports macOS --- CI runs a
`macos-latest` leg --- so a Linux-derived limit passes on the machine
that writes it and bind-fails on the other. **The usable PATH length is
one less than the array**, because the stored value is NUL-terminated:
103 on Darwin, 107 on Linux. The script takes **103**, and the
diagnostic says which platform's floor it is quoting.

**THE NESTED CASE IS NOW RULED, NOT ACCOMMODATED BY LOOSENING THE
GUARD.** The reserve exists for fixtures that bind sockets under
TMPDIR; this script's own behaviour suite runs nested gates whose plans
are synthetic and bind nothing, so charging them the fixture reserve
rejects a configuration that cannot suffer the failure it guards
against. Exempting nested runs was rejected --- it makes the guard
untestable in the very configuration the tests exercise, and "this run
is nested" is not reliably knowable. **The suite roots its gates at a
short base instead**, so a nested TMPDIR is ~24 bytes rather than ~71
and clears the real reserve. Recorded in revision 6 with the rejected
alternative, and with the obligation that a future row which DOES bind
a socket must move off that base and take the reserve with it.

**MIRRORING THE MARKER NAMES WAS NOT ENOUGH; THE TYPES ARE PART OF THE
CONTRACT.** `match_marker` requires `.git` to be a DIRECTORY and the
seven language markers to be FILES, so `[ -e ]` rejected ancestors
project detection walks straight past. The case is not exotic: **a git
WORKTREE has a `.git` FILE**, so every worktree in this repository
would have tripped the guard. It tests `[ -d ]` for `.git` and `[ -f ]`
for the rest, with a witness covering all three shapes --- `.git` file
accepted, `.git` directory refused, `Cargo.toml` directory accepted.

That witness keys on WHICH marker the gate named rather than on whether
a refusal happened, because the ancestors of any base a test can create
are outside its control; "no refusal" is not a claim it can make
anywhere, while "the refusal did not name MY file" is. `M-G-4` reverts
the guard to existence-only and the row fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-13 16:52:26 +02:00
Levi Neuwirth 30e976ae1e
docs,test: record the fourth managed_retry occurrence, and unblock the nested suite
**The gate's own run reproduced a REGISTERED signature**, and it is
recorded as a fourth occurrence rather than waved through: same
selector, same `gpu`-step flavor, all three required fragments verified
against the durable log. Three isolated re-runs were green, which this
file's rule says establishes intermittence only.

**This lane is code-neutral for `pmacs-gpu` but NOT
environment-neutral**, and that distinction is the entry's point.
Occurrence 3 excluded "the added GPU test is the mechanism"; this
occurrence adds nothing to that binary at all, which corroborates the
exclusion independently. But the lane moves `TMPDIR` off `/tmp`, taking
every `tempfile::tempdir()` in the run from **tmpfs to btrfs** --- and
the failing test runs a handshake against a **one-second deadline**. A
slower filesystem under a timing-bounded test is a plausible mechanism
that did not exist in occurrences 1-3. Booking this as "the usual
flake" when the observing lane changed the conditions the flake is
sensitive to is exactly the reasoning this registry exists to prevent.

Also: the suite's own roots move to a short base. Rooting them under
the ambient `TMPDIR` put a NESTED gate's TMPDIR near 70 bytes, which
legitimately tripped its own SUN_LEN guard --- the suite failing on a
configuration it created rather than on the behaviour under test. And
the marker row's `.then(..).unwrap_or_else(..)` chain is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-13 16:43:05 +02:00
Levi Neuwirth 1bd52b7f0d
fix(gate): review round 1 --- the propagation row proved nothing, and two guards were wrong
**THE PROPAGATION WITNESS DID NOT OBSERVE INHERITANCE.** The runner's
`eval` expanded `$TMPDIR` in the PARENT before `sh -c` ever started, so
the child received an already-substituted literal --- and an unexported
`TMPDIR=` would have passed the row unchanged. Single-quoted inside
`sh -c` now, so the CHILD expands it. **M-G-1b keeps the assignment and
removes only `export`: the row fails.** That is the mutation the
previous version could not catch, and the reason to prefer it over
M-G-1's blunter deletion.

**THE RESERVE WAS NOT THE MAXIMUM.**
`/.tmpXXXXXX/directory-target.sock` is 33 bytes
(`tests/gpu_invocation_acceptance.rs`), so paths of 76-78 passed the
30-byte guard and still blew the 108-byte limit during the CRDT sweep.
Reserve is 48 now --- the measured maximum plus ~45% headroom. And the
length is counted in BYTES: `${#var}` counts CHARACTERS under a UTF-8
locale while `sun_path` is byte-limited, so a multibyte path measured
short and passed a check it should fail.

**A MANAGED ROOT IS NOT INHERENTLY MARKER-FREE**, and assuming it was
rebuilt the original defect one directory up: a `.git` in `$HOME`, a
marker above `$HOME/build`, or a contaminated
`PMACS_GATE_TARGET_ROOT`. Placement under a directory the gate owns is
NECESSARY, NOT SUFFICIENT, and the old test proved only placement. The
gate now walks the ancestors and refuses, naming the marker it found.

`PMACS_GATE_ALLOW_ANCESTOR_MARKER` is the documented test-only escape,
beside `PMACS_GATE_TARGET_ROOT` in kind and risk: the behaviour tests
run under a tempdir whose ancestors they do not control, on a machine
whose `/tmp` carries this very marker, and their plans are synthetic so
no markerless fixture exists to re-root. **The check is witnessed by a
row that deliberately does not set it**, and M-G-3 (check removed)
fails that row.

**The guard leaked what it exists to manage.** It created both
temporary areas and exited before the trap was armed, so every
rejection left an AMBIENT and a TMPDIR behind. The trap is installed
first now; verified by rejecting a run and finding neither.

**`tmp/$$` with `mkdir -p` was not fresh.** PIDs are reused, so after a
SIGKILL it silently ADOPTS a leftover directory and the run inherits
another run's fixtures. `mktemp -d` fails rather than reuses.

**Prose corrected to match.** The handoff described
`<target>/gate-tmp/<stamp>-<pid>`; the implementation uses
`<gate-root>/tmp/<mktemp>`. Comments called the shared parent
per-worktree and pruned --- it is neither: `--prune` only considers
directories carrying an ownership marker, so the parent is skipped and
each run removes its own leaf.

**AND THE LANE CLAIMED A FRAMING EXCEPTION THAT DOES NOT EXIST.**
`AGENTS.md` says framing -> approval -> branch -> implement,
unconditionally; "the fix was already recorded as standing" is not an
exemption it grants. `docs/gate-script-framing.md` is amended as
**revision 6, AWAITING APPROVAL** --- a widening of §2's existing
isolation responsibility rather than a new feature, which is why it
amends that document instead of opening another. **This PR must not
merge before that revision is approved.**

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-13 16:34:21 +02:00
Levi Neuwirth 72647829df
docs: record PR #240 in the gate lane
The number goes in the moment the PR opens, per this file's own rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-13 16:00:17 +02:00
Levi Neuwirth d84df23aa7
fix(gate): isolate TMPDIR per invocation
Discharges the standing fix recorded in `docs/agent-handoff.md` §1 and
assigned to this lane. Every gate invocation now gets a fresh,
disk-backed `TMPDIR` at `<gate-root>/tmp/<pid>`, exported once so every
stage and every process they spawn inherits it, reaped by the same exit
trap as the ambient root. **A gate run no longer needs a `TMPDIR=`
override.**

**A CHILD OF `/tmp` WOULD NOT HAVE WORKED**, which is why the obvious
cheaper fix was not taken. The hazard is an ANCESTOR marker: project
detection walks upward, so a fresh subdirectory of `/tmp` inherits
`/tmp`'s ancestors and the same stray `.git`. The directory had to move
somewhere the gate already owns.

**`SUN_LEN` shaped the layout, and the fix's own gate run is what found
it.** A Unix socket path cannot exceed 108 bytes, and the suites bind
sockets INSIDE `TMPDIR`. The first placement --- `$TARGET/gate-tmp/$STAMP-$$`
--- produced a 114-byte socket path and failed SIX daemon and attach
tests with "path must be shorter than SUN_LEN". It hangs off the gate
root (36 bytes) rather than the per-worktree target (60) now, with a
short name: 47 bytes, leaving 61 for fixtures. Running the real gate
rather than only the witnesses is what caught this.

**A startup guard turns that failure class into a named one.** Six
socket failures deep in a suite name a LIMIT, not a CAUSE; the guard
fails immediately with the path, its length, and what to shorten.

**Its reserve is measured, not round, and the first value was wrong in
the more embarrassing direction.** The longest suffix a fixture appends
is `/.tmpXXXXXXX/test.sock`, 21 bytes, so 30 leaves ~40% headroom. An
earlier "generous" 45 FIRED ON THE GATE'S OWN BEHAVIOUR TESTS: they run
the gate inside the gate, so their root sits under the outer run's
TMPDIR and the nested path reaches 71 bytes. A guard that rejects a
legitimate configuration is worse than the failure it prevents, because
it fires on every run instead of a rare one. Verified both directions:
still catches an 87-byte root, silent on the real one and on the nested
tests.

**Two witnesses, each mutation-checked.** `M-G-1` removes the export ->
the propagation row alone; `M-G-2` stops the reaping -> the cleanup row
alone. Propagation is observed in a SPAWNED CHILD --- the self-test's
first step reports its own `$TMPDIR` into its log --- because asserting
the variable inside the script would only prove the script can set a
variable. The cleanup row runs against the self-test, which FAILS on
purpose, so it also pins that the trap fires on the failure path, which
is the path a leak would actually happen on.

One witness of mine needed correcting twice, both times because it
asserted something adjacent to the contract: first `!starts_with("/tmp/")`,
which tested where the FIXTURE put its root and failed on correct code;
then `contains("/gate-tmp/")`, stale after the directory was shortened.
It now asserts the exact parent, `<root>/tmp`.

**Proved against the live hazard:** `/tmp/.git` is still present on this
machine, and `m4_24_bare_string_glob_stays_relative` --- one of the two
tests it reddened --- passes with no override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-13 15:55:19 +02:00
Levi Neuwirth ca92796c71
Merge pull request #239 from levineuwirth/gui-stage1a-textinput
GUI Stage 1a — TextInput at protocol v24: multi-scalar input stops truncating
2026-08-13 13:00:13 +00:00
Levi Neuwirth 36a32961f2
docs: record PR #239 in the 1a lane
The number goes in the moment the PR opens, per this file's own rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-13 08:44:42 +02:00
Levi Neuwirth 2d40aea509
docs: state the PanelPointer pin's blast radius exactly
The comment claimed every v6-v23 daemon decodes the variants before
`PanelPointer` on every session. False on both counts: `PanelPointer`
arrived at v21 and is gated, so a wedge before it shifts its
discriminant and breaks v21-v23 PANEL traffic --- not v6-v20, and not
every session.

The pin itself is unchanged and still fires (15 -> 16). What was wrong
was the justification, and an inflated one is not harmless: it sends the
next reader hunting a larger blast radius than exists, and it teaches
the wrong rule about which populations an insertion actually reaches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 22:56:41 +02:00
Levi Neuwirth 69b9e6a41f
docs: record 1a's gate result and mutation inventory
Sixteen gates green with --protocol, under an isolated TMPDIR.

The crdt-gated wire suite is verified to have RUN --- 2 tests in the
crdt sweep, 0 in the default one --- rather than assumed, because a
suite that compiles to nothing reports ok and that is the shape this
project has been caught by before.

The mutation inventory names which row each one fails, including
M-1a-6: a mutation that targeted the wrong side of the append boundary
and so reported a sound pin as vacuous.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 22:55:55 +02:00
Levi Neuwirth 76beb79a4d
test: 1a --- A1-A4 witnesses and the PanelPointer frozen-byte pin
**A1 is an exhaustive loop over all 35 function keys, not spot checks.**
The defect it closes was `_ => return None` swallowing F13 upward, and a
test covering F1-F12 would have passed against exactly that. Each row
also asserts `should_forward_key`, because translating without
forwarding leaves a key mapped and inert --- which reads as a daemon
keymap gap rather than a frontend one.

**A2 asserts both halves**: `BackTab`, and `Shift` still set. A
`BackTab` that lost its modifier is indistinguishable from one the user
did not shift. **A3** likewise pairs the mapping with forwarding.

**A4 establishes idle rather than asserting it.** A fresh `State` starts
with `dispatch_idle` false --- the daemon has not spoken yet --- so the
first version of the row asserted the precondition and failed. Had it
been written the other way round it would have tested the INTERCEPTING
case under an idle name, which is the state where Escape never quit
anyway: the row would have passed while proving nothing about the
behaviour A4 changes. It now sets idle, confirms nothing intercepts, and
asserts both halves: the Escape reaches the daemon AND no exit occurs.

**The frozen-byte pin sits on `PanelPointer`, not on `TextInput`, and
the placement is the point.** `TextInput` is appended, so its own
round-trip is byte-identical whether or not a variant was inserted
beneath it; only the PREVIOUS final variant's bytes move. Every v6-v23
daemon decodes the variants below `PanelPointer` on every session, so an
insertion anywhere earlier is a silent wire break for all of them.

MY FIRST MUTATION OF THAT PIN WAS WRONG AND THE PIN WAS RIGHT. I
inserted the wedge variant before `TextInput` --- which is to say AFTER
`PanelPointer`, exactly where an append belongs --- and the pin passed,
correctly, because nothing shifted. Re-run with the wedge BEFORE
`PanelPointer`, it fails with the discriminant visibly moving 15 -> 16.
Worth recording because a mutation that targets the wrong side of the
boundary reports the pin as vacuous when it is sound.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 22:46:00 +02:00
Levi Neuwirth 42a0a91f58
test: A8's contrast waited for a prefix, not the sequence
**A partial write could have failed correct code.** The contrast broke
out of its poll as soon as `ESC[200~` appeared and then required
`pasted` and `ESC[201~` in the same breath --- but PTY delivery and the
child's writes split anywhere, so the closer may simply not have
arrived. A flake that only ever fires on a loaded machine, and one that
would have been read as an A8 regression.

It now polls for the COMPLETE `ESC[200~pastedESC[201~`, which makes a
partial write indistinguishable from "not yet" --- which is what it is.
Same rule the vterm suite already follows when it waits for `row19`
rather than for a prefix of it.

**The typed-text read above it is sound and is now documented as such**,
because the two loops look alike and are not. Its gate is a LOWER BOUND
ON LENGTH: a split delivery keeps waiting rather than being mistaken for
a wrong answer, and the exact-equality assertion afterwards can still
fail for the real reason. A wait-for-exact-content loop there would have
been tautological. Stating the difference beside the two loops is worth
more than making them superficially uniform.

M-1a-4 re-run after the change: still fails the row, so the fix removed
a race without removing the discrimination.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 22:38:45 +02:00
Levi Neuwirth e3a19a4e63
test: 1a --- A8 raw PTY delivery, and the negotiated-v23 refusal
The last two of the three discriminators. Each is proven by a mutation
that reinstates the defect it exists to catch.

**A8, delivered rather than routed.** The producer row proved a
terminal-focused session reaches the `TextInput` branch; this one
observes the CHILD PROCESS. A real PTY runs `printf '\033[?2004h'; exec
cat > FILE`, so the terminal turns bracketed paste ON and then records
exactly what arrives on its stdin.

**The enabled mode is the entire precondition**, and the row waits for
the child's own mode-set to be parsed before typing rather than assuming
it: with bracketed paste OFF, "no markers" is true of every code path
including a paste, so the assertion would pass against the behaviour it
forbids. The contrast closes it from the other side — through the SAME
terminal in the SAME mode, a paste IS bracketed. One path marked and the
other not, both observed at the PTY.

`M-1a-4` routes typed text through `encode_paste` and the row fails with
the forbidden bytes in hand:
`"\u{1b}[200~héllo\u{301}\u{1b}[201~"`.

**The negotiated-v23 refusal** gets its own suite, because it needs a
live daemon. A refusal is the hardest claim to witness honestly —
"nothing happened" is also what a dead daemon, a desynchronized stream
or a broken test look like — so the row pairs it with a POSITIVE CONTROL
on the same session: after the refused `TextInput`, an ordinary `Key`
that must take effect. Events from one session are processed in order,
so the control's edit arriving with no preceding `REFUSED` edit means
the gate fired rather than that the daemon was asleep.

Its complement runs the same traffic on a v24 session and requires the
edit to land, so the pair cannot be satisfied by `TextInput` being
broken outright.

`M-1a-5` disables the inbound gate and the v23 row fails with `REFUSED`
visible inside the CRDT op — a v23 peer editing a buffer through a
variant its session never declared, which is precisely the hole review
round 1 identified.

One setup lesson, recorded because it cost a red: the A8 row first
failed with an empty file, and the cause was that the frontend's view
was never pointed at the terminal buffer, so `active_terminal_key`
returned `None` and the DOCUMENT path ran. It now asserts that
precondition through public state before typing — a row that quietly
tests the document path and reports a terminal result is worse than one
that fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 21:29:48 +02:00
Levi Neuwirth 3ed37f7786
test(gpu): 1a --- producer reachability, and a narrowed structural exception
The first of the three remaining discriminators: **multi-scalar text
reaches the wire as `TextInput` while the daemon is INTERCEPTING** ---
the state a modal prompt or a focused terminal puts the session in, and
the state under which A7 and A8 were unreachable before review round 1.

**Getting there required narrowing the 1-pre exception, which is the
substantive part.** `apply_keyboard` took a `&KeyEvent`; `KeyEvent`
carries a `pub(crate)` field and cannot be constructed outside winit, so
the body was undrivable and only the pure classifier could be tested ---
which is precisely why the defect survived: the classifier was correct
throughout and the CALL SITE was wrong. It now takes the two fields it
actually reads, `&Key` and `Option<&str>`, both ordinary constructible
values.

The exception does not disappear, it shrinks: the router arm still
cannot be handed a `WindowEvent::KeyboardInput`, so what remains
unwitnessed is one pattern arm containing a match and a call. That is
recorded on `apply_keyboard` itself, where the next reader meets it.

**M-1a-3 reinstates the original defect** --- the selection moved back
below the intercept return --- **and fails the new row alone**, 23 of 24
still green. That is the shape the review asked for: a witness that
fails for the reason the defect existed.

Its complement is included so the pair cannot be satisfied by sending
`TextInput` for everything: a SINGLE scalar while intercepting still
travels as `Key`, which is §5 rule 4 preserving mode keymaps and typed
provenance.

The harness gains `feed_keyboard`, and the local-effect diffing it
shares with `feed` is extracted rather than copied --- two entry points
observing different effect sets by accident is the kind of divergence
that makes a transcript lie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 17:34:56 +02:00
Levi Neuwirth b2a273b1fd
docs,test: 1a review round 3 --- an overclaimed first, and a vacuous row
**"The first frontend→instance extension needing a gate in both
directions" was false**, and checking it took one grep: the v19 terminal
family and the v21 panel family each have an inbound variant AND an
outbound one, each gated. "First inbound-only extension" does not hold
either --- v7's `TripleDown` and v11's `Context` are inbound-only
`PointerKind` variants.

So the superlative is gone rather than weakened. The rustdoc now states
the PROPERTY and its consequence: the gate is producer-side and
receiver-side, and for an inbound variant the receiving half is the
load-bearing one, because withholding would otherwise be the peer's job
and a client built from this same crate can encode the discriminant
whatever it negotiated. The v19/v21 precedent is named so the shape does
not read as novel; what is unusual is only that this extension has no
outbound counterpart, so the receiver check is the whole of the daemon's
half.

**`multi_scalar_text_input_creates_no_typed_provenance` proved half of
what its name claimed.** It started from a fresh editor, where the
command chain is ALREADY empty, so asserting emptiness afterwards passed
whether or not `break_command_chain` ran --- the assertion could not
fail for the reason it existed.

Split in two. The record half keeps its name and its claim; the chain
half is a new row that PRIMES the chain first and then asserts it
cleared. Priming goes through 1a's own single-scalar path, because
`pmacs.command.invoke('buffer.self-insert')` cannot prime it: rotation
belongs to the dispatcher and invoking the command directly deliberately
never rotates --- the first attempt at this row failed on exactly that
precondition, which is a better outcome than passing on a chain that was
never live.

**M-1a-2 deletes `break_command_chain` from the multi-scalar branch and
fails the new row alone**; the other six stay green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 17:29:21 +02:00
Levi Neuwirth 97da79f136
fix: 1a review round 2 --- provenance was armed but never completed
**The P1 was real and my previous fix only did half of it.** Arming and
completing a `TypedEditRecord` are separate steps: `typed_edit_complete`
is called from `insert_char` / `insert_char_over_region` and NOWHERE
else, so routing the single-scalar branch through the generic
`insert_text_input` left the arm holding `None`. `this_command` rotated
correctly and `buffer.after-edit` saw no record --- auto-pairing stayed
broken while the command side looked right, which is the failure mode
that hides longest.

The single-scalar branch now goes through `insert_char_over_region(ch)`,
which handles the no-region case itself by delegating to `insert_char`.
`insert_text_input` is documented as the MULTI-scalar path only, with
the trap named at the definition rather than left for the next caller
to rediscover.

**The witness consumes the record rather than inspecting
`this_command`**, per the review. `single_scalar_text_input_produces_a_consumable_typed_edit_record`
takes it through the same `pmacs.pair._last_record` seam `pair.lua`
uses, and `single_scalar_text_input_auto_pairs_like_a_keypress` states
the same fact in the terms a user would notice: typing `(` must produce
`()`. **Mutation M-1a-1 reverts the fix and both rows fail**; the four
others stay green, so they are discriminating rather than duplicated.

**A ceiling tripwire proved less than it claimed.** The discovery
acceptance looped `6..=23` and then rejected `PROTOCOL_VERSION + 1`, so
a supported set that ENDED at 23 would have passed while
`PROTOCOL_VERSION` was 24 --- the accepted half said nothing about the
version the constant names. It runs to `PROTOCOL_VERSION` now.

**The public protocol history stopped at v23 while both constants
already included 24.** The rustdoc above `PROTOCOL_VERSION` and
`SUPPORTED_PROTOCOL_VERSIONS` now carries the v24 bump, and states the
thing that makes it unlike its predecessors: **it is the first
FRONTEND->INSTANCE extension needing a gate in BOTH directions**,
because the producer withholding is not enough when a peer compiled from
this same crate can encode the variant whatever it negotiated.

Three typed-edit doc sites said the arm is set by "the dispatch fallback
only" or named two producers; there are three now, and the single-scalar
`TextInput` path is one. `typed_edit_arm`'s own doc gains the warning
that arming is only half.

Also 1a's suite: A6 (one commit, one edit, one undo unit), A7 (a prompt
accumulates scalars in order), and A9's boundary row --- a payload
exactly at the cap lands intact, the complement of the rejection that is
enforced where a test can reach it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 17:20:15 +02:00
Levi Neuwirth 266bc6e4f3
fix(gpu,daemon): 1a review round 1 --- three P1s, and the version fallout
**A7 AND A8 WERE UNREACHABLE FROM THE REAL PRODUCER.** The intercept
branch sends a truncated `Key` and returns, and TextInput classification
sat below it --- but a modal prompt or a focused terminal is exactly
what makes `daemon_intercepts_keys` true, so the two contracts about
prompts and terminals were reachable only when neither was present. The
selection moves ABOVE the intercept return, where the producer sends the
same `TextInput` in every state and the daemon applies the modal
precedence, which is where it belongs: the frontend cannot see which
shadow is up.

Ordering against the branches below is safe by construction rather than
by luck --- `text_input_payload` returns `None` whenever a command
modifier is held, so Ctrl-V and command chords can never be shadowed.

**A pure `text_input_payload` test cannot catch this**, which is the
lesson worth keeping: the classifier was right the whole time and the
call site was wrong. The witness has to drive `intercept = true` and a
terminal.

**SINGLE-SCALAR PROVENANCE WAS PROMISED IN A COMMENT AND NOT
IMPLEMENTED.** §5 rules that a single-scalar commit is indistinguishable
from a keypress; the code only broke the chain for multi-scalar and
called a generic insert, so `this_command` went stale and no
`TypedEditRecord` was produced. Auto-pairing (Q#AP9) and every other
typed-edit consumer would have silently stopped recognizing GUI input
--- surfacing as "auto-pair stopped working in the GUI", far from its
cause. Now runs the real machinery: `rotate_command("buffer.self-insert")`
-> `typed_edit_arm(ch)` -> the one edit -> `typed_edit_finish` ->
`typed_edit_set_armed` -> `buffer.after-edit` -> clear, which is the
tail `dispatch_key` already runs.

**THE PRODUCER GATE WAS ONLY HALF THE WIRE CONTRACT.** The daemon
accepted `TextInput` from every installed session, so a peer negotiated
at v6-v23 --- compiled from this same crate, and postcard will happily
write the discriminant --- could mutate a buffer through a variant its
own session never declared. Now gated on the AUTHENTICATED session's
negotiated version.

**A4's structural half is implemented, not just its behaviour.**
`apply_keyboard` returns `()`, so `LifecycleRoute::Exit` is the sole
`EventOutcome::Exit` producer and the obsolete keyboard-exit channel is
gone rather than merely unused. The type survives, as ruled: one
producer is not one variant.

Also: `dispatch_text_input`'s rustdoc claimed a boolean return that its
signature does not have.

VERSION FALLOUT, SORTED RATHER THAN RENUMBERED.

Six deliberate tripwires took the conscious edit they exist to force
(protocol.rs, bottom-panel, discovery x2, statusline, and the vterm one
that was missing from my inventory). Two carried the version in their
NAME, so the name moved with the number rather than being left to lie.

Two ceiling assertions --- `!is_supported_protocol_version(24)` ---
now probe `PROTOCOL_VERSION + 1`, so they keep meaning "the set ends at
the current wire" instead of needing a hand-edit every bump.

`m4_6_handshake_accepts_v6_peer` was GENUINELY DEFECTIVE and is the one
real find: its name and the M4.6 contract say **v6 is the floor**, but
its body asserted `is_supported_protocol_version(PROTOCOL_VERSION)` ---
"the current wire accepts itself", a different and far weaker claim that
would have kept passing after v6 was dropped from the supported set,
which is the only regression it exists to catch. Anchored on literal 6.

The M10 pair needed no edit: they already use `PROTOCOL_VERSION`, and
they failed in the first sweep only because it predated the
`SUPPORTED_PROTOCOL_VERSIONS` fix.

`ADVERTISED_PROTOCOL_VERSION == 20` did not fire, as it must not.

Full `--workspace --no-fail-fast` sweep clean under an isolated TMPDIR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 17:05:55 +02:00
Levi Neuwirth 211241a437
feat(gpu,protocol): 1a --- A1-A5 and the v24 TextInput variant
The mechanism, without its witnesses yet; tests follow in the next
commits.

**A1-A3 were mapping gaps, and forwarding was half the fix.**
`translate_key` gained F1-F35, Shift+Tab -> `BackTab`, and
`ContextMenu` -> `Menu`. All three already existed in the protocol
`Key` enum and the TUI already sent them, so this closes a divergence
rather than inventing a convention. **`should_forward_key` had to learn
them too** --- translated but unforwarded, they would have mapped
correctly and still done nothing, which reads as a daemon keymap gap
rather than a frontend one. They forward with ANY modifier, like motion
keys: they are command keys that never insert text, so the
chord-withholding rule has nothing to protect them from.

F-keys are an exhaustive match, not arithmetic off `F1`: winit's
`NamedKey` is `#[non_exhaustive]` and its ordering is not a contract, so
an offset would corrupt silently the day a variant is inserted.

**A4 --- every Escape now reaches the daemon and none exits.** The
`intercept || completion_open` test went with the quit branch: it never
decided what to SEND (both arms sent the same `Escape`), only whether to
send at all, and with one behaviour left there is nothing to choose.
Both flags remain live for the OS-paste, round-trip and
completion-accept paths.

**The v24 wire variant is APPENDED and the reason is postcard.** It
encodes a variant by positional index, so widening any variant above
would re-interpret every older peer's bytes. `TextInput` carries an
untrusted `frontend_id` like its neighbours --- the daemon uses the
authenticated source --- plus the text.

**It is not `Paste`, and the difference is behavioural.** A terminal
receives it as RAW UTF-8, never bracketed (A8): a shell that sees
`ESC[200~` treats input as pasted and changes how it handles newlines
and completion. The clipboard slot is untouched, because nothing was
copied. And the document path is ONE edit (A6) --- one undo unit, one
`buffer.after-edit`, one eligible CRDT op --- which is the entire reason
the variant exists, since a two-scalar grapheme sent as two keypresses
is two undo units that a remote edit can interleave.

**A5's precedence is a pure function** (`text_input_payload`) so the
eight rules are testable without a window. A keypress stays `Key` unless
a rule moves it, and only printable MULTI-scalar moves; the version gate
WITHHOLDS rather than degrades, so a `< 24` daemon keeps exactly the
behaviour it has, truncation included.

**A7's ordering falls out of routing through the existing shadow
handlers** one scalar at a time, rather than reaching into prompt state:
history, completion and acceptance stay in one place.

THE 1-PRE EFFECT HARNESS CAUGHT A REAL DEFECT IN THIS COMMIT. Bumping
`PROTOCOL_VERSION` to 24 while leaving `SUPPORTED_PROTOCOL_VERSIONS` at
`..=23` made the handshake reject its own version. All NINE effect rows
failed while the thirteen routing rows passed --- the M21 signature,
meaning `EffectHarness::new` could not attach at all. A pure-routing
harness would have stayed green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 16:35:21 +02:00
Levi Neuwirth cd888bed5e
docs: Stage 1a --- re-measure the framing's ground truth, and open the lane
Docs only, and the first commit on the 1a branch. **No ruling changes.**

**1a needs no framing of its own** ---
`docs/gui-stage1-input-framing.md` already governs every Stage 1 slice,
and A1-A9, the eight Q#S1-9 precedence rules, §8's wire contract and
§11's gates are ruled there. What 1a does need is §2 to be TRUE, and it
was measured at `a994f37`, before 1-pre moved almost every GPU-side
coordinate in it. A framing whose ground truth points at the wrong lines
is how an implementation ends up arguing with the tree.

Re-measured at `4f77491`, and split by kind rather than renumbered
wholesale, because the two are not the same claim:

  STILL TRUE   `FrontendEvent`'s sixteen variants, `PROTOCOL_VERSION =
               23`, IME's zero occurrences, the TUI wheel arms (1-pre
               touched only `pmacs-gpu`), the handshake preceding the
               window, and `Outbox::enqueue`'s closed-return.
  MOVED        `window_event` `:2734`/655 lines -> `:4450`/FOUR lines;
               `translate_key` `:10975` -> `:12053`; "eight arms, rest
               fall to `_`" -> three family decision functions over nine
               variants. **1a edits `apply_keyboard` and
               `translate_key`, not `window_event`.**

**TWO CLAIMS WERE WRONG AT BOTH ANCHORS, so they are corrections rather
than drift.**

**"`KeyEvent.text` is never read" is false, and was false at `a994f37`
too** (`:2800` there, `main.rs:3251` now): the AltGr rule reads it via
`is_layout_text(key.text.as_deref(), pmods)`. The true claim is
narrower and is the one that matters --- **`text` is never read as the
text a keypress INSERTS**, only as a discriminator separating AltGr from
a command chord.

That is load-bearing for A5 rather than cosmetic. §5's rule 2 already
exempts "printable Ctrl+Alt recognized by the existing AltGr rule", so
the precedence table depends on code the section claimed did not exist.
**1a widens `text` from discriminator to payload, and that is the actual
change of kind** --- which "text is never read" hid, along with the one
place the new payload must leave undisturbed.

**A4's exit site moved without its behaviour changing.** 1-pre routed
the idle-Escape quit through `EventOutcome`, so **A4 edits a branch and
a return type in `apply_keyboard` (`main.rs:3219`) and does not touch
`window_event`**, which holds the crate's only executable
`event_loop.exit()` (`main.rs:4452`). And **`EventOutcome` survives
A4**: a native close still returns `Exit`.

The lane records the v24 serialization constraint and the bump's blast
radius up front --- eight version-sensitive failures across six suites
on the last bump, of which CI showed one, because cargo stops at the
first failing target --- together with the rule for sorting them and the
pin that must never fire, `ADVERTISED_PROTOCOL_VERSION == 20`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 14:29:04 +02:00
Levi Neuwirth 4f77491bca
Merge pull request #238 from levineuwirth/docs-absorb-237
docs: absorb #236 and #237 — anchors to d038f71, recovery procedure re-run
2026-08-12 12:17:19 +00:00
Levi Neuwirth 549e225505
docs: remove the #238 lane --- the promised final pre-merge commit
The lane recorded this PR while it was under review, because an open PR
without a lane is the #171 defect: it drifted 153 commits while
invisible in the ledger. Review is done, so the lane goes.

**It had to go in a commit rather than "at merge".** GitHub merges the
tree as it stands, so leaving it would have landed the words *PR #238
OPEN* on `main` --- a lane advertising an open PR that closed in the
same commit, which is the exact staleness class this absorption exists
to clear.

Rule 4 takes it **outright** rather than rewriting it: a lane is
rewritten when its ARC survives its PR, and this one describes a docs
pass with nothing left behind it. Every reference to #238 and to the
`docs-absorb-237` branch lived inside the lane, so the deletion is
self-contained and leaves no dangling pointer.

What the lane recorded that outlives it is already elsewhere: the
recovery rehearsal and its floor checks are in "Repository authority",
and the GUI arc's state is in the Stage 1 lane and the handoff anchor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 13:54:26 +02:00
Levi Neuwirth 6d6942e068
docs: absorption review round 1 --- seven findings, two of them absorption holes
**The absorption was incomplete, and the two holes were Stage 0's.**
`docs/agent-handoff.md` §6 still called Stage 0 "READY FOR PR (not yet
opened)" and the ledger's Stage 0 lane still handed out branch-recovery
instructions and called itself pre-PR --- for work that merged as #236.
A merged lane that still says "checkout the branch" sends a reader to a
tip that no longer moves. Both rewritten; the recovery lines are removed
rather than updated, because there is nothing left to recover.

**A LANE CANNOT REMOVE ITSELF AT MERGE, and saying so was wrong in a way
that would have shipped.** GitHub merges the tree as it stands, so
"removed at merge" would have landed the words *PR #238 OPEN* on `main`
--- a lane advertising an open PR that closed in the same commit. It
stays while the PR is under review, because an open PR without a lane is
the #171 defect, and **a final pre-merge commit on this branch takes it
out**. That obligation is now written into the lane itself.

**Q#GA5 was recorded backwards.** The P2 row said P2 was "gated by the
GUI arc reaching Stage 4b". The ruling is the reverse: **P2 is blocked
by nothing and may start now**; when the arc reaches Stage 4b, P2 must
already have started, and **no later GUI stage begins** until it has an
approved framing and an opened lane. The gate is on STARTING P2, not on
finishing it, and it binds the GUI arc rather than P2. Corrected in both
the P2 row and the GUI row, which had inherited the same inversion.

**`9a26ac8` was the previous last-EXERCISED anchor, not the previous
declared FLOOR** --- that was `e67ad07`, and the two had drifted apart.
Checking the exercised anchor in the floor's place verifies the wrong
claim, so the check is re-run and re-stated against `e67ad07` and
`d038f71`; `9a26ac8` is retained only under its own correct label.

**1-pre did not ADD the idle-Escape local quit.** The docs said "no
behaviour change" and then called the Escape quit a "1-pre change" and
"the one thing 1-pre added" --- which contradict each other and the
diff. It is **pre-existing behaviour that 1-pre preserved and isolated**
behind an `EventOutcome` return; **1a removes it**. Fixed in both files.

**§1a called the board current at `d038f71` while most of it dates from
2026-08-01**, and P5 still described #217 as a held PR although it had
merged. The board is now labelled **MIXED-ANCHOR and explicitly not
current as a whole**, with the drift named as evidence and a pointer to
`git log --first-parent`; the P5 row is corrected. Refreshing the rest
is a separate pass, deliberately not folded into a GUI absorption.

**The exact-head accounting was wrong --- one commit and 34 lines, when
it was two and 38 --- and the fix is to stop counting.** A count goes
stale on the very next commit, including the one carrying the fix, which
is how it broke in the first place. Both the lane and the PR body now
state the standing property instead: every commit after the gated tree
is markdown-only, checkable with `git diff --name-only 502308d..HEAD`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 13:32:40 +02:00
Levi Neuwirth 74054bb7ff
docs: record PR #238 in the absorption lane
The number goes in the moment the PR opens, per this file's own rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 13:23:05 +02:00
Levi Neuwirth 08d970c40d
docs: give the absorption PR its own lane, with the gate result
This file's standing rule is 'when a PR is opened, give it a lane', and
the correction behind that rule --- #171 drifted 153 commits while
invisible here --- applies to a docs PR as much as a code one. Written
with the branch's own commit rather than after the fact, which is also
what keeps a lane from re-conflicting on every other merge.

Records the eight-stage gate result, states the docs-only scope as
something verified with `git diff --name-only` rather than intended,
and notes that the lane is REMOVED at merge: it describes a docs pass,
not an arc, so rule 4 takes it outright instead of rewriting it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 13:22:32 +02:00
Levi Neuwirth 502308d8e4
docs: absorb #236 and #237 --- the GUI arc anchor moves to d038f71
Docs only. No `src/`, no `pmacs-gpu/`, no 1a implementation.

**The recovery procedure was RE-RUN, not SHA-swapped, and that is what
licenses the rest of this commit.** Every documented step, from an empty
directory, on git 2.55.0: clone the canonical URL (which names it
`origin`, and `git remote get-url githubsucks` errors with `No such
remote`, exactly as the section anticipates); add the alias; fetch;
`git log -1 --oneline githubsucks/main` -> `d038f71`; confirm BOTH the
old floor `9a26ac8` and the new `d038f71` are ancestors, so advancing
the floor is valid rather than merely plausible; recover with the
three-argument `git worktree add <path> -b <local> githubsucks/<branch>`
form and confirm the upstream is actually set. The documented TRAP was
reproduced too: the two-argument form succeeds and leaves
`## HEAD (no branch)`, so that correction still holds on 2.55.0.

**This discharges a debt rather than inheriting one.** The previous
absorption advanced the base to `e67ad07` WITHOUT re-running the
procedure and recorded that as an outstanding obligation. It is settled
here, by the run above.

Canonical base, recovery floor, the `git log` threshold and the
last-exercised record all move to `d038f71` together --- a floor that
accepts an older commit than the base it declares passes on a tree the
rest of the file does not describe.

**The GUI lane is REWRITTEN, not removed.** Rule 4 removes a lane when
its ARC is done; the arc is Stage 1 as a whole and **five slices
remain**. The lane now records 1-pre as merged, names 1a as next and
NOT STARTED, and keeps the fact that 1a (v24 `TextInput`) and 1e (v25
`OpenTarget`) are the two protocol-bearing slices and are serialized.
Open-PR and branch-recovery wording for 1-pre is gone --- a lane that
still says "recover with `git checkout`" for a merged branch is how a
reader ends up on a stale tip.

The handoff anchor absorbs both merges, and §1 gains the arc bullet the
anchor promises. Its durable facts are the ones that will bind future
slices rather than the diff: a route carries the DECISION and only the
body gives the EFFECT; a harness that re-implements what it tests
witnesses its own copy; winit's `KeyEvent` is non-constructible outside
winit and this will bind every GUI input slice; the sentinel is the
success condition and the timeout only an error ceiling; four test
defects that were all one shape --- witnessing an absence the harness
had manufactured; and `cargo metadata`, not intuition, decides which CI
job runs a member's unit tests.

**One judgement beyond the stated scope, flagged rather than slipped
in.** §1a's board had no GUI-arc row at all, which would have
contradicted the anchor two sections above it. A row is added at the
position `COHERENCE.md` §20 Q#GA5 gives it --- after Priority 1, running
alongside P2-P5, interacting only at the P2 start gate --- and the P2
row now names that gate. The section's own dateline is corrected in the
same breath: the table was written 2026-08-01 and claimed zero open PRs,
so it now says which row is current and which are as-of that date.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 13:14:51 +02:00
Levi Neuwirth d038f7144e
Merge pull request #237 from levineuwirth/gui-stage1-pre
GUI Stage 1-pre — the input seam: window_event from 655 lines to four
2026-08-12 09:56:04 +00:00
Levi Neuwirth 18ce60cdd7
docs: three counting and wording fixes from review round 3
M24 makes the total 24 mutations, not 23. Stated as M1-M24 with the
split named --- twenty-three fail their own rows, and M6 is the P3
exception check, which must STAY GREEN. Writing it as a bare total was
what let the count drift twice: an exception check and a failing
mutation are both 'mutations' in the ledger's usual sense, so the
sentence has to say which is which.

'Gate passes on the final tree' contradicted the very next sentence,
which records that prose and doc comments changed after the run. It now
says final EXECUTABLE tree and explains why the distinction is drawn ---
'the gate was green' and 'the gate was green on exactly this tree' are
different claims, and only the weaker one is true here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 11:31:13 +02:00
Levi Neuwirth 9f08f5278c
fix(gpu): 1-pre round 2 --- a read ceiling, and the A4 consequence was wrong
Two review findings, one of them a real defect.

**THE SENTINEL READ COULD HANG FOREVER.** `read_until_sentinel` blocked
with no bound, so a writer or encoder that regressed after `enqueue`
would WEDGE THE GATE rather than redden it --- and a hang is the worst
failure shape there is, because it looks like slowness until the job is
killed. A 30 s `READ_CEILING` is armed on the daemon socket.

The distinction is kept explicit in the code, because collapsing it is
how this fix would undo the design it protects: **the sentinel remains
the success condition and the ceiling is only an error ceiling.**
Arrival is still decided by the sentinel, so the harness never infers
"nothing was sent" from a duration --- the core-count assumption behind
PR #235's CI red is not reintroduced. The ceiling sits far above any
plausible drain, so reaching it means broken, never busy.

M24 proves it fires rather than trusting it: drop the sentinel enqueue
entirely and the row fails in under a second with a diagnostic naming
both candidate causes and the partial transcript, instead of hanging.

**THE STAGE 1a CONSEQUENCE WAS WRONG IN FOUR PLACES.** Every record
claimed A4 would leave `EventOutcome` with one variant, so the type
should go with the Escape branch. It will not, and it should not.
`LifecycleRoute::Exit` --- a native window close --- returns
`EventOutcome::Exit` too. A4 removes the KEYBOARD producer only, leaving
one `Exit` producer.

And **one producer is not one variant**: the type survives because
`dispatch_window_event` must still distinguish `Continue` from `Exit` on
every event it handles --- nearly all must not exit, and the close must.
What A4 actually changes is `apply_keyboard`'s signature. Corrected in
the `EventOutcome` doc, the Escape-branch comment, the framing and the
ledger; the framing's superseded paragraph is deleted rather than
patched, since it also carried the stale "two `event_loop.exit()`
call sites" count. **There is exactly one executable
`event_loop.exit()`**, in `window_event`.

Also: the sentinel-tag comment claimed four modifier bits and used
three. It now says three, wrapping every eight steps, and why that
suffices --- each sentinel is read before the next is issued, so a tag
only has to differ from its immediate predecessor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 11:07:41 +02:00
Levi Neuwirth 68c99fb2e2
docs: where the effect rows actually execute in CI
Checked with `cargo metadata`, not assumed: `workspace_default_members`
is the root `pmacs` package alone, so the `test` matrix and
`crdt-test` --- both bare `cargo test --all-targets` --- never compile
pmacs-gpu's unit tests. Only `gpu-render` runs them, and it installs
lavapipe, proves the adapter with vulkaninfo, and sets
PMACS_REQUIRE_GPU=1.

That matters because the harness's adapter assert is UNCONDITIONAL
rather than env-gated: it cannot be turned into a quiet ok anywhere, and
a future job that runs -p pmacs-gpu without an adapter fails loudly
instead of silently covering nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 10:45:44 +02:00
Levi Neuwirth 4da3a3432b
docs: P2 implemented --- framing revision 11 retracts revision 10's claim
Revision 10 argued a route-classification transcript satisfied P2
because a route 'names its local effect'. The wheel falsifies it: a
wheel route carries a delta, and whether that becomes a viewport
update, a panel event, a terminal event or nothing at all depends on
State. The route names the family; only running the body names the
effect. Retracted rather than quietly amended, since it was an argument
this document made and review overturned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 10:37:24 +02:00
Levi Neuwirth f976dc1042
feat(gpu): 1-pre P2 --- the effect harness, outbound AND local
Review round 1's blocker: P2 requires outbound events plus local
effects, and the harness recorded `Route` classifications only. The
wheel is the proof it was not enough --- a wheel route carries a delta,
and whether that becomes a viewport update, a panel event, a terminal
event or nothing at all is `State`'s to decide. A route names the
family; only running the body names the effect.

**`App::dispatch_window_event` is what makes P2 reachable, and it is the
substantive change here.** Left inside `window_event`, the dispatch
would force a harness to re-implement it --- and a harness that
re-implements the thing it tests witnesses its own copy. `window_event`
is now four lines: call dispatch, exit if it asks. **P3 narrows from a
33-line match to a single `if`.**

`EffectHarness` drives production code end to end:

  * a REAL `AttachClient` over a `socketpair`, through the real
    handshake, outbox, writer thread and encoder, so what is recorded is
    the wire and not a mock's idea of it (`connect_stream_for_test` in
    attach.rs exists only because the constructor is private to that
    module; it adds no behaviour);
  * a REAL windowless `State`, so the bodies take their real branches;
  * `dispatch_window_event` itself.

Local effects have no wire trace, so each is read where it lands: exit
from the returned `EventOutcome`, redraw from a test-only
`State::render_calls`, resize from the surface config, the modifier
mutation from `App::modifiers`, and the scroll from `scroll_top`.

**Steps are delimited by a sentinel key, not a sleep.** "This step sent
nothing" is otherwise undecidable without waiting, and a fixed-duration
wait against a writer thread is the core-count assumption PR #235's CI
red was made of. The sentinel is not coalesceable (only viewport and
drag kinds are), so it can neither replace nor be replaced by a recorded
event. It does sit between steps, so cross-step coalescing that
production would perform is absent here --- stated in the harness doc,
since it makes the transcript per-step rather than as-coalesced.

**Never skips.** Per the ruling, a missing wgpu adapter is an assertion
failure and not a skip: this project has twice recorded a suite that
returned `ok` without running. Mutation M21 makes `new_headless` return
`None` and all NINE effect rows fail loudly while the thirteen pure
routing rows, which need no GPU, stay green --- the two tiers behaving
exactly as intended.

TWO ROWS WERE WRONG AND THE MUTATIONS FOUND THEM, WHICH IS THE POINT:

  * the wheel row asserted `.all(|e| matches!(e, Viewport))` over the
    transcript --- VACUOUSLY TRUE ON AN EMPTY ONE, so an outbound-blind
    harness passed it. Now asserts non-empty first.
  * with that fixed it still failed, for a second reason: the fixture
    was two lines and could not scroll, and a headless `State` has no
    attached buffer, so `scroll_by_lines` returned `None` and withheld
    every send. Both are absences the harness manufactured itself ---
    the same shape as the panel wire, below.

The panel wire is the third of those. `resumed` sets the frontend id and
the session version on the state before any geometry flush; the harness
did not, so `flush_panel_geometry` silently withheld the declaration and
the resize row failed against an absence of its own making. The harness
now mirrors that wiring and drains the attach-time declaration, so each
row's transcript holds only what its own event produced.

Evidence --- 22 rows (13 routing, 9 effect), 6 further mutations:

  M18 exit effect discarded            -> the close row
  M19 redraw effect discarded          -> the redraw row
  M20 apply_resize stops declaring     -> the resize row
  M21 no wgpu adapter                  -> all NINE effect rows, loudly
  M22 harness blind to OUTBOUND        -> resize + wheel
  M23 harness blind to LOCAL           -> six rows

M22 and M23 together are P2's contract made executable: blind the
harness to either half and rows fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 10:36:12 +02:00