Commit Graph

843 Commits

Author SHA1 Message Date
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 7e107bd137
docs(framing): Q#BP-R3 overruled --- the lane blocks on a cell-mapping generation
Records the overrule and frames the slice (SS5b). No implementation;
the lane is PAUSED behind it.

**ALL THREE OF MY BOUNDS WERE WRONG**, and each is recorded because
each is why the trade looked cheap:

  "self-inflicted"       another frontend, or a background process, can
                         edit the same buffer --- the mapping moves with
                         `view_top` untouched and the clicking frontend
                         did nothing
  "bounded by 3 rows"    unbounded: ticks, paging, folds, edits and
                         reloads accumulate before the new frame is seen
  "one round trip"       until the frontend PRESENTS the replacement
                         frame; a slow or backed-up frontend widens the
                         window arbitrarily

With the narrowness gone there is nothing left to trade on.

**AND THE FIX IS A GENERATION, NOT A TOKEN.** A per-frame token would
invalidate a gesture on every repaint --- the exact mistake
`panel_epoch` exists to avoid, and it would break drags outright.
`mapping_generation` identifies the INVERSE MAPPING: it moves on
viewport, folds, wrap/gutter geometry and buffer content, and holds
across focus, styling, cursor motion and selection-only changes. The
stability half is load-bearing rather than an optimisation --- a drag
provokes selection repaints on every motion, and a generation that
moved with them would cancel the drag after one step.

SS5b frames the slice: appended variants (`PresentMapped`,
`PanelPointerMapped`) rather than widened structs, because postcard
encodes positionally and a shipped variant's field list is frozen;
bilateral gating, with neither side assuming the other's support and a
bare `PanelPointer` from a new-enough peer NOT silently upgraded; and a
drop-before-mutation check at the same rung of the ladder as the epoch
gates, one level finer. Dropping mid-drag is correct: if the mapping
changed, the cells being dragged across no longer mean what they meant.

This is where the hole always was --- `buffer_id` catches replacement,
`panel_epoch` catches close/reopen, `geometry_epoch` catches a
declaration race, and nothing catches "the text under that cell
changed".

Consequence: SS5b takes the next protocol version and GUI arc 1e's
`OpenTarget` moves to the one after. That edit is OWED BY THE 1b
BRANCH, which already revises `docs/gui-stage1-input-framing.md`
heavily; making it here would collide at the rebase 1b is scheduled
for, so it is recorded as required rather than made.

Gates: all nine green under `env -u TMPDIR`, log 20260814T155745Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-14 18:02:24 +02:00
Levi Neuwirth 3dff79a0ec
fix(panel): D2 was broken on the real sequence, and Q#BP-R3 is ruled
Two of review's findings: the framing blocker, and a live defect in the
commit before this one. The remaining six are queued.

**I REPORTED M-D2 AS BITING AND IT DID NOT.** `presented()` filters on
`frame.geometry_epoch == self.panel.geometry_epoch`, and a geometry
change advances that field FIRST --- so by the time the matching frame
arrives, `presented()` already answers `None` and my
`is_some_and` predicate skipped the reset entirely. The shipped D2 did
nothing on the production sequence.

The witness could not see it because it invented a higher-epoch frame
without driving `next_geometry_declaration`, leaving
`self.panel.geometry_epoch` untouched so `presented()` still matched.
A test that skips the step which breaks the code cannot fail on it.

The predicate now compares against the RETAINED frame
(`self.panel.frame`), which survives the epoch advance, and the witness
drives `GeometryTrigger::Metrics` for real --- asserting along the way
that `presented()` IS `None` in that window, so the trap is pinned
rather than merely avoided. Restoring the `presented()` predicate now
fails the row.

**M-D3 WAS ALSO UNCONSTRAINED**, for a smaller reason: arming clears
`last_pointer_cell`, and the leg only armed, so the field was already
`None` before the replacement and deleting its reset changed nothing.
The arm helper now seeds the baseline with one accepted motion --- what
a real gesture would have produced --- and after replacement the row
requires `panel_motion_is_new` at that same cell to return true.
Deleting only that line now fails.

**Q#BP-R3 IS RULED: current-state hit semantics, narrowly, with the
token named as follow-up.** `PanelPointer` carries epochs and a cell
but nothing identifying the frame CONTENT the user saw, and
`panel_epoch` is stable across ordinary frames by design. So a document
wheel moves `view_top` daemon-side, and a click sent before the new
frame lands is inverted through the NEW `view_top` --- selecting a row
the user never saw, with every validation passing.

Closing it properly needs a per-frame token on `PanelFrame` echoed by
`PanelPointer`: a WIRE CHANGE, and this lane is non-protocol-bearing
with 1b blocked behind it. A daemon-only mitigation was considered and
does not work --- inverting against the last EMITTED frame still cannot
tell which frame the user SAW, and the failing window is identical.

So the lane accepts current-state semantics and says so: the window is
narrow and self-inflicted (the same frontend must move the view and
then click within one round trip), the magnitude is bounded by
`SCROLL_LINES`, and the TUI is structurally unaffected. The token is
recorded as a named follow-up for the next protocol-bearing slice, so
it is inherited rather than rediscovered. Overrule stated explicitly:
the trade is a narrow same-frontend mis-hit now, against serializing
this lane and 1b behind a v25 wire change.

Gates: all nine green under `env -u TMPDIR`, log 20260814T154611Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-14 17:50:54 +02:00
Levi Neuwirth 515a4515be
docs(framing): mark bottom-panel revision 12 approved
SS5a is the acceptance-48 ground truth and its rulings are settled:
Q#BP-R1 (a single click selects a listview row only), Q#BP-R2 (a
terminal-chrome wheel is consumed daemon-side, before activation), the
R-a..R-d replay edges, and the witness matrices A1-A5, B1-B6, D1-D4.

Also records what the eight rounds cost, because the pattern repeated:
every round found a witness or a rule that would have PASSED against a
broken implementation. A `#[cfg(test)]` fixture cited as a production
handler. Activation ordering mistaken for a safety proof. A mode-line
rule that was per-row when the tree's is per-kind, and document-only at
that. A producer rule the frontend had no information to obey. A
consume check that would have left focus stolen. And twice, an
assertion added without a state in which it could fail.

No round found a design disagreement. The rulings from 8 and 9 survived
intact; what changed each time was WHERE a rule lived or WHAT A WITNESS
COULD SEE.

Implementation begins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-14 16:55:24 +02:00
Levi Neuwirth 78e5bec2d4
docs(framing): bottom-panel revision 12 --- the passive precondition, and two assertions that are not interchangeable
Answers review of revision 11. Still framing only; no implementation.

**"FOCUS UNCHANGED" IS VACUOUS IF THE PANEL IS ALREADY FOCUSED.** With
the terminal side window active, the below-activation mutation calls
`focus_window` on the window that is ALREADY active --- nothing
changes, and the row stays green against the exact bug it exists to
catch. I added the assertion in revision 11 and did not give it a state
in which it could fail.

Leg 2 now starts passive, and says so as assertions rather than setup:
the primary document window is active, the terminal side window is
distinct from it and passive, and the terminal controller identity is
captured as a baseline. Asserted rather than arranged, because each is
the kind of detail a later edit changes silently, and each failure mode
is a witness that passes while proving nothing.

**AND THE TWO ASSERTIONS ARE NOT INTERCHANGEABLE**, which revision 11's
prose blurred by naming them together as though either would do:

  consume check below activation
      -> `focus_window` runs, the panel takes focus, and NOTHING
         scrolls, so every movement assertion still passes
      -> caught by FOCUS, and focus alone

  terminal branch calls `apply_terminal_gesture`
      -> `claims_control` is `!matches!(kind, Move)` (`:3555`), so a
         wheel CLAIMS THE PANEL'S CONTROLLER at `:3571`, before any
         local handling at `:3575`; the chrome coordinate then fails
         the reporting bounds check and drops into the local branch,
         which may also move scrollback
      -> caught by CONTROLLER IDENTITY, plus scrollback when it moves

Activation alone claims nothing --- `focus_window` and
`claim_terminal_controller` are separate acts --- so a row asserting
only controller identity misses the ordering bug entirely, and one
asserting only focus misses a replay that quietly claims the terminal.

Gates: all nine green under `env -u TMPDIR`, log 20260814T144513Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-14 16:49:54 +02:00
Levi Neuwirth 112fcf593f
docs(framing): bottom-panel revision 11 --- consume the chrome wheel before ACTIVATION
Answers review of revision 10. Still framing only; no implementation.

**"CONSUME BEFORE `apply_terminal_gesture`" WAS NOT EARLY ENOUGH.**
`activates` is `!matches!(kind, Move)` for a terminal panel
(`src/editor.rs:2695`), which INCLUDES the wheel, and focus plus
`active_frontend` are written at `:2699` --- ahead of any replay
decision. A consume check below that block leaves the wheel CHANGING
FOCUS while scrolling nothing and claiming no controller: exactly the
half-state AC48's activate-then-claim rule exists to prevent. The panel
steals focus and does not move.

A terminal-chrome wheel is therefore not a terminal gesture at all, and
the dispatcher must treat it that way. The order is now stated:

  1. authenticate and validate (the existing ladder)
  2. resolve the side window and its buffer kind
  3. terminal + chrome wheel -> CONSUME IMMEDIATELY, before
     `focus_window`, `active_frontend`, any controller claim, any
     command-chain mutation, and the shared terminal path
  4. otherwise, the existing activation and replay rules

Step 3 is implementable where it belongs: `is_terminal` is resolved
from `buffer_id` at `:2687`, before the `core` borrow and before the
activation block, so the kind is already in hand.

**The witness gains focus and controller assertions, and they are
load-bearing.** The document->terminal replacement row now asserts the
focused window and terminal controller identity are unchanged, on top
of no child bytes, no scrollback and no document movement. Two
mutations bite different halves:

  - terminal branch calls `apply_terminal_gesture` -> the chrome
    coordinate fails the reporting bounds check, falls into the local
    branch, and the row catches the accidental local scrollback
  - the consume check sits BELOW activation -> nothing scrolls, so
    every movement assertion still passes and ONLY the focus and
    controller assertions catch it

That second mutation is the reason the new assertions exist. Without
them the row stays green against an implementation that steals focus on
every chrome wheel over a terminal, because the half-state is invisible
to any assertion about movement --- nothing moves either way.

Gates: all nine green under `env -u TMPDIR`, log 20260814T143558Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-14 16:41:02 +02:00
Levi Neuwirth ec127fc8fa
docs(framing): bottom-panel revision 10 --- the ruling was on the wrong side of the seam
Answers review of revision 9. Still framing only; no implementation.
Q#BP-R2's outcome is unchanged; its ENFORCEMENT POINT moves.

**THE GPU CANNOT KNOW WHETHER A PANEL HOLDS A TERMINAL.** `PanelFrame`
carries `buffer_id`, both epochs, `size`, `cells`, `cursor` and
`focused` --- and no target-kind discriminator
(`pmacs-protocol/src/panel.rs:73`). The panel is an OPAQUE CELL
PROJECTION to the frontend. `state.terminal`
(`pmacs-gpu/src/main.rs:1964`) cannot answer either: it describes the
primary full-window terminal, not the side-window.

So revision 9's producer rule --- "terminal chrome wheel: do not send"
--- was unimplementable without a new wire field, and this lane is
explicitly non-protocol-bearing. I wrote a rule the frontend has no
information to obey.

The correction is structural, not cosmetic. **Every producer rule is
now target-blind**, and the table is split by SEAM SIDE rather than by
target:

  producer   claims the chrome wheel and sends `PanelPointer` with its
             valid frame coordinate, for EVERY panel
  daemon     after validation and resolving the side window ---
             document panel -> `scroll_window`;
             terminal panel -> CONSUME, producing no child bytes, no
             local scrollback, and no document fallthrough

The other producer rules collapse safely because both targets want the
same behaviour there: a chrome press is reserved by documents and
dropped by terminals --- both drop --- and a crossing `Up` must arrive
for either, to terminate a selection or deliver a release. The wheel
was the sole divergence, which is exactly why it was the rule that
could not survive on the producer side.

The chrome coordinate does cross the wire now, and that is fine: it is
a valid frame cell, coord validation accepts it, `scroll_window` is a
window-level move that never reads it, and the terminal branch never
forwards it to a child. Nothing is fabricated and nothing reaches an
application --- which was the whole objection to clamping.

**Witness: ONE frontend across a document->terminal replacement.**
Document chrome wheel scrolls the panel; after replacement the terminal
chrome wheel changes nothing. Doing it in one frontend across a
replacement is what makes it a control rather than two unrelated
observations --- geometry, pointer position and producer path are
identical, and only the target differs. Mutation: let the terminal
branch call `apply_terminal_gesture`, and the chrome coordinate fails
its reporting bounds check, falls into the local branch, and the row
catches the accidental local scrollback that revision 8's clamp would
have shipped on purpose.

Gates: all nine green under `env -u TMPDIR`, log 20260814T142336Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-14 16:28:18 +02:00
Levi Neuwirth 7d6b6da366
docs(framing): bottom-panel revision 9 --- Q#BP-R2 overruled, and R-c2 stops fighting a tested contract
Answers review of revision 8. Still framing only; no implementation.
Both items reverse a decision revision 8 made.

**Q#BP-R2 IS OVERRULED: a terminal-chrome wheel is CONSUMED, not
clamped.** I argued clamping on consistency grounds and missed that SGR
wheel input is COORDINATE-BEARING: `encode_mouse` takes a `coord` and
writes `coord.col + 1` / `coord.row + 1` into the emitted sequence
(`src/terminal/input.rs:102`, `:146`) --- which A4, added one revision
earlier, pins exactly. Clamping therefore fabricates a hit on the
terminal's final content row, and an application that routes wheel
input by position acts on a cell the user never pointed at. The
consistency I was buying is cosmetic; the price is a synthetic
coordinate handed to a program as real.

The asymmetry with `Up` is the part worth keeping in the record: `Up`
must be normalized because an unterminated gesture HANGS --- the daemon
holds a button down forever --- while a wheel tick is self-contained
and dropping one strands nothing. Liveness is what earns
normalization, and the wheel has no liveness obligation. Consumed, and
never fallen through to the document; TUI parity restored.

**R-c2 WOULD HAVE BROKEN A TESTED GUARANTEE.** Revision 8 said to
retain the `Down` cell in `last_pointer_cell`. That field is cleared on
press deliberately, and a live test says why: "the first drag after a
press must reach the daemon even at the cell the press landed on"
(`pmacs-gpu/src/main.rs:19841`). Storing `Down` there makes the press's
own cell the dedupe baseline and suppresses exactly that `Drag`.

Ruled: a separate `gesture_last_content_cell`. The two fields have
different jobs and conflating them was the error ---
`last_pointer_cell` answers "is this motion worth sending?", the new
one answers "where did this gesture last legitimately point?" ---
different lifetimes, different resets. Written on arm and on each
accepted content motion; reset on release and on BOTH identity changes;
never consulted by `panel_motion_is_new`. A second mutation guards the
separation itself: point the dedupe at the new field and the existing
first-`Drag`-after-press assertion must fail.

The alternative --- ruling `Down` the dedupe baseline and retiring the
guarantee --- is recorded as considered and rejected. It would need a
mutation showing the first same-cell `Drag` is redundant, and it is not
obviously so, since that event is what establishes the daemon's drag
state.

**And the crossing table follows the choice.** Revision 8 promised a
content-originated `Drag` over chrome would be sent. It should not be:
once normalized, its coordinate is usually the one already reported and
the ordinary dedupe suppresses it, correctly --- promising delivery
would oblige the producer to defeat its own dedupe for a daemon state
that is identical either way. `Up` is the load-bearing crossing event
and the only one promised unconditionally.

Gates: all nine green under `env -u TMPDIR`, log 20260814T123502Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-14 14:39:47 +02:00
Levi Neuwirth 4833a73dfe
docs(framing): bottom-panel revision 8 --- target x origin, and a negative row standing alone
Answers review of revision 7. Still framing only; no implementation.

**R-c NEEDED TARGET x GESTURE-ORIGIN, NOT KIND ALONE.** The per-kind
rule I took from the TUI is DOCUMENT-ONLY: for a terminal window the
TUI rejects every kind on the mode line before any per-kind match
(`src/editor.rs:3273`) and hands `dispatch_terminal_mouse` a
content-sized viewport (`:3272`). Terminals never see a chrome
coordinate.

And passing one is not merely untidy, it is wrong twice:
`apply_terminal_gesture`'s reporting branch is bounds-checked on
`coord.row < screen_size.rows` (`:3560`), and a chrome row equals
`screen_size.rows`, so the check FAILS and the gesture falls into the
LOCAL branch. A reporting child therefore receives a `Down` and then no
`Up`, and a chrome wheel silently becomes local scrollback instead of
behaving like a content wheel. The table is now split by target, and a
content-originated gesture terminates at its LAST VALID CONTENT
COORDINATE.

**Q#BP-R2 is ruled: a chrome wheel over a terminal panel CLAMPS.** This
diverges from the TUI, which drops it, and is flagged as the one
product call inside R-c. The panel's mode line is the band's own
chrome, one row, and a document panel already scrolls on a chrome
wheel; dropping only for terminals would make the same pixel behave
differently depending on which buffer happens to be shown. Overrule if
TUI parity is preferred --- nothing else in R-c depends on it.

**R-c2 --- THE PRODUCER NEVER REMEMBERS THE `Down` CELL.**
`set_panel_pointer_held` clears `last_pointer_cell`
(`pmacs-gpu/src/main.rs:7250`) and only motion refills it, while
`panel_release_cell` falls back to exactly that field. Today the
fallback is rarely reached because chrome IS a `PanelCell`; the moment
R-c stops that, a `Down` in content followed immediately by a release
over chrome --- no intervening motion --- has nothing to fall back to.
Retain the `Down` cell at arm time. For a reporting terminal the row
asserts the exact child release BYTES, because a latch that clears
while the child never hears the release is the failure being guarded.

**A1 WAS A NEGATIVE ROW STANDING ALONE.** It asserts bytes that must
not appear, so disabling child reporting entirely satisfies it --- as
does never reaching `apply_terminal_gesture`. The whole edge matrix
could have stayed green against a replay that never reports anything.
A3-A5 add the positive half on the SAME terminal: exact SGR sequences
for `Down`/`Drag`/`Up`, exact wheel bytes when reporting, and the
non-reporting wheel's local scrollback effect.

**Four witness seams tightened.** B2 was checking the wrong thing:
`active_region()` returns `None` "if no region is set OR IT IS EMPTY"
(`src/editor_core.rs:4684`), so it answers None both before and after
the `Up` and cannot see the residue --- which is an active-but-EMPTY
selection whose stale anchor would capture the next shift-motion. It
reads the owning window's raw `selection` now. B4 pins the exact
selected word rather than that click state "reads a multi-click". B6
runs its visit sentinel through single AND double click, since the
ruling forbids both. D4 uses a CHANGED frame with unchanged epochs ---
the focus repaint the `Down` itself causes --- because a byte-identical
duplicate returns early ("A duplicate does no work", `:6918`) and would
never reach the reset code it is meant to constrain.

Gates: all nine green under `env -u TMPDIR`, log 20260814T122442Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-14 14:29:23 +02:00
Levi Neuwirth d9a23f30eb
docs(framing): bottom-panel revision 7 --- per-kind mode line, geometry identity, and three half-fixes
Answers review of revision 6. Still framing only; no implementation.
Three of the five are corrections to rules 6 introduced.

**R-c's "the last row is inert" WAS WRONG.** The TUI precedent I cited
is per KIND, not per row: `inner_rows` guards `Down(Left)` (:3303),
`Drag(Left)` (:3331) and `Down(Right)` (:3348), and deliberately does
NOT guard `Up(Left)` (:3339) or the wheel (:3358/:3362). A blanket
inert rule breaks two things at once --- a wheel over the mode line
stops scrolling, and a gesture begun in content and released over the
mode line NEVER TERMINATES.

And a receiver-only rule cannot work at all here, because the producer
arms first: `PanelCell` spans the whole frame, so a mode-line `Down`
runs `set_panel_pointer_held(true)` locally
(`pmacs-gpu/src/main.rs:2878`) before the daemon sees anything.
Dragging into content then emits an orphan `Drag` the daemon cannot
tell from a real one. R-c now carries a per-kind producer/receiver
table, both crossings as witnesses --- mode-line->content must emit no
Drag, content->mode-line must terminate --- and rules the wheel
explicitly.

**R-d COVERED PANEL IDENTITY AND MISSED GEOMETRY IDENTITY.** The two
epochs move independently by design: `panel_epoch` is stable across
ordinary frames while `geometry_epoch` moves on any new declaration
"including a font or scale change that leaves `CellSize` identical"
(`pmacs-protocol/src/panel.rs:61`). `next_geometry_declaration`
(`pmacs-gpu/src/main.rs:6847`) advances it and clears neither pointer
field, so a held gesture resumes UNDER A NEW GRID carrying epochs that
are current and valid --- acceptance 49 rejects stale epochs and this
one is not stale.

R-d also constrained only one of the two latch fields. Clearing
`pointer_held` alone kills the orphan drag but leaves
`last_pointer_cell`, and `panel_motion_is_new` (`:7238`) then
suppresses the successor's first same-cell `Move` as a duplicate. Four
mutations now, D1-D4, including the NEGATIVE one: an ordinary
same-identity refresh must not cancel a live gesture, or D1/D2 are
satisfiable by resetting on every frame and selection stops working.

**R-a WITNESSED ONLY THE TERMINAL.** `dispatch_pointer`'s `Down` arm
reads `extending = mods.contains(SHIFT)` and either keeps the anchor or
collapses the selection (`src/editor.rs:3673`), so Shift-click in a
document or listview panel is selection extension. Threading modifiers
into `apply_terminal_gesture` alone would have passed the proposed row
while leaving document panels broken --- a witness certifying half a
fix. Two rows now, A1 terminal and A2 document, with separate
mutations: a single "drop mods at the boundary" bites both and proves
the boundary matters, not that each CONSUMER is wired.

**R-b'S ROWS WERE SATISFIABLE BY DOING NOTHING.** "Only A's panel
changes" passes for an implementation that drops the tail entirely. B1
now pins A's anchor at the Down cell's byte and A's cursor at the Drag
cell's byte; B2 proves `Up` collapses an empty click. B4 and B5 carry
Q#BP16's existing multi-click and Context statements, which replay is
where they first become executable. B6 is a listview visit sentinel:
the row is selected and `on_visit` does NOT run, so Q#BP-R1's ruling is
falsifiable rather than merely asserted.

Gates: all nine green under `env -u TMPDIR`, log 20260814T120856Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-14 14:13:37 +02:00
Levi Neuwirth c861d24e91
docs(framing): bottom-panel revision 6 --- four replay edges, and Q#BP-R1 ruled
Answers review of revision 5. Still framing only; no implementation.

**Q#BP-R1 IS RULED: a single click SELECTS a listview row only.**
RET/SPC remain the activation path (`listview.lua:610`); no
click-to-visit, no double-click-to-visit. That is acceptance 48's own
wording, and it keeps document navigation from arriving as an
incidental consequence of wiring replay.

**REVISION 5 CONCLUDED THAT ACTIVATION ORDERING MADE REPLAY SAFE. IT
IS NECESSARY BUT NOT SUFFICIENT.** The ordering holds only for gestures
that activate, in a session nothing interleaves with, on a presentation
that does not change mid-gesture, with modifiers intact and the mode
line excluded. Each of those four provisos is an edge:

**R-a --- modifiers are dropped before the shared path.** `PanelPointer`
carries `mods`; the daemon destructures them into `..`
(`src/daemon.rs:2425`) and the dispatcher has no modifier parameter.
This breaks PRECEDENCE, not just fidelity: `apply_terminal_gesture`
gates child reporting on `!shift && ... && modes.mouse_sgr`
(`src/editor.rs:3534`), so Shift is the user's "select locally instead
of talking to the child" override. Zeroed, a Shift-drag over a
reporting terminal panel sends SGR. Row: Shift-drag selects locally and
the child receives NO bytes. Mutation: drop `mods` at the boundary.

**R-b --- `Drag` and `Up` do not activate**, and another frontend's
input can interleave between a Down and its Drag, so a replay reading
ambient active-window state acts on whatever is active then. The
framing now NAMES the mechanism: an explicit side-window cell->byte
adapter, and selection through a window-TARGETED path.
`activate_and_position` (`src/editor.rs:3795`) is both the precedent
and the trap --- its conversion is properly window-scoped, but it calls
`set_active_window_id`. Rows: panel A Down -> frontend B input -> A
Drag/Up changes only A; an orphan Drag/Up on a passive panel leaves the
document mirror byte-identical.

**R-c --- `panel_grid_size` is the FRAME, not the terminal viewport.**
The panel's last row is its mode line: projection derives content as
`rows - 1` (`src/editor.rs:2499`) while `panel_hit_test` reports across
the whole frame (`pmacs-gpu/src/main.rs:7184`), so a `PanelPointer` can
name the mode-line row. Passing the full grid to
`apply_terminal_gesture` would make the mode line a child terminal
cell. Terminal viewport is `rows - 1`; document replay follows the
TUI's existing rule, "Mode-line click: reserved"
(`src/editor.rs:3304`). Rows must distinguish content from chrome or an
off-by-one passes.

**R-d --- replacement leaves the frontend's gesture latch armed.**
`Absent` clears `pointer_held` and `last_pointer_cell`
(`pmacs-gpu/src/main.rs:6909`); `Present`->`Present` does not
(`:6913`). Press on A, A replaced by B, and the latch emits a Drag or
release for B with no B press --- and acceptance 49 CANNOT reject it,
because the event carries B's current epochs. 49 is a staleness gate
and this is not a stale event; it is a well-formed event from a gesture
whose presentation no longer exists. The fix has a precedent in the
same file: the DIVIDER drag latch already carries both epochs and
self-invalidates (`:7288`). The pointer latch never got it.

Three of the four are places where the tree already contains the right
answer and the panel path does not use it, which is worth saying
plainly: this is wiring that has to be done carefully, not mechanism
that has to be invented.

Gates: all nine green under `env -u TMPDIR`, log 20260814T115257Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-14 13:57:37 +02:00
Levi Neuwirth d97dcdfc46
docs(framing): acceptance 48 re-measured --- it is HALF implemented
Ground truth before code, and the lane entry in the same commit, per
the standing correction from #171 and #215 --- the one the 1b lane
missed. Base `githubsucks/main` @ `72da24a`. No implementation yet.

**AC48 IS NOT UNIMPLEMENTED. IT IS HALF IMPLEMENTED**, and nothing had
written the halves down separately, which is how "PanelPointer drives
listview rows, selection, terminal reporting and focus" could sit next
to a dispatcher that does none of the first three.

Landed already: click-to-focus and the terminal activation rule
(`src/editor.rs:2701`); the focused-only auto-scroll clamp with a
passive panel's `view_top` preserved (`:2569`, which has cited parent
48 all along); and the coalescing rules --- `Move`/`Drag` tails
coalesce while press/release/context/wheel stay lossless and ordered
(`pmacs-gpu/src/attach.rs:374`).

Missing: listview row selection, panel selection, terminal mouse
reporting, wheel replay.

**AND THE REPLAY IS MOSTLY WIRING.** `apply_terminal_gesture`
(`src/editor.rs:3525`) is already "the one terminal pointer path,
shared by both frontend kinds" and already drives child mouse
reporting, selection and scrollback; a panel terminal needs the SAME
CALL, with `side_window_for` + `TerminalViewKey` + the
`panel_grid_size` the dispatcher already fetches. `scroll_window`
(`:3845`) is window-scoped including its cursor carry, and documents
that a wheel "names the pane under the pointer and does NOT activate
it". This is why a wheel-only bridge would be the wrong shape: the
shared path takes every kind at once, and the wheel-only version would
have to grow a second one later.

**The scoping hazard, and why no ruling is needed.** `set_cursor_byte`,
`begin_selection` and `clear_selection` are ACTIVE-WINDOW scoped
(`src/editor_core.rs:1216`, `:4691`), so replaying naively from the
panel path would move the DOCUMENT's point --- exactly what AC48's
"without disturbing the document mirror" forbids. The existing
activation rule already resolves it: activation runs before replay in
the same dispatch, and the gestures that need the active-window API are
precisely the ones that activate. The single gesture that does not --- a
document panel's wheel --- needs only the window-scoped
`scroll_window`.

**One genuine question, opened not answered: Q#BP-R1.** Does a listview
row VISIT on click, or only select? `listview.lua` binds visiting to
RET/SPC (`:610`) and there is no pointer precedent anywhere in the
file. Single-click-visits matches a file tree; click-selects,
double-click-visits matches an editor list; AC48 says "row selection"
and nothing more. This lane implements SELECTION and does not invent
activation, because the difference is whether a click navigates the
user's document.

Gates: all nine green under `env -u TMPDIR`, log 20260814T113155Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-14 13:40:24 +02: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 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 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 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 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 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 1f8d194908
docs: review round 1 --- four accuracy defects, one of them a bad command
None changes a decision; all four were claims a reader would check and
find false.

**The durable diagnosis command did not run the pair it claimed.**
`m4_24_` is a PREFIX matching **18** tests, so the recorded invocation
would report roughly 16/2 contaminated and 18/0 clean --- not the 0/2
and 2/2 beside it. A reader following it would see a mostly-green run
and conclude the hazard was mis-diagnosed. Replaced with **four literal
`--exact` invocations, one test each**, every one of them executed
before being written down: `running 1 test`, `171 filtered out`,
contaminated `0 passed; 1 failed` panicking at `:5668:5` and `:6615:5`,
clean `1 passed; 0 failed` with no panic. The block now also says to
read the `running N tests` line, pointing at the libtest-filter bullet
two entries below --- which is the trap that produced this defect in the
first place.

**"The diff touches only `pmacs-gpu/src/main.rs`" -> "the whole
EXECUTABLE diff".** The branch changes six files, five under `docs/`.
The structural argument was always about linkage, not file count, but as
written it was simply false and the first `git diff --name-only` would
say so. Fixed in both the ledger and the handoff.

**"a headless test can drive every family"** contradicted the keyboard
exception three paragraphs below it. Now says every family whose event
winit lets a test construct --- all of them except keyboard --- and
points at `route_keyboard` for how far that reaches.

**`[KeyboardRoute::Press]` names a type that does not exist.** It was
renamed to `KeyAction` when the payload moved onto `Route::Keyboard`,
and this doc link was left behind pointing at nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 10:05:41 +02:00
Levi Neuwirth c256c630cf
docs: record PR #237 in the 1-pre lane
An open PR gets its number in the ledger at the moment it opens, per the
standing correction from #171 and #215 --- #171 drifted 153 commits while
invisible here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 00:05:21 +02:00
Levi Neuwirth b7fea705a0
docs: the m4 red is environmental --- signature-level pair, and a green gate
The `/tmp/.git` diagnosis, finished properly.

**Compared on SIGNATURE, not test name**, which is what the ledger's own
CI-triage rule requires. The contaminated leg panics at
`m4_acceptance.rs:5668:5` and `:6615:5` with `.received = ""` ---
byte-identical to the gate red's own signature --- and the clean leg has
ZERO panics. Same binary, same commit, one variable.

**All nine gates pass under an isolated `TMPDIR`**: m4 168/0/3 by the
mandated command, and the 117-target `--workspace --no-fail-fast` sweep
with zero failures anywhere, `m4_acceptance` running all 171 inside it.

**The marker is left in place.** It is foreign, isolating is sufficient,
and deleting it is not this lane's call. Recorded with the constraint
that matters: the isolated `TMPDIR` must be outside `/tmp` AND outside
every git worktree, because a child of `/tmp` is not isolated ---
`/tmp/.git` is still its ancestor.

**One hazard this cost a round, worth the bullet it gets.** A libtest
filter that matches nothing prints `0 passed; 0 failed; N filtered out`
and exits ZERO. This shell is zsh, which does not word-split unquoted
parameter expansions, so `NAMES="a b"; cargo test -- $NAMES` passes one
argument, matches no test, and reports green. The contaminated leg
"passed" that way and briefly looked like the hypothesis collapsing.
The fix is to read the `running N tests` line, which is the only place
a filter's actual reach is stated --- the same family as the
`feature-census` trap already recorded two bullets above, and the same
lesson as "a probe must fail loudly".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-12 00:04:12 +02:00
Levi Neuwirth 41a08b8237
docs: 1-pre implemented --- framing revision 10, the lane, and a hazard
Three documents, one finding each.

**`docs/gui-stage1-input-framing.md` -> revision 10.** Revision 9 is the
approved design and revision 10 changes none of it. It records ONE scope
correction that could not be seen from the design: P1 has a second
structural exception, for the keyboard family alone, and it is winit's
rather than this seam's. `KeyEvent` carries a `pub(crate)
platform_specific` field, so no `WindowEvent::KeyboardInput` can be
constructed outside winit. Bounded three ways rather than accepted
whole --- it does not reach the pointer families (`DeviceId::dummy()`
exists for exactly this, checked BEFORE writing the exception down), the
family's only decision is factored into `route_key_action` and witnessed
directly, and what stays uncovered is one pattern arm with no logic.

Also records that P3 is now MEASURED: deleting the whole delegation
leaves all 256 `pmacs-gpu` tests green, not merely the 13 new rows.

**`docs/active-work.md`** --- the lane moves to IMPLEMENTED with the
four commits, the shape, the verbatim-move method, and the gate result.

**`docs/agent-handoff.md`** --- the stray-marker hazard gains what this
run earned: `scripts/gate` DOES NOT ISOLATE `TMPDIR`. It isolates the
target directory and five ambient roots, so `tempfile::tempdir()` still
lands under whatever `/tmp` happens to contain, and the hazard therefore
reproduces INSIDE a gate run --- which is how it surfaced here, on a
lane that touches only `pmacs-gpu/src/main.rs`. The bullet now carries
the discriminating command pair (`TMPDIR=/tmp` 0/2 versus a marker-free
root 2/2) rather than only the narrative, because a rerun establishes
nothing about this and the pair establishes everything. Isolating
`TMPDIR` is assigned to the gate lane, not to whichever feature PR trips
over it next.

One code change rides along: `EventOutcome`'s doc comment said
`event_loop.exit()` is called in "exactly one place", which is true of
the function and false of the call sites --- there are two, both inside
`window_event`. Stated precisely now, since the whole point of the
sentence is that a reader can check it by grep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-11 23:55:57 +02:00
Levi Neuwirth 2d9f0550e0
docs: Q#S1-7 --- Meta/Super moves to Stage 2, in both records
The obligation this PR owes, discharged before any code lands so the
scope Stage 1 is implementing is the scope both records describe.

Meta/Super forwarding was mapped to Stage 1a. Forwarding it there would
turn platform Command/Super shortcuts into daemon Meta chords BEFORE
capability-aware and local-binding policy exists --- so the OS
reservation currently in the tree is a deliberate, tested decision
rather than an omission, and Stage 1 keeps it.

Both records are amended, not one. The arc's §2.5 mapping table and the
standing backlog's input bullet each claimed Stage 1a, and a mapping
that lives in two files drifts unless both move together --- which is
the failure class the Stage 0 lane spent seven review rounds retiring.

The same row also recorded command/minibuffer chord forwarding as
outstanding when `bc32332` shipped it. Marked shipped rather than left
to imply work that does not exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-11 23:12:04 +02:00
Levi Neuwirth 4e6730acb9
docs: GUI Stage 1 framing (approved) and the 1-pre lane
The framing and the lane are this branch's FIRST commit, which is the
arc's §5 rule for every PR in it and the reason this branch exists
before any code: revision 9 was approved as an untracked file, and an
approved framing that lives in one worktree is not portable.

Revision 9 landed after EIGHT rejected revisions. The through-line of
those rounds is worth carrying: each early draft turned a fresh source
audit into SCOPE LABELS --- "DroppedFile", "translate_key completion",
"no behaviour change" --- and review kept asking the same question,
which is what the label means when someone has to test it. Revision 9 is
a contract/witness/mutation matrix over every numbered clause, split
into CHANGE clauses that fail today and PRESERVATION clauses that pass
and must keep passing.

Several findings only existed because the contracts were written down.
"Send Detach before exit" was unexecutable against an outbox whose
`enqueue` rejects once closed. A one-slot terminal reservation
contradicted the FocusLost-before-Detach ordering two sections above it.
"Terminal result after the commit resolves" was not total over a
pipeline with three legitimate exits that never commit --- a claimed
listener, a cleared handler slot, and the default handler's `open_async`
returning immediately. None of those are visible from a scope label.

This branch is 1-pre ALONE: the input seam, no behaviour change. Its
evidence is a headless routing harness recording routing decisions,
outbound events AND local effects, with production `window_event` as a
thin call-through. P3 is recorded as an accepted structural exception:
a headless test cannot construct `ActiveEventLoop`, so the delegation
itself is a code-review invariant rather than a tested one, and saying
so is better than a mutation that cannot exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-11 23:10:32 +02:00
Levi Neuwirth a994f377d2
docs: require the full gate suite for GUI Stage 0
The governing pre-PR gates apply to documentation-only branches too.
Replace the reduced fmt-and-diff declaration with the complete suite,
while recording that there is no touched acceptance suite for this lane.
2026-08-11 21:24:40 +02:00
Levi Neuwirth c2b613c28d
docs: Stage 0 --- a total grading rule, honest counts, and the recovery floor
The recovery authority contradicted itself in the direction that
matters: the floor said `9a26ac8` while the operative check said
`e67ad07`, so the stated floor would have ACCEPTED a tree missing
everything since 2026-08-08. Floor and check are now both `e67ad07`.

The exercise claim is corrected rather than quietly inherited. That
paragraph argued its own value --- advancing the base is exactly when
the procedure must be re-run --- and then presented a 2026-08-08 run at
`9a26ac8` as if it covered today's base. It is now marked as the LAST
exercise, with the advance to `e67ad07` recorded as an OUTSTANDING
obligation. What is unverified is the procedure against the new base,
not the SHA.

The grading rule was not total. `Weak` required a non-empty blocker list
and `Partial` named blockers, which left a real state ungraded:
criterion 1 passing, blockers empty, criterion 3 failing or unassessed.
The rule now turns on criterion 1 alone --- pass is `Partial`, fail is
`Weak`, all-pass is `Strong` --- so every usable state is covered by
exactly one grade. Criterion 1 is the right discriminator because it is
the only one measuring the GPU AGAINST THE TUI; the others measure it
against its own goals. Today's grade is unchanged and now derives.

The listview counts were wrong because I used `grep -c`, which counts
comment mentions. Four executable calls in `lsp.lua`, one in `git.lua`
--- not five and four. The claim those numbers support is untouched:
listview is no longer LSP-only.

Two status claims corrected: the lane understated its own commits (15,
actually 16), now stated AS OF a SHA so it cannot rot on the next
commit; and the handoff said "Stage 0 in PR" when no PR exists ---
"ready for PR (not yet opened)".

The inference ruling is recorded where the flags live: they do not block
this docs PR, they must be closed before the table is first enforced as
a gate, 3(c) and 9 can change the deficit set, and 6(c) cannot change
step 6 today but still owes evidentiary closure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-11 21:15:23 +02:00
Levi Neuwirth 30e22090ae
docs: Stage 0 --- six review findings, including the ledger it exists to fix
P1, and the worst of them: the AUTHORITY BLOCK was stale in four ways
while this lane's whole purpose was retiring stale ledger state. The
canonical base said `122b8e8` (actual: `e67ad07`), protocol said
`v6..=v22` (actual: v23, #228), the recovery threshold accepted
`9a26ac8` --- far older than the base above it, so the check it defines
would pass on a tree missing everything since --- and this lane's own
entry named base `0e4c58d` and declared itself PARKED beneath a heading
saying ready-for-PR. A ledger that contradicts itself defeats its own
cross-machine purpose. All four corrected; the park bullet is replaced
by what actually happened.

"Exactly two journey deficits" was stronger than the evidence permits,
and the table said so three paragraphs below itself: GPU 3(c) and 9 are
INFERRED, so a third failure cannot be ruled out. Now "known failures at
two steps; exact set pending verification". GPU 6(c) is inferred too and
is noted as unable to change step 6, which 6(e) already floors.

The product subgrade had criteria but no GRADING RULE --- three closure
conditions distinguish closed from not-closed, which does not derive
`Weak` rather than `Partial` or `Missing`. A rule is now stated, and the
distinction it turns on is worth having: `Partial` would mean the GPU is
merely unfinished; `Weak` means it is BEHIND a frontend shipping in the
same binary. Criterion 3 is marked NOT ASSESSED --- §3.2's register is
seeded empty --- rather than being quietly counted as a pass.

The framing REQUIRED a handoff §6 cross-reference and there was none;
only the Lean 4 numbering note existed. Added, carrying the facts a
cross-machine reader needs first: the product subgrade, the P2 start
gate, and that "Arc 8" means Lean 4.

Three more scorecard-versus-ground-truth drifts, the same class this
lane exists to close: row 14 said listview has 4 call sites "all LSP
panels" (it has 5 in `lsp.lua` and 4 in `git.lua` since #227, so it is
no longer LSP-only); row 15 said "no git integration at all" while §15
records status and diff; and §9's plane table said no builtin opens
`*lsp*`, though `M-x lsp.status` does (`lsp.lua:3533`) --- it has no
BINDING, which is the distinction this document now draws everywhere
else.

Finally, §20 physically placed the arc after Priority 5 while its prose
said after Priority 1. In a *Recommended Priority Order* position is the
schedule, so the block moves to sit directly after P1, and now states
its interleaving with P2-P5 explicitly rather than leaving it to
position alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-11 21:10:32 +02:00
Levi Neuwirth a4d6ba4028
docs: Stage 0 --- §16 gains a product subgrade, §20 places the arc, Arc 8 retires
The three remaining Stage 0 items, and the first is the one that
justifies the arc existing.

§16 now carries TWO subgrades. Architectural stays Strong; Product is
Weak. The single grade was true and was answering the wrong question ---
the protocol is excellent AND the GUI is not yet a daily driver, and one
cell could not hold both, so the product half was invisible. That is how
a frontend the reporter would not choose came to sit inside the
healthiest concern in this document.

The product subgrade has criteria, not adjectives: GPU >= local TUI at
every journey step (currently FAILS at 5 and 12, per §2b); the
daily-driver blocker list empty (currently nine); every surviving
divergence declared. The GUI arc is what moves it, and it should be
re-graded when the arc closes, not before. The scorecard row now POINTS
at §16 instead of carrying a grade, which is the "no orphan scorecard
row" ruling from revision 3 --- criteria and ground truth first, pointer
second.

§20 places the arc: Half A after Priority 1, with reaching Stage 4b as a
P2 START GATE. No later GUI stage begins until P2 has an approved
framing and an opened lane; the gate is on STARTING P2, not finishing
it, so the arc is never blocked on work nobody has begun and cannot
outrun the model it depends on.

Arc 8 retires as a GUI label (Q#GA4). "Arc 8" now unambiguously means
the LEAN 4 arc, which is the numbering that stays; the roadmap's "Arc 8
--- GPU structural parity" scope is Half B of the GUI arc, which is a
name and not a number. Recorded in `docs/agent-handoff.md` beside the
Lean 4 entry too, because that is where a cross-machine reader meets the
number first. Landed framings that say "Arc 8 adjacent" keep their text
--- they are dated records, and this is the disambiguation for anything
written from here on.

Also swept while in §16: its ground truth still said
`SUPPORTED=[6..=21]`. Same stale-range class as the scorecard row fixed
earlier in this lane, one section deeper, and found only because the
subgrade work required reading the section rather than the row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-11 20:56:11 +02:00
Levi Neuwirth be73766638
docs: Stage 0 --- `*workers*` has a binding; what it lacks is one that OPENS it
"`*workers*` has no binding at all" was false. The view carries a
buffer-local `C-c C-k` for `workers.cancel-at-point`
(`builtin/runtime/async.lua`, documented at `docs/keybindings.md:282`).
Corrected at all four sites it had spread to: the table row, the
contract paragraph, the summary, and the framing's historical example.

The absent thing is narrower and is now said precisely: NO BINDING
OPENS `editor.list-workers`. A binding that only works once you are
already inside the view cannot satisfy a discoverability subclaim ---
which is the same shape as the route-versus-binding distinction the
previous commit drew, one level further in. A route reaches the view
but is not a binding; `C-c C-k` is a binding but does not reach it.
11(c) asks for the conjunction, and neither half alone supplies it.

The Missing grade is unchanged, and so is the two-step GPU deficit. What
changes is that the row now states a fact about the tree rather than an
overstatement that happened to point at the right grade --- which is the
distinction this table exists to hold, since a grade resting on a false
premise is not falsifiable, it is merely unfalsified.

Left alone deliberately: the audit quote at COHERENCE.md:562, where
"no bindings at all" refers to opening a file, opening a terminal and
running a build. That is a historical observation about OTHER commands,
and the paragraph immediately below it already records that all three
were answered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-11 20:32:03 +02:00
Levi Neuwirth 926ac75def
docs: Stage 0 --- retract a "verified", fix the contract, split route from binding
Three findings, one of which is a retraction.

GPU 3(c) is inferred again, in BOTH halves. The previous commit called
its rendering half "verified" on gpu_invocation_acceptance.rs:706. That
test runs `--headless-managed-probe`, which connects, receives and
DECODES a `BufferSnapshot` into text; it never constructs GPU render
state and never calls `render_offscreen` (pmacs-gpu/src/main.rs:1065).
It witnesses delivery and decoding, which is real and is not rendering.
Retracted rather than softened: "verified" was the word doing the
damage, and this file is where a reader goes to find out what has
actually been measured.

The discoverability contract said the four welcome key entries decide
every discoverability grade --- while the table two paragraphs below it
graded steps 4 and 7 on welcome PROSE and on the help graph. A standard
cannot carry both. The contract now states what it actually applies:
advertisement includes the welcome's prose (`M-x`, `M-x help` at
welcome.lua:61) and is TRANSITIVE through the help graph, which is how
`help.list-keybindings` carries browse and symbol.

That transitivity forced the distinction 11(c) was already resting on
without saying so: a BINDING is a stronger claim than a ROUTE. Step 7
passes because its commands are bound and a listing names them; step 11
fails because `*workers*` has no binding for any listing to name. So the
summary's "no advertised route at all" was FALSE --- the route is
`M-x help` -> `help.list-commands` -> `editor.list-workers` --- and the
Missing grade survives on the binding, exactly as 11(c) is worded.

The framing's worked examples are marked HISTORICAL, with COHERENCE.md
§2b named authoritative. A framing and the standard it serves were
handing a reader two different grades for step 7. Both examples keep
their point: the head-grade rule would have graded BOTH `Works`, which
is what makes it unsound. Step 7 moving on better evidence is the system
working; step 11 not moving is the defect surviving contact with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-11 20:25:23 +02:00
Levi Neuwirth 3fc12ac4c2
docs: Stage 0 --- retire five merged lanes, after re-homing what was missing
Rule 4 retires a lane only once its durable facts have reached
`docs/agent-handoff.md`, so that was CHECKED per lane rather than
assumed. Four of the five were already covered --- the `commit_to`
capture-at-request-time lesson (#231), per-worktree gate target dirs
(#225), the `build-crdt` step and the `gpu_invocation` signature
(#228/#229), the v23 frozen-variant lesson (#228). One was NOT:
`texlab` appeared ZERO times in the handoff, so #230's lane could not
be retired yet and its facts are added first --- LaTeX is served by
texlab, its root comes from an upward `.texlabroot` walk verified in
texlab's own source, and `.git` is deliberately excluded because a
repository root is the wrong answer for a multi-file document.

The handoff's Fake LSP mode list was stale in the same breath: it named
four modes when the binary has twenty-eight. Enumerated from the binary
and grouped, because a stale mode list is precisely how a test ends up
covering the shape next to the defect --- which is the failure this
lane has now watched happen twice in the file-watcher work.

Retired: #224, #225, #228's PR-specific block, #230, #231. That is 737
lines of ledger describing work that has shipped.

#228 is the one that is NOT a deletion. Its PR block is retired INTO
the standing Discovery lane, which is rewritten to "Stages 1 and 2
merged; later work remains" --- deleting that lane on the strength of
one merged stage would have dropped four named pieces of open work.
While rewriting it, its first open item turned out to be what Stage 2
actually shipped (richer M-x rows over v23), so it is removed and the
rest renumbered: a completed item sitting in an open list is the same
staleness this commit exists to clear, one level down.

Left deliberately: #229's and #232's lanes, which are already labelled
MERGED accurately. They are Rule 4 candidates but not STALE, and
widening this commit to them would mix "the ledger lied" with "the
ledger is verbose".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-11 19:33:09 +02:00
Levi Neuwirth 9460bf721c
docs: Stage 0 --- correct three claims the tree had already falsified
Rebased onto e67ad07; the framing commit replayed with no conflict.

Three corrections, each verified against the tree rather than against
this lane's own notes:

COHERENCE.md said `v6..=v21` schema support. The ceiling moved TWICE
since: #221 to v22 for `LineWrapFacts`, #228 to v23 for
`MinibufferPromptRows`. Now v23. The same row's "production attach
remains v20" is correct --- `ADVERTISED_PROTOCOL_VERSION` is 20 --- and
is deliberately left alone.

Journey step 11 read "Works but undiscoverable --- no statusline
spinner/progress indicator anywhere". #232 shipped exactly that
indicator on 2026-08-09, so the row went stale the day it landed. Now
Partial, with what actually exists and what does not: the indicator is
there, the `*workers*` view still has no keybinding.

§9's "No progress indicator exists anywhere" carried the grep that was
the evidence for opening worker identity in the first place. Corrected,
with the part that did NOT change stated as plainly: a purpose says
what a job is doing, never who asked, and attribution is what §9
grades. THE SECTION'S GRADE IS LEFT UNTOUCHED pending a re-audit ---
moving a grade is an audit act, not a documentation correction, and
Stage 0 is docs-only.

U9's row claimed "whatever this is, it is not the tree" about a
same-tree green. This file's own rerun rule forbids that: a same-tree
green establishes intermittence only, and a tree can raise an
intermittent failure RATE without making it deterministic. Replaced
with "not deterministic on this tree; causation and rate effect
unresolved", and the wrong claim is quoted rather than deleted, because
it is the one a later reader would otherwise reach for.

Also corrects this lane's own earlier claim that `add0ba1` had done
half of Stage 0's absorption. It absorbed #227 and #234; five stale
lanes and both COHERENCE corrections remained.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-11 18:18:17 +02:00
Levi Neuwirth d2c51338fb
docs: the GUI arc framing, revision 3 approved --- and its Stage 0 lane
Opens the GUI arc. The framing and the lane are this branch's FIRST
commit, which is both the arc's own rule for every PR in it and the
reason this branch exists today rather than after #227: the document
spent two review rounds as an untracked file in one worktree, and
uncommitted work does not travel between machines.

Revision 3 closed two blocking findings from the second round.

The closure comparison is falsifiable now. Revision 2 ordered its
grades but never said how a cell acquires one, and its "normalize to
the head grade" rule mis-graded exactly the rows it was written for:
journey steps 7 and 11 both read "works but undiscoverable" and would
both have graded `Works`, with the undiscoverability demoted to prose
that cannot block closure --- while discoverability IS the substance of
steps 4, 7 and 11. Grades now have observable criteria, a compound step
takes the minimum over declared subclaims, and the table carries three
columns (local TUI, attached TUI, GPU) compared against the local TUI.

Half B's dependency graph was inverted. Viewport facts are ABOUT a
window, and whether a semantic window is a daemon projection or a
frontend-local object is what the model stage decides --- so the facts
cannot be designed first. Revision 2 also had a framing-only stage
handing the sidebar geometry no stage shipped. The order is now model
framing, then the viewport/identity substrate, then splits plus
IMPLEMENTED side geometry, then the sidebar.

Q#GA4 through Q#GA12 are ruled and closed.

Four claims this revision inherited were wrong and are corrected with
their sources rather than quietly dropped: the daemon has always
emitted `InstanceSignal::Bell` (daemon.rs:1373), so Bell is
consumer-only; the GPU NEVER consumes daemon `view_top`, which the
backlog states in as many words; Q#DS9 scopes save AND restore, so
Stage 4b owns both; and the protocol ceiling is v23, not v22 --- #228
raised it after #221, and revisions 2 and 3 both read the range at the
audit anchor without re-reading it afterwards.

The three-column claim is narrowed to what it can support. The attached
TUI handshakes `semantic_render: false` and never consumes the
SemanticFrame family, so a shared GPU/attached gap cannot implicate the
semantic wire. The columns separate local from daemon-attached, and
attached-grid from semantic; source tracing still assigns ownership.

Q#GA6's citation is corrected to the one site that matters: local and
attached document events converge on the document handler, whose only
wheel arms are Up/Down at editor.rs:3189. The sites revision 3 cited
were terminal-content paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-11 18:15:31 +02:00
Levi Neuwirth e67ad07352
docs: absorb the file-watcher arc --- issue #233 closed by #234 and #235
The arc completed at 122b8e8, so per rule 4 the lane comes out of the
ledger: durable facts move to the handoff section-1 arc bullet (the
pool-thread cost of sleep jobs, the loops-that-never-run cancellation
lesson, the group state machine and its double-enforced retirement,
the server-owned scan root with its reachable deterministic fallback,
the core-count-assumption CI lesson and the taskset rehearsal, probe
and witness-bound discipline, the stray-marker re-rooting hazard, and
the deliberately unbuilt kernel-notification option). The handoff
anchor and the canonical-base line move to 122b8e8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:14:06 +02:00
Levi Neuwirth ac02952c91
docs: remove the last superseded unreachability claim from the D3 lane
The four-blockers bullet kept its own copy of the round-one claim that
round two disproved; both copies now point at the manual-spawn
adoption path. The facts-list heading no longer counts itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 17:39:35 +02:00
Levi Neuwirth bf1a0ac1be
fix(tests): D3 round three --- two witness overclaims, and the CI red was a core-count assumption
Review round three on PR #235, plus the diagnosis of its first CI run:
all five test legs failed on one test, deterministically, while
sixteen local cores stayed green.

The mid-walk cancellation bound could not bite the per-entry poll
alone. The cancel lands two files into a 41-file directory --- root
contributes three dir entries --- so with the per-entry poll deleted
the directory finishes and the per-DIRECTORY poll catches at seen ==
44, under the old bound of 60. The bound is now 40 against an expected
exactly-35 (3 + one 32-entry poll stride), and the entry-poll-only
bite goes red at 44. Verified both ways.

The retirement helper observed a REQUEST, not settlement: it returned
as soon as an active row showed cancel_requested, which a worker that
ignored the token and completed successfully would satisfy. It now
waits for a completed row with status == "cancelled", making the
lane's "settles cancelled" claim true at the witness, not just at the
Rust layer.

The CI red: d3_pump(1600) between the mid-walk join and the late.bbb
write assumed the held walk would complete within 1.6 s. On a 3-thread
CI pool, 8 sleeps of 1200 ms drain in ~3.6 s of waves, so the file
landed before the held walk even STARTED and folded into the joiner's
baseline --- exactly the fold the test exists to assert for mid.bbb,
applied to the wrong file. Deterministic on every 2-4-core runner,
invisible on 16 cores. The drain is now an observable condition ---
at least one post-join walk completed and none active --- with the
saturation sleeps at 800 ms, and the three saturation tests plus the
whole eighteen-test family re-run green under taskset -c 0-3, the CI
pool shape reproduced locally.

A fixed-duration pump against pool-dependent timing is a core-count
assumption in disguise; the lane records it as such.

Superseded round-one text in the lane (the fallback "unreachability"
claim round two disproved) is corrected in place.

One gate run also hit the live attach-retry BrokenPipe row --- fourth
occurrence, all three required fragments verified against the durable
sweep log, recorded in docs/ci-red-signatures.md. This lane touches no
pmacs-gpu code, no wire, and no protocol; the same sweep passed twice
earlier the same day on materially the same tree. The retirement bar
(mechanism, not rate) is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 17:18:47 +02:00
Levi Neuwirth 888c40d963
docs: record PR #235 in the D3 lane
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 15:00:10 +02:00
Levi Neuwirth db24abb64e
feat(lsp): D3 --- the file watcher stops sleeping and walks once per scan (#233)
Implements docs/lsp-file-watch-d3-framing.md revision 4, approved
2026-08-11 with the four rulings adopted as proposed: the honest bar
(absent at idle, one attributable job per concurrently due group), no
exclusions by default, server root_uri -> cwd -> attachment fallback,
and constants rather than config keys.

pmacs.fs.walk_tree: the whole recursive tree as ONE cancellable job
(JobKind::FsWalkTree, reply reuses ReplyKind::ReadDir --- identical
payload shape, so the Lua boundary needs no second conversion). Names
are base-relative; symlinks recorded, never traversed; an unreadable
subdirectory skips its subtree (scan_tree's pcall behaviour); only the
root failing to open fails the walk; the cancel token is polled once
per directory. Eight Rust unit tests, including flat-directory entry
parity with read_dir_blocking and the two review-round cancellation
cases (empty-tree pre-cancel; mid-walk via the cfg(test) entry hook).

The watcher itself is rewritten as the framed group scheduler. No
sleeps anywhere: one process.after-tick subscription (installed once
and guarded --- pmacs.hook.remove does not exist) drives every
(server, base) group's deadline off monotonic_ms, autosave's Q#AS2
idiom. The old design held one pool thread per sleeping watcher and
allocated 1 sleep + D read_dir jobs per watcher per tick --- 1,326
per tick for rust-analyzer's six watchers on this 220-directory
checkout. At idle there is now NO running job, which is also the
strongest witness in the suite: activity_summary settles to None, and
that assertion is unwritable under the old design.

The scheduler is the framing's state machine, all three review rounds
included: single-flight per group with generation-checked completions;
deadlines advanced from completion; the round-3 three-arm completion
partition (success / stale-or-retired / live non-success, with the
failure latch and quiet cancellation); joins wake the group, queue
exactly one follow-up mid-walk, and never reset the backoff curve;
per-watcher baselines --- the first snapshot whose WALK STARTED after
the join; membership captured at scan start; per-member cancellation
recheck at emit through the preserved _after_scan_for_tests seam;
backoff 250ms x2 to a 4s cap, reset by any emitted change; retirement
cancels the in-flight walk cooperatively.

Verification: eighteen acceptance tests. The six #234 tests are
byte-unchanged and green. Ten witnesses cover the framing's plan (the
review rounds added the fallback-determinism and root-boundary pair,
making twelve):
idle absence (and never a sleep purpose), one walk job per scan on a
twelve-directory fixture, join-wakes plus the registration epoch,
queued baseline for a mid-walk join (driven by saturating the worker
pool so the walk genuinely queues), single-flight under a withheld
completion pump, retirement and rebaseline through the fake's
unregister/re-register triggers, live cancel via pmacs._async._cancel
on the queued job, live failure with the once-per-error latch and the
preserved-snapshot recovery (DELETED for the pre-failure file is only
derivable from the retained snapshot), backoff shape from seam
timestamps, and the configured-root base.

Every witness was mutation-tested. Two findings from the bites:

- Retirement is DOUBLE-ENFORCED (unregister path and post-scan sweep)
  and biting either copy alone is masked by the other; only biting
  both goes red. Kept deliberately: the sweep covers seam-cancelled
  members, the unregister path covers idle groups whose next deadline
  is seconds away.
- The first idle probe was VACUOUS: it read pmacs.async instead of
  pmacs._async, errored, and the unwrap_or_default made every sample
  read as "absent". The probe now expects rather than defaults, so a
  broken probe is a red test, not a green lie.

One environmental fact, recorded in the lane: an empty stray /tmp/.git
(since removed) made project detection root every markerless tempdir
fixture at /tmp, which under Q#D3-3 the watcher then faithfully
watched. A markerless-fixture red that looks like a watcher bug may be
an ancestor marker.

A pre-commit review round found four blockers, all fixed here:

- walk_tree checked cancellation only inside its entry loops, which an
  EMPTY tree never enters --- a pre-cancelled queued walk returned an
  empty SUCCESS, which the success arm would commit and diff into a
  deletion storm. Cancellation is now checked before opening and
  before returning, cancellation outranks a missing-root error, and a
  unit test pins both.
- The neither-root-nor-cwd attachment fallback was still pairs-order
  nondeterministic --- the exact accident D3 set out to remove, behind
  a comment claiming otherwise. It now takes the lexicographically
  smallest attachment directory. Verified at the spawn sites: every
  server spawned with an attached file gets cwd = root, so the arm is
  defensive and unreachable through production spawning --- which is
  also why it carries no through-the-server witness.
- A base at the filesystem root joined as //path (and file:////path in
  URIs). Both join sites now go through join_under, the root-aware
  idiom dired's handler already uses, and the dir-of capture for a
  root-level file ("" from the match) normalizes to "/".
- The walk-count and scan-times probes defaulted on error, so two
  broken probes could compare equal and pass the retirement witness.
  Every probe now expects --- a broken probe is a red test, the same
  correction the vacuous idle probe forced.

A second pre-commit round found three more, all fixed here:

- Mid-walk cancellation was UNWITNESSED: both Rust cancel tests
  pre-cancelled and the acceptance test cancelled a queued walk, so
  deleting the internal polls left every test green. A cfg(test)
  entry hook now flips the token at an exact entry boundary and the
  witness asserts the walk stopped NEAR it (bound on entries
  processed), which is what discriminates the polls from the
  entry/exit checks. The retirement witness now holds a walk in
  flight across the unregister and asserts the job settles cancelled.
- The "unreachable fallback" claim was WRONG: pmacs.lsp.spawn may
  omit both cwd and root_uri, and ensure_server adopts such a live
  server for markerless files (root_uri and key_uri both nil). The
  lexicographic-minimum fallback now has a through-the-server
  witness: five sibling directories, the minimum opened last ---
  five, because with two the build's hash order coincided with the
  lexicographic answer and the first-pairs bite survived.
- The root-boundary joins gained a witness through exported
  production functions (the _deliver_status pattern): the matcher and
  URI builder driven at base "/", where reverting either join_under
  call makes the anchored glob refuse //hit and the URI grow a fourth
  slash. No fixture can walk / for real.

Verification totals after both rounds: eight walk_tree unit tests,
eighteen acceptance tests (six byte-unchanged, twelve witnesses), all
mutation-verified.

No wire change, no PROTOCOL_VERSION bump; walk_tree is an fs binding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 14:59:28 +02:00
Levi Neuwirth ec3473d598
docs: close D3 scheduler non-success transition
Revision 4 absorbs review round 3. A live walk cancellation or failure
now has an explicit group-state transition: no snapshot, epoch, or emit;
the prior state and backoff survive; in-flight clears; queued joins run
immediately; otherwise the group reschedules. Distinct failures report
once until a successful scan clears the latch.

Correct the queued-baseline latency bound and add live-cancel and
live-failure witnesses. Record the round in the active-work lane; the
four user rulings still block implementation.
2026-08-11 13:00:41 +02:00
Levi Neuwirth 9c644b0ae7
docs: D3 framing revision 3 --- the scheduler becomes a state machine
Review round 2: two P1 design gaps and one P2 overclaim, all in the
cadence revision 2 introduced.

A join now wakes the group: next_scan_at pulls to now, an in-flight
walk queues exactly one immediate follow-up, and a baseline is only a
snapshot whose WALK STARTED after the join --- an in-flight walk may
have passed a directory before a pre-join file appeared there, so its
snapshot as a baseline would turn that file into a false CREATED.
Without the wake, a backed-off group folds post-registration files
into the baseline and never reports them; today registration scans
immediately and the coalesced design must not regress that.

The group is now a defined state machine: single-flight per group
(overlap unrepresentable, not avoided), deadlines advanced from
completion (a walk outliving its interval degrades to back-to-back
scans, never overlap), stale completions rejected by generation
(#234 P2 at group scope), and retirement --- last member gone or
server death --- that cooperatively cancels the walk. Cancellation
therefore enters walk_tree contract and tests; polling the cancel
token between directory reads is the established job shape. The
after-tick subscription installs once and guards, because
pmacs.hook.remove does not exist (the P3 gap).

Q#D3-1 restated honestly: groups key on (server, base), so several
can be due on one frame and per-group single-flight still permits N
jobs. The bar offered is absence at idle plus one attributable job
per concurrently due group, with a global scan queue as the
alternative if one-at-a-time must be guaranteed.

Six round-2 witnesses join the plan: join-wakes, no-overlap,
retirement, queued-baseline, plus the round-1 epoch and idle pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 12:48:41 +02:00
Levi Neuwirth 0aac3b8992
docs: D3 framing revision 2 --- round 1 removed the sleep, the guess, and the skip
Review round 1 (five findings, four P1) and revision 1 did not survive
it. The findings are recorded in the document; three are cases where
the framing reasoned from the wrong mechanism.

The promised idle state was impossible: pmacs.workers.sleep is a
RUNNING job for its whole duration --- dispatched onto the worker
pool, sleeping in 1ms slices on one of available_parallelism - 1
threads --- and activity_summary counts every running job. A 4s
backoff sleep renders as a constant "sleep 4000ms", and filtering it
would touch the instrument. Revision 2 removes the sleep from the
design: one process.after-tick subscription owns every group's
schedule via monotonic_ms --- autosave's Q#AS2 idiom, whose own
comment names the pool-thread hazard. Waiting now allocates no job
and no thread; the indicator is absent at idle by its None-at-zero
contract, which also becomes the strongest witness in the plan.

The scan root is the server's, not a guess: root_uri then cwd then
the attached-file fallback. project.detect was wrong by the tree's
own testimony --- server rooting honors configured strings and
resolvers first, and texlab's Q#LX2 documents a root that detect can
never produce.

Coalescing gained delivery semantics: shared snapshots, per-watcher
baselines (the first snapshot completed after join), membership
captured at scan start, cancellation rechecked per watcher at emit
(#234's P2 rule, per member). Two epoch witnesses join the plan; the
six existing tests do not cover this and were never claimed to.

Exclusions default to NONE. Any unconditional skip deviates from the
registered glob contract (**/-leading globs can match under .git/,
and a server may register .git/HEAD outright), and walk_tree removes
the job-count economics that made skipping look necessary: 80% of
jobs becomes readdir syscalls inside one job.

Arithmetic corrected to this checkout: D = 220, so 221 jobs per
watcher per tick and 1,326 for rust-analyzer's six --- six of them
pool-thread-holding sleeps. Revised steady state: zero jobs at idle,
one walk_tree job while a scan runs.

Q#D3-1..4 rewritten accordingly; all four still block implementation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 12:37:40 +02:00
Levi Neuwirth bfa1e77d1b
docs: frame D3 --- the file-watch polling cost (issue #233)
Framing and lane update at the branch's first commit, portable during
review. Revision 1 is a DRAFT and no implementation may start from it.

The remainder of issue #233 after #234: the watcher is correct and
still walks everything. The framing re-verifies the cost model at
add0ba1 and adds two facts the issue does not carry:

- On this checkout .git alone is 177 of 220 directories --- over 80%
  of every walk --- and the machine does not even have an in-tree
  target/ (external CARGO_TARGET_DIR). The issue's 187/46 numbers were
  measured with one.
- The string-form base is NONDETERMINISTIC: resolve_watcher takes the
  directory of whichever attachment pairs() yields first, so which
  tree a bare-string watcher can see depends on table hash order. #234
  made matching correct per base; which base is still accidental.

Design space A-E with the trade-offs stated: coalesce per (server,
base); a Rust walk_tree primitive (one job per scan instead of one per
directory --- no new crate, no wire change); an ignore list, with the
target/ staleness hazard named (rust-analyzer's **/*.rs covers
OUT_DIR outputs, so an aggressive default trades churn for staleness
in exactly the server the issue is about); idle backoff; and kernel
notification, deliberately staged separately because A-D are pure wins
it does not obsolete and a new-crate decision deserves its own
framing.

Proposed Stage 1: walk_tree + coalescing + backoff, VCS-only skip
default, project-root base preference. At rest on this repo: from
~1,100 jobs per scan-bound tick to one job every four seconds, named
for its root.

Four rulings block implementation (Q#D3-1..4): the acceptance bar
(one attributable blip at idle, not silence --- silence is Stage 2),
the skip-list default and where it lives (a list-valued setting is
not expressible in today's ConfigValue), the string-form base change,
and knobs vs constants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 10:03:57 +02:00
Levi Neuwirth add0ba1a20
docs: absorb #234 and #227, discharge the hold, and rule D3 next
The absorption pass after the two merges of 2026-08-11: the LSP
file-watcher fix #234 (ae84d58) and git integration Stage 1 #227
(b867f64), landed in that order so the 2026-08-10 hold on #227 was
discharged rather than overridden.

The handoff anchor moves 9a26ac8 -> b867f64 and lists the ten-merge
first-parent chain between them. Only #227 and #234 are absorbed into
section 1 at this anchor; the eight between keep their facts in their
ledger lanes, several of whose headers still say OPEN --- the anchor
paragraph says so explicitly and repeats the ledger's own rule: trust
the canonical-base line over any lane header.

Both lanes are rewritten to their remainders per rule 4's arc test:

- LSP file watcher: the arc is issue #233 and it stays open until D3
  closes it. The lane now carries the D3 start-state --- the walk
  still recurses into everything every 250ms, no notify dependency in
  the tree, no ignore-list infrastructure to reuse --- and the user's
  ruling that D3 is next.
- Git integration: Stage 2 (gutter markers) is the remainder, and the
  lane preserves the one scheduling constraint that matters: it needs
  new DecorationKind variants, a PROTOCOL_VERSION bump, so it must run
  alone. The five-round review history stays in the PR and framing;
  what the handoff absorbs is what the next lane needs: the
  capture-at-invocation census and its single-mechanism fix, the macOS
  EILSEQ portability fact and its latent crdt-module sibling, the
  root-parsing byte lessons, and the copy/rename presentation ruling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 09:59:26 +02:00
Levi Neuwirth e2394c7ded
Merge remote-tracking branch 'githubsucks/main' into git-status-stage1 2026-08-11 09:23:34 +02:00
Levi Neuwirth 2a16e0eed5
fix(lsp): read the glob form from the pattern, and stop a cancelled scan emitting
Review of the #233 implementation found two correctness defects. Both
are cases where the FIRST fix for #233 was itself wrong, which is worth
naming: this lane repaired absolute globs and, in the same change, broke
a case that had worked since May.

P1 --- `resolve_watcher` returned "absolute" for EVERY string, so the
form was carried but derived from the union arm rather than from the
pattern. A bare `*.txt` is a valid relative pattern (LSP 3.17 defines
`Pattern` relative to a base path; VS Code treats string watchers as
applying across workspace folders), and classifying it absolute matched
it against `<base>/foo.txt`, which `^[^/]*%.txt$` can never match. A
leading `/` is what makes a pattern absolute. The `filewatchflat` test
could not catch this: it sends the RelativePattern OBJECT form, so it
constrains the object arm, never the string arm the regression lived in
--- the framing's own F1 finding, repeating inside the lane that named
it.

P2 --- `scan_tree` awaits `read_dir` once per directory, so the watcher
coroutine sits suspended for most of a tick with `_sleep` already
cleared. A cancel arriving there sets `cancelled` and has no sleep to
interrupt, and the resumed scan ran on to `did_change_watched_files`:
one stale batch under the superseded pattern, which is a wrong-pattern
notification the server acts on. Cancellation and liveness are rechecked
after the scan.

Both tests are mutation-checked and each bite fails only its own defect.
Reverting P1 fails the bare-string test alone --- the absolute test
still passes, so the two readings are independent. Deleting P2's recheck
reproduces the defect verbatim: `.received = "1 file:///…/foo.txt"`,
a batch from a watcher that was already cancelled.

P2's witness needs a seam. The race is a cancel landing during one of
the scan's suspensions, which no arrangement of real timing produces on
demand --- the same situation, and the same device, as `git.lua`'s
`_deliver_status`. `pmacs.lsp._after_scan_for_tests` is nil in
production and is handed the SCAN RESULT deliberately: a test that
cancels on any other scan passes with the recheck deleted, because the
loop would break at the post-sleep check and emit nothing anyway.

Gates: 9/9 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-11 09:00:15 +02:00
Levi Neuwirth 0723017754
docs: record PR #234 in the file-watcher lane
The lane header carries the PR number and the ref it was opened at, so
the next machine can find the review without searching GitHub.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 23:30:57 +02:00
Levi Neuwirth ed3033c1fb
fix(lsp): carry the GlobPattern form so plain-string globs can match, and cancel superseded watchers (#233)
Implements D1 and D2 from docs/lsp-file-watcher-framing.md (revision 1,
approved 2026-08-10). The user ruled that the walking survives this
lane; D3 gets its own framing.

D1. resolve_watcher now returns (base, pattern, form) and the watch
record carries the form. Per LSP, a plain-string glob matches the
file's ABSOLUTE path while a RelativePattern's pattern is relative to
its base --- and resolve_watcher discarded the distinction, so every
downstream consumer matched relatively. Real servers send absolute
globs: rust-analyzer was never told about any file change (all six of
its globs absolute), and gopls saw go.mod but never a .go edit. The
match subject is chosen in start_file_watcher --- form "absolute"
matches base .. "/" .. rel, form "relative" matches rel unchanged.
scan_tree still walks in relative terms; only the string handed to the
matcher changes.

D2. register_file_watchers now cancels the outgoing record list before
file_watchers[skey][reg.id] = recs drops the only reference to it. The
cancel loop is factored into cancel_watch_records, shared with
unregister_file_watchers, so the two paths cannot diverge.
rust-analyzer registers the same id twice with no unregister between
--- previously 12 concurrent pollers, 6 permanently uncancellable.

Verification, per the framing's plan. Three new fake-LSP modes and
tests beside m4_24, each mutation-tested against the defect it names:

- filewatchabs registers a plain-string absolute glob whose relative
  reading matches nothing. Red before D1 (reverting the match subject
  fails exactly this test), green after.
- filewatchflat registers a RelativePattern without a leading **/
  (*.txt at the base) --- F2's guard. Matching every form absolutely
  fails exactly this test, so the obvious wrong fix cannot land green.
  It also pins that a base-level pattern does not match into
  subdirectories.
- filewatchrereg registers the same id twice (rust-analyzer's shape).
  The witness is observable polling, not table shape: f.old exists on
  disk before either .new event lands, so a leaked watcher at the same
  250ms cadence reports it before the second positive. Reverting D2
  fails exactly this test, the leaked .old event visible in .received.

m4_24 stayed green under all three mutations --- the framing's F1
finding (the existing test is insensitive to D1 in both directions),
confirmed rather than assumed. It is kept unchanged.

The framing doc records the approval and the answered ruling; the
active-work lane moves to IMPLEMENTED with the verification results.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 23:29:12 +02:00
Levi Neuwirth 5cbcb1cf03
docs: frame the LSP file watcher fix (issue #233), and its lane
Framing and lane at the branch's first commit, so the document is
portable while it is reviewed. The GUI arc framing spent two review
rounds as an untracked file in one worktree; that is the lesson being
applied, not a preference.

Revision 1 is a DRAFT and no implementation may start from it.

Scope is D1 and D2, the two bug-shaped defects. Every claim in the
framing was read or executed against `0e4c58d` rather than carried from
the issue --- including re-running the tree's own `expand_braces` /
`glob_one_to_pattern` / `glob_matcher` under LuaJIT, which reproduces
the issue's glob table exactly, compiled patterns included.

#232 is NOT at fault and nothing about it should be reverted. The
activity indicator renders real in-flight jobs; what changed is
visibility, not behaviour, and the watcher has polled since 1c25730 in
May. Quieting the indicator would delete the instrument that found this.

Two findings the issue does not carry, both of which change the fix:

The existing test cannot discriminate this fix IN EITHER DIRECTION. The
fake LSP registers `**/*.txt`, which compiles to `^.-[^/]*%.txt$`, and
`.-` spans `/` --- so it matches relative and absolute subjects alike
and `m4_24` passes whether D1 is fixed or broken. The issue calls the
tested and exercised paths disjoint; the sharper statement is that the
one existing test is INSENSITIVE.

And "match the absolute path" alone would break `RelativePattern`:
measured, `*.txt` matches `a.txt` but not `/base/a.txt`.
`resolve_watcher` returns `(base, pattern)` and discards which form it
came from, so its CONTRACT has to change, not just the match subject. A
fix that misses this trades six broken rust-analyzer globs for every
RelativePattern that does not begin `**/`. The verification plan
includes a test that fails against exactly that wrong fix.

The framing also records what this lane does NOT fix, so the report is
not mistaken for closed: neither D1 nor D2 stops the walking, because
`walk` recurses unconditionally and `matches` gates only recording. The
modeline will keep flipping at roughly half the rate. Whether that meets
the acceptance bar is an open ruling for the user, stated as one rather
than assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 22:55:40 +02:00
Levi Neuwirth 4903c7cfb6
feat(git): adopt the destination capture --- P1a fixed, lane unblocked
The async completions rendered through whichever frontend was ambient
when git exited. Run `M-x git.status` in frontend A, let B become
active while `git status` runs, and A's panel opened in B. The
generation and the root were already captured at the keypress; the
frontend was the one input still read late.

Captured at invocation in all four entrances --- `git.status()`,
`_on_refresh`, `_deliver_root`'s hand-off, and the `git.diff-file`
command --- and threaded on the request table exactly as this module
already threads the generation and root. Committed under the profile
each surface actually takes: `"panel"` for `*git-status*`, `"document"`
for `*git-diff*`.

`set_status` moved INSIDE the status commit. Rows and message are now
computed first and emitted together, because a failure message
announcing a panel that the commit then refuses is the same misrouting
in its most confusing form.

A refused commit DROPS the render, which is the answer the
`expect_buffer` rule already gives when the panel a refresh belongs to
was killed. `commit_to` refuses before the body runs, so there is no
partial render to undo.

THE FIRST VERSION OF `g6_25` WAS WORTHLESS AND PASSED ITS OWN MUTATION.
`panel_text` finds `*git-status*` by NAME, which is global --- it
answers "does this buffer exist", not "which frontend is showing it"
--- so a render into the competitor satisfied it. Rewritten against
`side_window_for` and each view's active window, it now fails both
bites: removing the status commit grows a `*git-status*` panel in the
competing frontend; removing the diff commit hands it the document
window.

The merge also surfaced a cross-lane break invisible until the suites
ran: #232 made `purpose` required on `pmacs.process.spawn`, and this
module's spawn is on this branch, so it was never among the 11 sites
#232 updated. Each spawn now carries its own purpose and NOT the label
--- all three are labelled `git`, and only the purpose separates
resolving a repository from reading its status from diffing one file.

Existing tests that drive `_deliver_status` / `_deliver_diff` through
the internal seam now supply a captured destination. That includes the
one place it is load-bearing rather than uniform: `g6_23` reuses its
"stale" request table at the CURRENT ticket as its positive control,
which is the only one of those three deliveries that reaches a render.

Gates: 12/12 green --- git_status_stage1, destination_capture, journey
and worker_identity acceptance, plus the full sweep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 22:17:37 +02:00
Levi Neuwirth 4109ce6246
Merge main into git-status-stage1 --- the prerequisite has landed
Merged rather than rebased, for the reason #231 was: nineteen commits
replayed against a ledger four lanes had rewritten is nineteen chances
to lose a lane entry, and the reviewed commits keep their SHAs. One
file conflicted.

The conflict was diff3-style with an EMPTY base --- both sides added
lanes at the same offset --- so it resolved by keeping both, this
branch's #227 lane ahead of main's. Worth noting because an empty base
section is exactly what produced the stray `|||||||` markers found
committed in this file earlier today: the base half of a diff3 conflict
reads as content if it is resolved by hand in a hurry.

This unblocks the lane. `pmacs.window.capture_destination` now exists
on the branch (`src/lua_bindings/window_panel.rs:638`), which is what
#227's review found missing and what #231 was split out to provide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 21:53:55 +02:00
Levi Neuwirth 700037116f
docs: U9 --- the first red in this family with an in-run control
The merge gate for this lane failed at `11-sweep` on two selectors, and
both had already passed in `03-lib` and `04-lib-crdt` of the SAME gate
invocation, minutes earlier, on the same tree and machine. U6 and U7
could only ever compare a red run against a different run; this is the
first occurrence in the family where the control is inside the run, and
that is what the row is for.

Both are near misses against existing rows, and neither is folded in:

- The PTY failure carries U2's exact fragment, but U2's selector field
  names only the *raw* selector. U2's occurrence 2 had raw and canonical
  failing together; here canonical redded ALONE and raw passed, which
  U2's evidence has never shown.
- `composition_overhead_under_ten_percent` is one of U6's two selectors,
  and U6 instructs in its own text that one-without-the-other is a
  different incident. It redded without its pair, in a different step,
  at 1.613x against U6's 1.297x. Judged as instructed.

The row also records the first checkable candidate this family has had.
`cargo test --workspace` runs many test binaries concurrently while
`--lib` runs one, so the passing and failing steps differ in kind and
not merely in load average --- with a stated control that separates
load from concurrency. U6 and U7 both left the confound atmospheric
and unmeasured; this does not measure it either, but it names something
that can be.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 20:18:45 +02:00
Levi Neuwirth a1b931fa0e
Merge main into destination-capture, and correct the U4 row it turns on
Merged rather than rebased. Eighteen commits replayed against a ledger
that three other lanes had rewritten meant eighteen conflict
resolutions in `docs/active-work.md`, each one a chance to lose a lane
entry; merging resolves it once, against the state that actually ships,
and leaves the reviewed commits' SHAs intact. Only one file conflicted.

`docs/ci-red-signatures.md` auto-merged **without a conflict** — the
same silent path that produced duplicate U4/U5 ids when #232 rebased.
Verified by hand afterwards: ids U1-U8 are disjoint. They are out of
numeric order (U6/U7 sit ahead of U4/U5) and are left that way rather
than moved, since the note at the U6 row explains the history and
relocating sixty lines inside a merge commit hides real changes.

Three leftover conflict markers were sitting in `docs/active-work.md`
on `main`, committed by an earlier lane's resolution. `git diff --check`
flags them — but only for a working-tree diff, which is why the gate's
`diff-check` step never saw them and they survived several merges.
Removed here.

The U4 row is corrected on evidence this lane produced:

- **Flavour was wrong as a matching key.** The row was filed from
  #229's `lua54` red and put the flavour in the key; #231 reddened the
  identical selector with the identical three fragments twice on
  `luajit`. Matching as filed would have missed both.
- **A fourth sighting was a deliberate bite, not an occurrence** — the
  defect reintroduced on purpose during the test's own development. It
  is recorded for what it proves instead: the genuine defect and these
  CI reds are signature-indistinguishable, same message class and same
  full-timeout duration.
- **The control experiment is written down with its own bounds** — five
  green base observations against 0/2, 4.8% under an equal-rate model,
  and the two facts that bound it: attempt 5 reddened a different
  selector, and the branch side was never resampled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 20:05:07 +02:00
Levi Neuwirth 56e9a6442a
docs: U8 --- a third macOS selector, and I destroyed its fragments
Attempt 5 of the merge-base control at 0190102 failed on
acc28_child_input_and_the_c_c_escape_work_unchanged_in_a_panel, a
selector in no registry row. I then reran that job before reading its
log, and GitHub keeps only the latest attempt logs for a rerun job, so
the assertion text is gone. Recovery was attempted through the jobs API
and the attempt-scoped endpoint; it is not recoverable.

That leaves the row in U2 original condition --- a selector with no
fragments, unmatchable --- produced by exactly the mistake U3 is named
for. This is the fourth time this project has lost fragments this way,
and the first time I did it while holding the correction in my own
hands: I had corrected two other lanes for it earlier in the same
session.

Numbered U8, not U6, deliberately. U6 and U7 are reserved for the two
wall-clock rows on worker-identity-stage1, which renumbered into that
range when #229 took U4/U5. Taking U6 here would recreate the
duplicate-id collision that rebase already produced once, through the
same mechanism --- two lanes appending rows with no textual conflict.

The row is kept despite being unmatchable because of what it implies
together with U4 and U5: three distinct macOS selectors reddening in
one session points at a background failure rate on that platform rather
than three independent test bugs. That matters beyond bookkeeping,
because it undermines the equal-rate assumption behind any argument
about which branch a failure happened to land on --- including the one
currently being used to weigh #231.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 18:03:56 +02:00
Levi Neuwirth 6983496b74
docs: renumber this lane U4/U5 to U6/U7 --- and git did not warn
The pre-rebase warning was right, and the mechanism is worth recording
because it is the quiet kind.

gate-protocol-build landed its own U4 and U5 in #229. On this rebase
git merged docs/ci-red-signatures.md WITHOUT A CONFLICT --- the two
lanes appended their rows in different places, so there was nothing
textual to resolve --- and produced two ### U4 and two ### U5 headings
describing entirely different incidents. No marker, no complaint.

That is the failure the matching rule exists to prevent, arriving
through the one path a careful conflict resolution would never catch:
there was no conflict to resolve.

Renumbered across all four sites the warning enumerated: both headings,
the prose relation-to-U4 field inside what is now U7, and the
active-work.md reference. Ids verified unique afterwards rather than
assumed.

The warning block itself is retired in place, replaced by a note saying
what was done and why, so the next reader sees a completed action
rather than an outstanding one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:52:49 +02:00
Levi Neuwirth 0449d67a7f
docs: warn that this lane U4/U5 must become U6/U7 on rebase
gate-protocol-build independently defines its own U4 and U5 --- a macOS
lua54 PTY-resize failure and a Ctrl-C-as-SIGINT failure --- and it
merges first, so on main those ids are taken.

A rebase that resolves the textual conflict without renumbering leaves
two different incidents sharing an id, which is precisely the failure
this file matching rule exists to prevent. The registry authority rests
on ids meaning one thing.

The warning enumerates all four sites rather than saying "renumber the
rows", because one of them is a prose cross-reference inside U5
relation-to-U4 field and another is in active-work.md --- both easy to
miss when the conflict presenting itself is two adjacent headings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth 68c6d6732c
docs: U2 enumerates three occurrences, not two
The header and the mechanism boundary were corrected last round; the
TABLE still numbered two. It called the 2026-08-09 worker run
"Occurrence 2", omitted the 2026-08-06 CRDT occurrence from the
enumeration entirely, and concluded "Two occurrences establish
intermittence" --- in the row I had just rewritten because it omitted
that same occurrence.

That is the head-and-body split this session keeps reproducing, this
time inside a single table, in the row whose whole purpose is to be the
authoritative account of what is known.

The row now enumerates all three, and says which one carries the most
weight: the 2026-08-06 CRDT run, because it shows the failure is not
confined to one feature flavor and can take the raw and canonical
selectors at once. That is a fact neither of the other two supplies.

Also corrected: "what is NOT: any mechanism, still" is now "no
mechanism is ESTABLISHED", because one IS proposed --- read-before-
write on the child output, the R4/R6 readiness family. Proposed is not
confirmed, and the row says so rather than flattening the distinction
in either direction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth 71d262e08d
docs: U2 was already known, and my fragment claim overreached
Two corrections, both mine, both the same failure the row exists to
warn about.

First, this is at least the THIRD occurrence, not the second, and the
fragment was not newly captured. docs/active-work.md records a
2026-08-06 loaded --features crdt run failing this selector AND
m6_1_pty_canonical_mode_keeps_kernel_echo with the same stty -a output
was: "" --- and it already proposed a mechanism family, read-before-
write on the child output, the shape of R4 and R6. So the row claim
that no mechanism had been proposed was false of the tree it was
written in. The evidence was in this repository the whole time; I wrote
a registry row without reading the registry neighbour.

Second, the fragment does not show what I said it showed. The test
inspects collect_stdout(&evs) after drain_until --- what the SUPERVISOR
collected. It cannot distinguish stty never writing from the PTY
dropping the bytes from event collection missing them. I wrote "stty
produced no output at all", which asserts a mechanism the test cannot
see, in the same row that says no mechanism is established.

What survives is narrower and still worth having: this is not a termios
failure, since nothing observed shows echo configured wrongly. Which of
child-never-wrote, delivery-lost, collection-missed is open.

The control changes accordingly. Sampling the collected string more
times cannot separate those three however often it fails; the next
occurrence needs the full process event stream and the child exit
disposition captured, cross-checked against the R4/R6 readiness family
that the 2026-08-06 entry already implicates.

The gate conclusion is unaffected: a markdown-only delta cannot cause a
PTY failure, so worker identity code is excluded as a cause.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth 7675142d4f
docs: U2 has a second occurrence, and this time the fragments were read
The worker-identity tip gate went red on step 03-lib with one failure:
m6_1_pty_raw_mode_disables_kernel_echo. U2 already had that exact
selector but no fragments, so it could not be matched. Reading the
durable gate log rather than filtering a rerun supplies them.

The fragment reframes the failure. stty -a returned the EMPTY STRING,
not a wrong mode --- so this is not raw mode failing to disable echo,
it is stty producing no output at all, which points at PTY or spawn
readiness under load rather than termios handling. The assertion own
message is misleading on exactly that point, and anyone diagnosing it
from the message will look in the wrong place.

Occurrence 2 also EXCLUDES the change under test, which occurrence 1
could not. The tree carried zero code change since a 13/13 green run on
this same lane --- the only delta was three lines of markdown. A docs
edit cannot break a PTY test, so the diff is ruled out as a cause
rather than merely doubted. Isolated rerun passes in 0.01s.

Still no mechanism, and the row says so. Two occurrences establish
intermittence and a load correlation; neither establishes cause. The
row now names the discriminating control for a third: loop the selector
under synthetic load logging stty output every iteration, since whether
stty is empty EVERY time it fails is what separates a readiness race
from a termios one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth 80b3dcf897
docs: the suite is 25, not 26
The previous round left the suite at 24 and this round adds one test,
so it is 25. grep -c on the test attribute confirms 25 and the run
reports 25/25. The bullet said 26, and I repeated it upstream without
counting.

The entry now shows the arithmetic --- 24 before, plus one --- rather
than just a corrected number, and names the earlier figure so a reader
who saw it does not treat this as a second suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth 3570e1ad42
docs: the third gate run is green, and U5 says so
The two reds recorded a moment ago were followed by a full green run of
the same command on the same tree — all 13 steps, log
`20260809T200907Z-2672209`. Both the lane entry and U5 now carry that,
because a signature row that records only the reds overstates them: the
green rerun is part of the evidence, not a reason to delete the row.

The row stays live and stays U-classified. Three load-sensitive render
budgets going red one per run and then green is consistent with a loaded
machine and with nothing else in hand; it is not a measurement of one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth 1a00d8130d
docs: record review round 3, and a sweep red that moves each run
The lane entry gains round 3: the wrong-surface diagnostic, why the job
and process refusals now say different things, the anti-collapse test
and its three mutation checks. Written here rather than left in the
commit message because this file is what a recovering agent reads.

`docs/ci-red-signatures.md` gains **U5**. Two consecutive `scripts/gate`
runs of the same command, on the same tree, red on step `12-sweep` with
a DIFFERENT wall-clock render-budget test each time — 224ms and 258ms
against a 200ms budget, 114ms against a 100ms budget, at load average
12.9/23.9 with sibling worktrees building. Each passes in an isolated
rerun of its own selector, and no selector reds twice.

The rotating selector is the signature, and it is a stronger one than
any single test name: a regression that moved between three unrelated
render paths on an unchanged tree is far less likely than one loaded
machine. The observing diff is two string literals, their doc comments
and one test, and touches no render path at all.

Kept separate from U4 rather than merged. U4 is two budget tests in
`04-lib-crdt` failing TOGETHER; this is three render-budget tests in
`12-sweep` failing ONE PER RUN. Merging them would assert a shared
mechanism nothing in hand shows, and the load confound stays unmeasured
in both — a rival explanation, not a finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth 028016333c
docs: record review round 2, and R7's control finally discriminated
Two files, no code.

## `docs/active-work.md` — review round 2

The lane's volatile block gains the round-2 record: the three findings,
why P2a's fix is a mapped diagnostic and P2b's is escaping at
presentation rather than rejection at the registry, and the seven new
mutation checks. The gate outcome is recorded with its step counts and
the two stop-signal facts — `journey_acceptance` 47/47 and all three
`#pmacs.process.list()` leak detectors byte-identical to `main`.

It also records what P2a's audit found and did NOT fix:
`pmacs.process.spawn`'s other string fields still convert generically.
That is pre-existing and out of this lane's diff, and it is named so it
is not silently inherited by whoever reads the fixed `purpose` read and
assumes the rest matches.

## `docs/ci-red-signatures.md` — R7's third occurrence, and U4

**R7 reproduced, and the control the second-occurrence note prescribed
finally discriminated — against its own hypothesis.**

Occurrence 2 left exactly one causal path open: the observing lane had
added a GPU-heavy `render_offscreen` test to the same binary, and
contention with a one-second socket handshake was plausible. That note
prescribed the control to run if a third occurrence landed — with the
added test removed, not at the merge base. A third occurrence landed, at
the gate's `gpu` step, with all three fragments verified against the
durable log.

The control was run. **Ten full `-p pmacs-gpu` runs with the added test:
10/10 green. Ten with it `#[ignore]`d, nothing else changed: 1 failure
in 10, all three fragments present.** Removing the suspect made the
failure more frequent, so the concurrent-test path is excluded — no
contention story from that test survives that direction.

The more useful result is the rate. This is the first rerun in R7's
history to reproduce anything at all, and it puts the failure at roughly
1-in-10 under ordinary `-p pmacs-gpu` load. Three sightings were not
enough to bisect a handshake; 1-in-10 is. The row now says so, and tells
the next agent to instrument which side closes the pipe rather than
re-run for green.

The lane is still not attributed — now for a measured reason rather than
an argument from diff shape: the arm without the lane's only
`pmacs-gpu` addition is the arm that went red.

**U4** records the other two reds from that same gate run:
`criterion_1_end_of_line_typing_completes_sub_frame_per_keystroke` and
`composition_overhead_under_ten_percent`, both wall-clock budget
assertions, failing together in `04-lib-crdt` and both green in
isolation and in the next full run. Fragments captured, so unlike U1–U3
it is matchable — it is a `U` row for want of a mechanism, not for want
of evidence. The signature named is **the pair**: two budget tests
failing in one run and neither in the next is far more likely to be one
loaded machine than two simultaneous regressions, and a future run that
reds only one of them is a different incident.

Neither row claims harmlessness, and the concurrent-worktree load
confound is recorded as a rival explanation rather than as a finding,
because it was not measured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth 70262888b4
fix(workers): a safe display-text boundary for purpose and handler names
Review round 2, findings P2a and P2b, plus P3's stale recovery
summaries. Three defects, and the fix is deliberately different in each
place because the constraint is.

## P2a — invalid UTF-8 bypassed the `purpose` diagnostic

`required_purpose` read the field with `value.to_str()?`. Lua strings
are BYTE strings, so `purpose = string.char(255)` is a value a caller
can write, and `?` surfaced mlua's generic conversion error BEFORE this
lane's own diagnostic was ever constructed: the caller was told neither
the field nor the rule.

**This is the third time this project has hit the class** — an unowned
Lua string converted with `?` ahead of the owned message; the
destination-capture lane corrected the same shape two rounds ago. It
refused before spawning and nothing leaked, so the defect was the
message, not the outcome. The conversion failure is now mapped onto
this function's own message, and the new acceptance row asserts on
message CONTENT so retyping the read as a bare `?` breaks the test
rather than silently degrading the error.

Auditing the rest of the lane's diff for the same class turned up
exactly one more: `_push_dispatch_name` took `name: String`, so a
registered handler name that was not valid UTF-8 failed at first
dispatch with mlua's generic message. It now takes `mlua::String` and
maps that failure onto an owned diagnostic naming the argument and the
rule. Those are the only two Lua-string reads this lane added; every
other binding it adds takes `()`.

## P2b — no safe display-text boundary. Two halves, two different fixes

### Handler names are refused at the source

`pmacs.workers.register` type-checked its name and nothing more, which
was defensible while the name died inside `dispatch`. It no longer dies
there: the ambient carries it into every job the handler allocates and
composes it into `purpose`, which `*workers*` and the modeline both
render. So it now gets `purpose`'s meaningful-value standard —
non-empty, not whitespace-only — plus control characters, which have no
legitimate place in a registered identifier.

### Purposes are ESCAPED at presentation, not rejected at the registry

A purpose may legitimately contain a newline: a filesystem path can, and
`pmacs-magit`'s spawn purpose is a whole argv. **This is the shape of
the `#228` decision, and it is consistent with it** — the one-line
constraint belongs to the surface that has it, not to the registry that
does not. There, `Command.description` stays free-form and the two
single-row consumers clip with `description_first_line`. Here the
equivalent is escaping rather than clipping, because a purpose's later
words are load-bearing: an argv's second word says which file, and a
clip would drop it silently.

`purpose_for_one_row` states the property it exists for: **a row must
not be able to forge another row.** It escapes `\n`, `\r`, `\t` and the
rest of the Unicode `Cc` class (which covers ESC, so a purpose cannot
open a terminal escape sequence either), borrows unchanged when there is
nothing to escape — making byte-identity structural rather than
asserted — and deliberately does NOT escape backslashes: no number of
them produces a second row, and doubling them would cost byte-identity
for ordinary text.

Two surfaces call it: the `*workers*` rows, and `ActivitySummary`, which
exists for one consumer that has exactly one row.
`pmacs.workers.snapshot()` is this lane's `describe-command` and stays
raw, which is what makes this a rendering decision rather than data
loss — asserted, not assumed.

## P3 — two stale recovery summaries

`docs/worker-identity-framing.md` still said "Implementation may
proceed"; it is implemented. `docs/active-work.md` still said Stage 1
takes the "first two" of owner/purpose/parent — `owner` was REMOVED in
revision 2, so it takes one of the three, and the claim the whole
`owner` argument overturned was still standing in the volatile state of
record. Both fixed section-locally.

## Verification

`tests/worker_identity_acceptance.rs`, 18 -> 24 tests:

* invalid-UTF-8 purpose refused by THIS lane's message, asserted on
  content, alongside the absent / empty / whitespace / wrong-type /
  metatable rows;
* a whitespace-only handler name and a control-character one are each
  refused AT `register`, asserted on the error and on the handler not
  being installed (dispatch reports `unknown handler`);
* a non-UTF-8 handler name is refused before the handler runs, with the
  dispatch-name stack left empty;
* a purpose containing a newline renders as ONE row in `*workers*` and
  as one line in the modeline — through the real rendering path, the
  latter through a painted frame as well as the evaluator;
* **a purpose crafted to look like a row boundary does not produce a
  second row** — asserted by counting rows, with the escaped text
  asserted present so a renderer that dropped the purpose entirely could
  not pass;
* a purpose with no control characters is byte-identical on both
  surfaces, fixtured with a literal backslash, a literal `\v`, quotes
  and a non-ASCII character.

Mutation-checked, seven guards, each failing its own test and no other:
the purpose UTF-8 diagnostic; the `_push_dispatch_name` one; the
register whitespace guard; the register control-character guard; the
`*workers*` call site; the `ActivitySummary` call site; and
`purpose_for_one_row` itself neutered to the identity, which fails both
surfaces' tests and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth 37a81227c7
docs: R7 has a second occurrence, and this time the fragments were captured
`attach::tests::managed_retry_survives_transients_and_uses_the_successful_stream`
failed once at this lane's `scripts/gate` **`gpu` step** on 2026-08-09.
Judged against this file rather than rerun-and-shrugged.

**It matches R7 on all three of its required fragments**, verified rather
than inferred:

    transient sequence must attach: Attach(Handshake(Io(Os {
      code: 32, kind: BrokenPipe, message: "Broken pipe" })))

**That capture is the point.** U2 and U3 both record the identical loss —
"output was filtered to the `FAILED` line" — and U3 says outright that
the recurring mistake was its author's, twice, with a mechanical fix:
read the durable log, never the live stream. The gate writes
`NN-gpu.log` for exactly this, and reading it turned what would have been
a third unjudgeable `U` note into a second occurrence of a row that had
one.

The flavor is a third one (`PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`,
neither occurrence 1's `--features crdt` sweep nor U3's default-features
workspace sweep). Recorded because this file's own R2 worked example
treats flavor as outside matching.

**The merge-base control R7 asked for was run, and it settles nothing.**
15 runs at `4bc55e8`, green — but the observing branch was green over 30
runs too (15 isolated selector, 15 full suite), so neither side
reproduced and the comparison separates nothing. Logged as a null result,
not as exculpation. Per the rerun rule, all 45 green runs establish
**intermittence only**.

**And one causal path is named rather than dismissed:** this lane adds a
GPU-heavy `render_offscreen` test to `pmacs-gpu`'s test module. It
touches no `attach.rs`, no protocol and no wire — but it does add a
concurrent test to the same binary, and the failing test is a socket
handshake on a one-second deadline. Contention is a plausible
`BrokenPipe` mechanism and 30 green runs do not exclude it. The row now
says what the discriminating control would be if there is a third
occurrence: remove the added test, not go to the merge base.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth d4a69cae14
docs: record review round 1, and name the statusline adopter that is not named after its file
Two section-local edits, no reflowing.

**`docs/worker-identity-framing.md` §2** named the three existing
statusline adopters by FILE — `terminal.lua`, `syntax.lua`, `lsp.lua` —
which is accurate and misleading together: `syntax.lua` registers its
provider under the name **`"mode"`**, so the registry inventory reads
`["mode", "terminal", "lsp"]` and a reader looking for the syntax adopter
by name does not find one. That is what made this lane's change to
`tests/statusline_segments_acceptance.rs` surprising, and the next reader
should not have to rediscover it. Also records that where a fourth
registration sorts is decided by **load order**, not by name.

**`docs/active-work.md`'s lane block** records review round 1: the
`pmacs.process.spawn` blocker and its fix at `2162737`, the five refused
shapes, the eleven updated call sites, the two audit-fixture occurrences
that are deliberately untouched, the three added mutation checks, and the
two acceptance suites the round added to the gate line
(`compile_mode_acceptance` and `m8_6_acceptance`, because the round moved
their spawn call sites and `m8_6` covers the `pmacs-magit` package
fixture).

It also records the breaking-change decision with the reasoning that
justified it, rather than only the outcome: the binding has no
API-reference documentation and no stability promise, `lua_to_spec` has
one caller, and §10/P7 put the third-party population at ~zero — so the
cost of requiring the field is at its minimum now and rises from here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth 1ca76e055b
docs: the worker-identity lane is implemented, with its gate outcome
Section-local update to `docs/active-work.md`'s "Worker identity Stage 1
(§9)" block, which was written pre-implementation with the lane's first
commit. It now records what actually shipped rather than what was
planned, because the difference is where the reviewable claims are.

Three things it records that a status flip alone would not:

- **`journey_acceptance` passed UNTOUCHED (47/47).** Q#W-7 edits the
  `commit_to` guard family, so that suite was the lane's stop signal: any
  established pin needing an edit would have meant the change altered
  Journey Stage 1a's semantics rather than closing a gap in them. The
  same for all three `#pmacs.process.list()` leak detectors, which are
  Q#W-4's preservation claim.
- **One pre-existing assertion did change**, and it is named here so the
  change is not mistaken for an accommodation: the builtin statusline
  provider inventory in `statusline_segments_acceptance` grows by the
  fourth adopter. That assertion exists to grow.
- **Two residuals, stated rather than tested around.** A raw
  `coroutine.yield` inside either dynamic scope still leaks the scope,
  and Q#W-7's reachability by a real caller is unproven.

Also lists the surfaces that changed shape — the collapsed allocation
funnel, the two grown constructor signatures, the new required fields —
for anyone rebasing a concurrent lane onto this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth 26a26006bb
docs: worker identity framing revision 4 --- scope rule 1, take Q#W-7
Two changes, both from review, both about claiming exactly what is
enforced and no more.

Rule 1 said it covered "all yield points". It covers the two supported
pmacs yield APIs. Raw coroutine.yield remains reachable: R46 is a
convention ("package code uses :await() rather than coroutine.yield",
async.lua:26-27), not an enforcement, and the scheduler diagnoses a
non-Handle yield only after the fact --- step() resumes at :197 and
inspects what came back at :212, by which point the coroutine has
already suspended and the enclosing dispatch never returns to run its
pop. No refusal sited in a yield helper can intercept that.

So the property is stated as what it is: the supported ways to yield
are refused inside the scope, and an R46 violation can still leak the
name --- loudly, through pmacs.error into *errors*, but unrestored.
Section 6 says explicitly that this is NOT asserted, because a test
implying coverage the design lacks is worse than the recorded gap.

Q#W-7 is approved into this lane rather than split out. Same supported
helper, same invariant, same async.lua edit family; splitting would
preserve a known hole without reducing integration risk. So
yield_to_next_tick gains both refusals --- the new
_in_dispatch_name_scope and the missing _in_commit_scope --- and the
commit_to gap closes in the same commit as rule 1. Its witnesses are
the same pair as rule 1: the refusal fires AND the scope restores, on
the reasoning that a guard which raises while leaving the scope pushed
trades a silent fault for a loud one and fixes neither.

Reachability by a real caller stays UNPROVEN and the framing says so in
three places, including here. The defect was found by reading; the
tests pin the guard rather than reproducing a user-visible bug. Nobody
should later cite this as evidence the bug was observed.

The causal-extent paragraph and the Q#W-5 comparison were both
re-scoped to match, since both leaned on "non-yieldable" as an
unqualified property.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth 7730f87bba
docs: worker identity framing revision 3 --- the ambient must not yield
Review found that revision 2 asserted the property its whole design
rested on. It called the dispatch-name extent "synchronous" and never
checked. A registered handler is arbitrary Lua running inside
pmacs.async and may call Handle:await(), which parks the coroutine with
the name still pushed --- so every tick callback and every other
coroutine allocating a job in the meantime inherits it. The existing
tests already await inside pcall, so this is the ordinary shape of an
awaiting handler, not a corner case.

Rule 1 now enforces non-yieldability instead of assuming it, and the
enforcement was already in this file: Handle:await refuses to run
inside pmacs.window.commit_to (async.lua:87-90), with a comment giving
exactly this reasoning --- yielding "would restore the scope while this
coroutine is still parked". _in_dispatch_name_scope joins
_in_commit_scope in the same place.

Three details decide whether the guard holds, and all three are pinned:
it rejects before the park, not after; it rejects unconditionally
rather than only when the handle is incomplete, because a guard keyed
on whether the job happened to finish first passes under test and fails
intermittently in production; and it covers both yield points.

That last one is a finding. pmacs.async.yield_to_next_tick
(async.lua:243-245) also yields and is public. Guarding only await
would have left the hole open through a second door.

Which exposes Q#W-7: the existing commit_to guard has exactly that gap
today. yield_to_next_tick carries no _in_commit_scope refusal, so
Journey Stage 1a Q#JR14b invariant has a second entrance. Reported
rather than patched, and reachability by a real caller is explicitly
UNPROVEN --- it is a code reading, not a repro. My vote is to fix it in
this lane since the lane already edits that function family, but it is
another lane invariant so it is a question.

Q#W-5 justification is rewritten rather than left standing. It argued
parent was deferrable because its ambient would span asynchronous
lifetimes while this one did not --- an argument revision 2 was not
entitled to make, since its own ambient could be parked by any awaiting
handler. With rule 1 the distinction is real: this extent cannot be
suspended, and parent cannot be rescued by refusing to yield because
yielding is the mechanism it needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth e1ca382ea5
docs: worker identity framing revision 2 --- drop owner, specify the name path
Two review blockers, both correct, both about the same failure: a field
or a claim that looks like attribution without being it.

BLOCKER 1 --- owner is not honest before P3. Revision 1 proposed
owner = package-or-builtin while populating it from static
per-subsystem constants at each dispatcher. Those disagree. A generic
dispatcher has no trustworthy knowledge of who invoked it, and
pmacs.process.spawn is callable by any package, so a static "lsp" label
is an origin or category and would confidently misattribute
third-party work to a builtin at exactly the point section 9 wants
attribution.

owner is removed rather than renamed. origin or subsystem would be
honest wording, but a second string field beside purpose, used to group
the view, gets adopted as ownership by the next reader regardless of
its name --- and it would squat on the slot P3 has to fill. Stage 2
needs a grouping key and should get a real one. No P3 alignment is
claimed any more.

BLOCKER 2 --- the handler name needs a mechanism, not a parameter.
Revision 1 said the name was "in hand at the one place that throws it
away". That was wrong about the call chain, and re-reading it is what
showed why: dispatch(name) calls an arbitrary handler, which calls a
Lua wrapper, which calls the Rust binding, and name is a parameter of
none of them. Worse, async.lua:337-345 documents the wrapper layer as
bypassable --- other runtime files are told to call their own raw
_dispatch_* primitives --- so capturing in the wrappers would miss
exactly the callers attribution exists for.

Q#W-2 is rewritten as a contract: a dispatch-name stack owned by the
async runtime and read at allocate, the same single funnel Q#W-1
collapses. Seven rules, including the two that decide whether it is
better than nothing --- unwind-safe popping, because one erroring
handler would otherwise poison every later dispatch with a stale name,
and composition rather than replacement of a caller-supplied purpose,
because replacing recreates blocker 1 in a new place.

It also answers the objection it invites: why is this ambient allowed
when Q#W-5 defers parent for needing one. Because they are different
mechanisms --- this is a synchronous single-threaded extent with a
deterministic pop, and parent needs a lifetime model spanning ticks and
post-settlement callbacks.

Verification takes the reviewer wording fix: presence is a type
obligation now that purpose is non-optional in a private JobSpec, so
the compiler proves it and the tests prove semantics at representative
entry paths. The handler witness must be a registered handler calling a
real dispatcher, not a synthetic funnel test.

The title changed too: "who asked for it" overclaimed once owner left.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth dda90a2c37
docs: frame worker identity Stage 1 (revision 1)
COHERENCE.md section 0 names background-work ownership as one of two
remaining thin ends of the golden journey, and section 20 puts it
outside Priority 1 while conceding it is the last of that priority own
work. Section 9 grades the worker model "mechanism without identity".

The felt gap is narrower than the arc and is checkable: grep -c for
spinner/progress/busy in src/statusline.rs returns 0, so section 3
promise of "visible asynchronous work" is false today unless the user
knows to run M-x editor.list-workers. The git Stage 1 lane in flight
right now records a deliberate negative section 9 impact for exactly
this reason; this lane is the one that repays it.

Two scouting findings shaped the staging rather than confirmed it.

PendingJob carries eight fields, not the seven the audit lists, and the
eighth doc comment cites section 9 by name as the reason identity
belongs on the job and not in a side map. So this extends a merged
decision instead of introducing one.

pmacs.process.list filters to LineOriented, dating to the vterm Stage 1
commit, and three acceptance suites use #pmacs.process.list() as a leak
detector. Widening that accessor to show terminal PTYs would inflate
all three baselines. Making PTYs visible therefore moves to Stage 2
behind a separate accessor, which is a better answer than editing tests
that are correctly detecting a semantic change.

NO WIRE CHANGE, and that is load-bearing for scheduling: discovery
Stage 2 holds the v22-to-v23 bump slot and git Stage 2 is queued behind
it. The activity indicator is a fourth pmacs.statusline.register
provider on the existing StatuslineSegments vector.

The framing also flags a deliberate deviation from the audit rather
than quietly taking it: section 9 names owner/purpose/parent together,
and Stage 1 takes only the first two, because an unpopulated parent
field reads as "no parent" rather than "not tracked".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:51:54 +02:00
Levi Neuwirth 4654b940ff
docs: carry the four-plus-one dedication count into the framing
fb3974b corrected the ledger --- "eight writes exist; five are
reachable" listed four, the fifth being quit_window's
QuitAction::Restore, proved unreachable and guarded anyway --- but the
framing kept the old count, and the framing is the artifact that
outlives the ledger.

Swept rather than patched at the two known lines. Every count claim
about dedication routes, sites and writes now agrees with the §3 table,
in the ledger's phrasing: four are reachable, a fifth is guarded
defensively, and ALL FIVE ARE GUARDED --- the last being the count the
safety argument actually runs on.

* The section heading said "FIVE WRITES REACH DEDICATION". It now says
  four reach it and a fifth is guarded defensively, and the "found
  three more" arithmetic is spelled out (two further apply_placement
  arms plus the unreachable quit_window site) so the total is legible
  as five GUARDED rather than five reachable.
* "all five reachable sites were momentarily unguarded together"
  (revision 8's masked contract) --- true of all five GUARDED sites,
  which is what that sentence means; the four reachable ones and the
  defensive fifth are now named there.
* "Two live guards, five reachable sites" --- two live guards cover the
  four reachable sites; site 7 carries a third, defensive guard. There
  really are three call sites of panel_commit_dedication_refusal
  (editor_core.rs display_buffer and quit_window,
  lua_bindings/window_panel.rs set_params), so the old sentence
  undercounted guards while overcounting reachability.
* Two "every site in it is still guarded" claims were literally false
  of sites 4, 5 and 8 (two harmless Ordinary arms and a unit test).
  Narrowed to every site that can dedicate the slot.
* Table row 7's verdict now carries "guarded anyway, defensively", so
  the four-plus-one reads off the table itself.

The old count is preserved as history and marked as such --- "not the
current count" --- with the correcting SHA, so a reader who saw the
earlier text knows which way the correction ran.

The miscount had NOT propagated. Repo-wide grep for the phrasing finds
it only here: DEDICATION_ROUTES in
tests/destination_capture_acceptance.rs is a [_; 4] and its doc comment
already said "four and not two"; the framing's own acceptance bullet
already said "which is four and not two"; the ledger was fixed in
fb3974b. No src/ or tests/ comment claims five reachable routes. (The
suite's unrelated "five distinct refusals" of commit_to is a different
count and is correct.)

Documentation only. Gate run twice with --acceptance
destination_capture_acceptance: fmt, clippy, lib-crdt, the destination
capture suite, m4 and gpu green both times; diff-check clean. Each run
had one wall-clock RATIO test fail under load from concurrent gates in
sibling worktrees --- m8_2's 10K-entry render (457ms vs a 200ms budget)
on the first, editor's composition_overhead_under_ten_percent (1.169)
on the second --- a different test each time, and each passes in
isolation on this tree (0.19s and ok respectively). Neither is
reachable from a markdown edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth 3b8e426f90
test(window): pin the cross-frontend exception, and fix two ledger counts
panel_commit_dedication_refusal matches on `fid` as well as on the
profile: a nested commit for a DIFFERENT frontend may dedicate that
frontend's own side slot, because resolve_placement consults only the
requesting frontend's panel_capable and its own one side window, so
nothing done to B can change where A's side request lands.

That promise was documented and unpinned. Both revision 9 nesting
tests drive a single frontend, so the comparison is trivially true
throughout them: deleting it, and making any outer "panel" contract
globally restrictive, passed the whole file.

a_nested_commit_for_another_frontend_may_dedicate_its_own_slot runs
two frontends. While an outer "panel" commit for A is in force, a
nested commit for B dedicates B's slot and is ALLOWED --- and B's
slot is asserted really dedicated afterwards, not merely unrefused.
The far side runs in the same test: A's slot stays undedicated and
A's result still lands in A's panel, so the row cannot pass by having
weakened the restriction generally.

This is the suite's only POSITIVE row; every other asserts a refusal,
which is the shape it was thinnest on. An exception only the doc
comment knows about is one review round from being simplified out.

Mutation-checked: deleting `&& contract.destination.frontend == fid`
fails ONLY this test. Both single-frontend nesting tests pass under
it, which is the evidence they are independent of the frontend match
rather than merely looking so. journey_acceptance (47),
dired_acceptance (31) and cargo test --lib (1920) stay green.

Two ledger corrections, both section-local:

* "Eight writes exist; five are reachable" then listed four. The
  fifth is quit_window's QuitAction::Restore --- the site proved
  unreachable and guarded anyway. It now appears in the list that
  justifies it, and the bullet counts what actually matters: all five
  are guarded.
* The revision 9 mutation paragraph had the preservation counts
  REVERSED (journey 31 / dired 47). It is journey 47 / dired 31,
  matching the bullet further up and measured per target. The same
  reversal is in 394fa43's commit message; that is left as written
  rather than rewriting a pushed commit, and the ledger now says so
  where the numbers are, so a reader following the SHA takes the
  corrected pair.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth 5f3f38dfd7
fix(window): keep a panel commit's restriction across nested scopes
Revision 8 refuses, inside a "panel" commit_to, the mutations that
would make its relaxed preflight wrong. A nested commit_to REPLACED
the enclosing contract with its own and restored it afterwards, so
the outer restriction went out of force for the whole inner body:

  commit_to(outer, function()                   -- "panel", relaxed preflight
    commit_to(inner, function()                 -- "document", MASKS the outer
      set_params(panel(), { dedicated = true }) -- ...and succeeds
    end)
    display(result, { side = "bottom" })        -- ...which now FALLS BACK
  end, "panel")

Every step is legal on its own, and the outer commit then overwrote
a newer document buffer --- the P1a failure the lane exists to
remove, reached through one extra call.

What this invalidated, precisely: NOT the enumeration of dedication
write sites. Every site in it is real and still guarded. What was
wrong was the claim that the guard was in force for the whole outer
body. So the enumeration is inherited and qualified, not redone.

Contracts now COMPOSE rather than replace. The core holds a stack;
ScopedFrontendGuard pushes on entry and truncates back to its own
depth on every exit path; panel_commit_dedication_refusal consults
every contract in force rather than the innermost. The strictest
active restriction wins. Matching stays per frontend --- a nested
commit for a different frontend may dedicate its own side slot,
which cannot change where this frontend's side request lands.

Nesting itself is NOT forbidden, which was the other candidate fix.
It closes the hole by prohibiting a construction no rule objects to:
commit_to is public Lua API for saying where a continuation's result
belongs, and a body committing to a second destination (a diff
beside a status panel) is where #227's adoption is heading. Only the
restriction needed preserving. Detecting the dedication when the
outer commit resumed was not available either --- that is a late
refusal, which is what revision 7 was rejected for.

Two pins, and they are a pair rather than one test written twice:

* a_nested_commit_cannot_mask_an_outer_panel_restriction drives the
  same four write-site rows through a nested, entirely valid
  "document" commit, and asserts the attempt is refused, the slot is
  still undedicated, and the outer commit's destination is intact.
* an_ordinary_nested_commit_still_runs_and_restores_the_outer_restriction
  pins that nesting without dedication is accepted, that the
  enclosing restriction is back in force once the nested commit
  returns, and that outside every commit dedication is ordinary
  again.

Mutation-checked: restoring the guard to the innermost contract
(.last(), exactly revision 8's swapped slot) fails only the first of
those. The other 13 pins, journey_acceptance (31), dired_acceptance
(47) and cargo test --lib (1920) all stay green. The ordinary-nesting
pin deliberately survives it --- it exists to fail the other
candidate fix.

Also sweeps the comments left by revision 7, which revision 8
superseded: no fallback_commit_refusal symbol remains, but six doc
sites still described placement-boundary enforcement as the
guarantee (ViewDestination, CommitProfile::Panel, CommitContract,
capture_view_destination, commit_destination_refusal,
panel_placement_can_fall_back), plus two comment blocks in the
commit_to binding and one stale mutation note in the acceptance
suite. Net rustdoc warnings down three.

Framing to revision 9; the active-work lane entry updated in place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth 2fc2985029
fix(window): refuse the mutation that would invalidate a panel commit
Revision 8 of `docs/destination-capture-framing.md`, replacing the
revision-7 design at `ca72461`, plus the invalid-UTF-8 profile hole.
The framing now carries §3's enumeration, performed.

THE BLOCKER, unchanged. The `"panel"` commit profile skips preflight
checks 2-4 on the claim that a panel result never touches a document
window. Panel placement FALLS BACK into an ordinary document window
when the frontend is not `panel_capable` or its one side slot is
dedicated elsewhere, and installs the result there --- so a `"panel"`
commit could replace a NEWER document with every stale-intent guard
skipped.

TWO REJECTED SHAPES, kept in the framing as the record of why not
those. Revision 6 predicted the fallback at preflight and argued the
body could not change it; false, because refusing `await` stops another
COROUTINE interleaving, not the body's own synchronous statements.
Revision 7 (`ca72461`) moved enforcement to the placement boundary;
that breaks the invariant `commit_to` exists for --- `docs/agent-handoff.md`
requires it to preflight BEFORE the callback, because a body creates
buffers, handles and paint long before it asks to display anything, so
"validating at display time is four mutations too late". A refusal
arriving after all of that is a partial commit with an error return.

REVISION 8 DOES NEITHER. The preflight stays exactly where it was, and
the mutations that would invalidate it are REFUSED AT THE ATTEMPT ---
the same shape as `Handle:await` being refused inside a commit scope,
for the identical reason: something that would invalidate the scope's
guarantee is rejected outright rather than predicted around or caught
late. With them refused, the fallback never comes into existence.

THE ENUMERATION, PERFORMED --- this is the load-bearing part, and it is
closed for a structural reason rather than because inspection ran out
of ideas. Full working in the framing §3.

`resolve_placement` reaches `Ordinary` from a side request through
exactly two branches, so only two pieces of state are levers at all:
`panel_capable`, and the one side window's `dedicated`.

`panel_capable` is UNREACHABLE from a body: written only where a
`FrontendView` is constructed, and nothing in `src/lua_bindings/`
constructs, registers or unregisters one --- `register_frontend_view`
has callers only in `daemon.rs` and core unit tests.

`dedicated` has eight writes. Five are reachable: `apply_placement`'s
`Side` created, replacing and non-replacing arms, and `set_params`. Two
`Ordinary` arms are harmless --- every `Ordinary` target is filtered
`!is_side`, and one only ever clears the flag. One is a unit test.

Closing the side window is NOT a route, checked rather than assumed:
with no side leaf `side_window_for` returns `None` and placement
CREATES a fresh panel instead of falling back. `panel_hidden` is not
consulted by placement, and `params.side` is unreachable.

`quit_window`'s `QuitAction::Restore { dedicated: true }` is
UNREACHABLE, and this was the surprise --- it looked like a route with
no `dedicated` argument at the call site at all. `Restore` is stored
only on a REPLACING side placement, and a dedicated slot can never be
the target of one: a side request with a different buffer falls through
to `Ordinary`, and an exact-target request is refused by
`window_accepts_buffer`. Guarded anyway, labelled defensive, because
its unreachability is emergent from two rules in another function.

GUARDS SITED WHERE THE PROPERTY CONVERGES. All three `Side` arms are
reached through `apply_placement`, which has EXACTLY ONE caller --- so
one guard in `display_buffer` covers every request-driven dedication,
including spellings that do not exist yet. `set_params` is a genuinely
separate write and is guarded separately; dedication does NOT converge
before the field itself, and that is stated rather than papered over.
`Window::params.dedicated` is a public field, so the compiler does not
enforce the funnel --- the acceptance rows are what would catch a new
direct writer.

WHAT IS DELIBERATELY NOT REFUSED. The document profile is untouched:
constraining its body would newly refuse dired's own documented panel
path, a preservation-suite stop signal. Dedicating a DOCUMENT window is
still allowed, since it cannot change which of panel-or-document a side
request resolves to. And falling back is still allowed --- a frontend
that cannot render a panel degrades gracefully exactly as today,
because this refuses the mutation that MANUFACTURES a fallback, never
the fallback itself.

THE SECOND HOLE. `commit_profile` did `name.to_str()?`, but Lua strings
are BYTE strings, so `string.char(255)` hit mlua's generic UTF-8 error
before `BAD_COMMIT_PROFILE` was constructed --- the same reachability
class as the `Option<String>` defect revision 5 fixed, one layer down.
Bytes now, with the row asserting on message content.

TESTS: 12 pins. The inside-the-body test is ONE ROW PER REACHABLE WRITE
SITE, not per call spelling, because one spelling reaches three
different writes: `set_params`, and `display{side, dedicated}` in each
of the created, replacing and non-replacing arms. Each asserts the
three things revision 8 requires --- the dedication call is refused, the
slot is still undedicated afterwards, and nothing partial was installed
(no `*result*` buffer, panel unchanged, document unchanged).

Mutation-checked per guard: deleting the `display_buffer` guard fails
all three display rows, verified INDIVIDUALLY by rotating each to the
front so the first failure cannot mask the rest; deleting the
`set_params` guard fails only that row.

THREE FRAMING CORRECTIONS ride along, all of them cases of the document
teaching something it later argues against. Section 3 stated the
disproved premise unconditionally --- "the panel case would inherit a
check about a window it never touches" --- a hundred lines before
correcting it, so a reader met the wrong claim first; it is now
qualified at the point of the claim, and section 2 carried the same
unconditional form one section earlier ("it lands in the bottom panel")
and now says it REQUESTS one. The handoff citation was written "section
748" twice when it is LINE 748, and this document's authority is that
its citations can be followed. And the "not asserted exhaustive" hedge
on the route list is retired: the enumeration is closed structurally,
because `resolve_placement` reaches `Ordinary` from a side request
through exactly two branches.

`journey_acceptance` (47) and `dired_acceptance` (31) pass UNCHANGED.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth ccbed7ab55
docs: destination-capture --- a second dedication route, and the ledger head
Review found dedication is reachable by a second path. Beyond
set_params, a body can call display(buf, { side = "bottom", dedicated =
true }), which writes request.dedicated straight into the side window
at editor_core.rs:4535, then request a second panel buffer and cause
the fallback. An implementation guarding only the named set_params call
passes revision 8 test while keeping the original defect.

That is the important part, and it is worth more than the route itself:
the second route was found in review AFTER the first was specified,
which is the evidence that guarding one named call site is not a
design. The framing now requires every discovered route recorded and
given its OWN acceptance row, states that the two known routes are not
asserted exhaustive, and says finding a third is part of the work
rather than a later review job.

The ledger head still announced revision 7 as implemented and correct,
declared the blocker closed, and prescribed placement-boundary
enforcement --- the design review had just rejected. I corrected the
lower Q#DC-2 paragraph last round and left the authoritative block
alone, so recovery met the rejected design first and the correction
second. That is the same one-site correction failure this session keeps
reproducing, and this time in the file whose entire job is to be the
volatile state of record.

The head now names all three designs, which two were rejected and why,
and that the shipped code implements the rejected one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth b72df34bc6
docs: destination-capture revision 8 --- refuse the mutation, keep the preflight
Revisions 6 and 7 were both wrong, in opposite directions, and review
caught each.

Revision 6 predicted the fallback at preflight and argued the body
could not change it. False: the await refusal stops concurrent
interleaving, not the body, which is arbitrary synchronous Lua and can
dedicate the side slot itself.

Revision 7 moved enforcement to the placement boundary. That breaks the
invariant commit_to exists for. Handoff section 748 states it without
qualification --- it preflights every precondition BEFORE invoking the
callback, because dired mutates handle state, prev and paint long
before it reaches anything that could refuse, so validating at display
time is four mutations too late. A refusal that arrives after arbitrary
Lua has created buffers, handles and paint is not a refusal; it is a
partial commit with an error return.

So revision 8 does neither. It keeps the preflight where it is and
REFUSES the mutations that would invalidate it --- the same shape as
the await refusal already in this file, for the identical reason:
something that would invalidate the scope guarantee is rejected rather
than predicted around. Refusal stays mutation-free on the normal
(false, reason) path.

The mutation surface is narrow, which is what makes this tight rather
than aspirational. dedicated is writable from Lua and is one of only
two writable window fields per Q#BP2c; panel_capable has no Lua binding
at all, checked across src/lua_bindings. But the implementation must
ENUMERATE the body-reachable transitions rather than trust that list
--- closing the side window, or any other route to no usable side slot,
counts, and I have not proven those two exhaustive.

If the enumeration is open-ended, the named fallback is to collapse the
two profiles and always run all four checks. Safe, simple, honest, and
it makes the parameterization pointless --- which is why it is the
fallback and not the answer, and why choosing it needs its own
approval.

The inside-the-body test is strengthened accordingly. Revision 7 asked
it to assert that document B was not replaced, which passes on a design
that lets the body mutate freely and merely declines the final
installation. It now asserts the dedication call is refused, the slot
is still undedicated afterwards, and nothing partial was installed. The
refusal must land on the mutation, not on the outcome.

The ledger Q#DC-2 summary still repeated the disproved premise
verbatim, so a recovering reader met two incompatible answers in one
lane entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth 86cd08959a
fix(window): enforce the panel profile at placement, not at preflight
Revision 7 of `docs/destination-capture-framing.md`, closing the
correctness blocker review found in `0efc8c0` and the smaller
reachability hole beside it.

THE BLOCKER. The `"panel"` commit profile skipped preflight checks 2-4
on the claim that a panel result never touches a document window. That
claim is false: panel placement FALLS BACK to an ordinary document
window when the frontend is not `panel_capable` or its one side slot is
dedicated elsewhere -- `apply_placement` says so in its own comment --
and then installs the result there. So a `"panel"` commit could replace
a NEWER document with every stale-intent guard skipped: capture A, the
user opens B, the continuation lands, B is gone. That is the exact
failure `commit_to` exists to prevent, reached through the profile
meant to be the safe one.

WHY NOT A PREFLIGHT PREDICTION. Revision 6 proposed predicting the
fallback at preflight, arguing nothing could change in between because
the body cannot `await`. Refusing `await` prevents another COROUTINE
interleaving; it places no restriction on the body itself, which is
arbitrary Lua running synchronously and can invalidate the snapshot in
two statements -- take the panel, set it `dedicated`, then request a
side display. No preflight predicate closes that, however phrased: the
measurement is taken before the thing it measures is decided.

WHAT THIS DOES INSTEAD. `EditorCore::display_buffer` refuses between
`resolve_placement` and `apply_placement` when a side request resolved
to `PlacementKind::Ordinary` under an active `"panel"` contract whose
destination fails the document preconditions. That is the first moment
the fallback is a fact rather than a guess, and refusing before
`apply_placement` means a refused fallback mutates nothing. The
contract rides on the core, installed and restored by the same
`ScopedFrontendGuard` that scopes the frontend, so a profile can never
outlive the body that declared it; the field is crate-private, so Lua
cannot claim a profile for a placement it did not commit to.

The preflight predicate SURVIVES as an early refusal and not as the
guarantee. `panel_placement_can_fall_back` still gates the relaxation
in `commit_destination_refusal`, so the statically knowable case -- a
frontend that cannot render a panel at all, and will not acquire the
capability mid-body -- refuses before the body allocates a buffer,
registers a handle and paints. That is the same reason `commit_to`
preflights at all. Both layers are pinned, and neither pin subsumes the
other.

The four document checks now live once, in
`EditorCore::document_destination_refusal`: they are evaluated from two
sites, and two hand-written copies is how a backstop ends up weaker
than the thing it backs.

THREE DELIBERATE LIMITS, each a different decision rather than a
stricter version of this one. The document profile is untouched --
re-running its checks at placement would newly refuse dired's own
documented panel path, which is a preservation-suite stop signal. Only
a fallback is guarded, not every `Ordinary` placement -- a `"panel"`
body calling `display_file` is pinned as succeeding. And the refusal is
of the PLACEMENT, not of falling back: a `"panel"` commit with an
intact destination still degrades gracefully into the document window,
because turning graceful degradation into an error would regress every
consumer that works today on a frontend without panel capability.

THE SECOND HOLE. `commit_profile` did `name.to_str()?`, but Lua strings
are BYTE strings, so a `string.char(255)` profile hit mlua's generic
UTF-8 conversion error before `BAD_COMMIT_PROFILE` was ever
constructed -- the same reachability class as the `Option<String>`
defect revision 5 fixed, one layer down. The comparison is on bytes
now, and the invalid-UTF-8 row joins the number/table/boolean rows
asserting on message content.

FOUR DOC SITES repeated the false claim (`ViewDestination`'s own doc
twice, `capture_view_destination`, `ViewDestinationLua`) and are
corrected. Nothing else relied on it: dired, the only Lua `commit_to`
consumer, takes the two-argument document profile and already had all
four checks; `compile.lua`'s `already_in_panel` queries live state; and
the terminal adopter's rollback keys off `created_side`, already false
on a fallback.

Tests: 12 pins, up from 8. Three carry the enforcement split and none
subsumes another -- the pre-established fallback (both causes, the body
must not run), the inside-the-body transition (the body runs, the
result must not land), and the graceful fallback (a valid destination
still lands). Mutation-checked four ways; the pattern of which rows
survive each mutation is in `docs/active-work.md`.

`journey_acceptance` (47) and `dired_acceptance` (31) pass UNCHANGED.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth edb84a520d
docs: destination-capture revision 7 --- enforce at placement, not preflight
Review found revision 6 fix unsound for the same reason its target was.

Revision 6 moved the panel/document decision to a preflight prediction,
arguing nothing could change before placement because commit_to refuses
await. That refusal stops CONCURRENT INTERLEAVING --- another coroutine
mutating state while this one is parked. It does nothing about the body
itself, which is arbitrary synchronous Lua and can obtain the panel,
set dedicated = true, and then request panel display. Preflight sees a
reusable panel and relaxes checks 2-4; the body causes the fallback;
the result replaces a stale document.

No preflight predicate closes this, however phrased --- the measurement
is taken before the thing it measures is decided. So enforcement moves
to the placement boundary, where resolving to Ordinary for a request
that asked for a side IS the fallback rather than a forecast of one.
The commit scope is already Rust-side app data, so the profile and the
destination can ride there.

The tempting non-fix is named so nobody reaches for it: widening the
predicate from "will it fall back" to "could it ever" is always true,
since the body can always dedicate the slot --- which collapses the two
profiles and buys nothing.

Section 7 gains the test that distinguishes the designs: the callback
dedicates the side slot MID-COMMIT. Both fallback tests revision 6
asked for establish their state before commit_to is entered, so a
preflight-snapshot design passes them. A design passing only those two
has not been shown to work.

The ledger claimed the lane implemented with eight pins covering
section 7. Those pins were written against revision 5 matrix, which
review disproved --- none exercises a fallback placement. A recovering
machine reading that entry would have prepared a PR from a lane with an
open correctness blocker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth 6032ed1c2a
docs: destination-capture revision 6 --- the panel matrix was unsound
Q#DC-2 gave the panel profile only check 1, on the stated claim that a
panel result never touches a document window. That claim is false, and
the tree says so in its own comment: reaching Ordinary while a side was
REQUESTED means the request fell back --- not panel-capable, or the one
side slot is dedicated elsewhere --- and the result is then installed
into an ordinary document window.

So a "panel" commit on a non-panel-capable frontend could replace a
NEWER document while skipping every stale-intent guard, reintroducing
exactly the failure this API exists to prevent. Reproduced in review,
not theorised. That makes it a correctness defect rather than a
strictness preference, and it is my framing error: I wrote the matrix.

The relaxation is now conditional on the placement really being a
panel. Both fallback causes are readable from core state at preflight,
and nothing can change between preflight and placement because
commit_to runs its body synchronously in a scope that refuses await ---
so the prediction cannot go stale under the commit it guards.

What is deliberately NOT the fix: refusing a panel commit that would
fall back. Falling back is existing, intentional behaviour for a
frame without panel capability, and refusing would turn a graceful
degradation into an error. The panel profile relaxes checks; it does
not get to change where things land.

Also closes an invalid-UTF-8 hole in the profile diagnostic. Lua
strings are byte strings, so string.char(255) reaches to_str() and
produces mlua generic conversion error before the documented message
naming the accepted values is ever constructed. Same reachability class
as revision 5 Option<String> defect, one layer further down --- which
is worth noticing, because I fixed that one and did not look for the
next one.

And the header said "Pre-implementation. Awaiting approval" through
revisions 2 to 5 while the ledger recorded the lane approved and
implemented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth 3e64898c64
docs: record the destination-capture lane as implemented
Section-local edit to the lane's own block; several lanes edit this file
concurrently.

Records the two commits, the green gate line (both preservation suites
unchanged), the mutation checks that show a matrix of deliberate
omissions is not passing vacuously, and the two places the framing did
not match the tree:

- the rename was 11 references across 5 files, not 8 across 4 —
  `src/daemon.rs` also calls the capture;
- Q#DC-4's "frontend with no document window" is a DEFENSIVE branch.
  Q#BP6 asserts a layout always keeps a non-side window, with a
  `debug_assert!` in `non_side_target` that fires under `cargo test`, so
  a registered frontend in a healthy editor always has a live document
  window. The decision stands, but #227 should not expect to meet that
  refusal.

Neither changed a decision, and both are recorded rather than quietly
absorbed: the framing says "counted, not estimated", and the next reader
will check.

`ViewDestination`'s own doc comment is corrected in the same commit,
because it repeated the framing's over-claim ("a frontend showing only a
side window") in the one place a reader would trust it, and
`capture_view_destination` now says how reachable its empty pair
actually is. Code, not only ledger, since the ledger is not what someone
reads when they wonder whether that branch can fire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth a177d61bf3
docs: destination-capture revision 5 --- make the profile error reachable
Revision 4 API spec contradicted itself at the binding boundary. It
required profile: Option<String> AND a pointed error naming "document"
and "panel" when a non-string arrives. mlua rejects a number or table
during argument conversion, before the closure body runs, so that
message was unreachable: a caller passing 42 would have got mlua
generic conversion error instead.

This is the identical trap the existing binding already documents for
dest --- typed Value rather than AnyUserData specifically so the
message stays REACHABLE and names the rule --- and revision 4 quoted
that comment as its reasoning while repeating the mistake one argument
to the right.

The profile is now mlua::Value, validated in the body. Nil and absence
BOTH mean document, spelled out because a Lua caller threading an
optional variable produces nil rather than absence and a third
behaviour there would stay invisible until someone hit it. A
non-string is refused by the same message that names the accepted
values.

The verification bullet is now the guard on the type choice rather than
on the behaviour: the non-string refusal is asserted ON ITS CONTENT, so
retyping the argument to Option<String> later stops the assertion
matching rather than silently degrading the error a user sees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth 1b4046b624
docs: destination-capture revision 4 --- pin the commit_to call shape
Revisions 2 and 3 said "the profile is declared at commit_to" and never
said how. That is not a detail. The binding accepts exactly (dest,
body) today, so without a specified form #227 has no stable API to
adopt against, and the promise that existing callers keep their
semantics was a hope rather than a contract.

Q#DC-5: commit_to(dest, body [, profile]). An optional TRAILING string,
typed Option<String>, so there is no arity sniffing and no
table-or-function dispatch on argument 2 --- the existing binding chose
Value over AnyUserData specifically to keep its error message reachable
and naming the rule, and a polymorphic second argument would undo that.

Trailing reads badly after a long inline closure, but that is not the
call shape in use: dired defines a named local commit at dired.lua:670
and calls commit_to(opts.dest, commit) at :717. Verified, not assumed.
Against a named body the trailing profile reads fine.

The value set is CLOSED --- document and panel, exactly Q#DC-2 two
profiles. A third is a decision, not a spelling.

Omitted means document, and that is the load-bearing part: every
existing two-argument call keeps all four preflight checks by
definition of the signature, so journey_acceptance passing untouched
follows from the API shape rather than from care.

An unrecognized profile is an ERROR naming the accepted values, not a
silent fallback to document. A fallback would hand a caller stricter or
looser checks than it asked for, which is the failure the whole
parameterization exists to prevent. Its witness asserts the legacy
two-argument form through a check the panel profile OMITS --- a
stale-buffer refusal --- because asserting merely that it does not
error would pass on a call silently downgraded to panel, which is the
regression that would quietly void Journey Stage 1a guarantees.

Git mapping settled here rather than rediscovered during adoption:
*git-status* takes panel, *git-diff* takes document.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth 6b8e07c730
docs: destination-capture revision 3 --- decide Q#DC-4, pin the gate line
Q#DC-4 contradicted Q#DC-2, and on the primary panel API. Q#DC-2
concluded a panel profile depends only on a live frontend, so it can
commit with no document window at all; Q#DC-4 still voted to return nil
in exactly that case and told git to fall back to ambient behaviour.
Those cannot both hold, and the fallback advice was independently
wrong: falling back to ambient IS the P1a bug this lane exists to
remove.

Decided rather than voted on, since it is the primary API. The
destination document pair is optional; capture_destination() is
profile-blind and argument-free, because making capture profile-aware
would force a caller to know at capture time what it will do at commit
time, which is the opposite of why capture exists. The profile is
declared at commit_to, where Q#DC-2 parameterization already lives, and
a document-profile commit with no document pair is refused alongside
the other four preflight refusals. Capture never returns nil while a
frame exists.

Section 4 outline and Q#DC-1 were updated to match rather than left to
disagree --- Q#DC-1 no-arguments answer is now load-bearing rather than
incidental, because no arguments is what keeps capture profile-blind.

The ledger gate line said "new suite plus dired". --acceptance is
repeatable, so it now carries the executable command including
journey_acceptance and dired_acceptance, both named as preservation
suites and a stop signal. A volatile ledger that understates required
coverage is how a recovering machine runs a weaker gate than the lane
agreed to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth 91e4e514a1
docs: destination-capture revision 2 --- full matrix, preservation suite, coherence
Three review findings.

Q#DC-2 parameterization was incomplete. Revision 1 said only "skip the
stale-buffer check for a non-replacing continuation", but a panel
result does not depend on the captured document window at all: it does
not replace that window buffer (check 3), does not occupy it (check 4),
and does not need that specific window to exist (check 2). Retaining
any of the three can reject git.status for an unrelated document-window
change; dropping them without an explicit profile risks weakening
document replacement. The question now carries a four-row matrix with
two profiles, and check 1 --- the requesting frontend still has a
layout --- is the entire panel profile.

That has a consequence the framing now states rather than leaving to be
discovered: if the panel profile needs only the frontend, a frontend
with no document window can still host a panel, so Q#DC-4 return-nil
rule is right for the document profile and possibly wrong for the panel
one. Settled as part of answering Q#DC-2, not after it.

tests/journey_acceptance.rs joins dired as a named preservation suite
and stop signal. It carries 27 commit_to references across nine named
pins --- forged destination, scope-and-restore on normal return and on
raise, await refusal, delivery to the requesting frontend, the
declining-listener redirect guard, and two already named preservation_*
--- and Journey Stage 1a own framing treats it as a required gate. A
lane that generalizes its substrate does not get to relax that. The
stop signal now covers both suites: a suite edited to accommodate the
change under test has stopped being evidence.

The coherence-impact section was missing entirely. CLAUDE.md and
COHERENCE.md section 25 both require one for coherence-affecting work,
and this lane qualifies twice over --- new Lua API surface, and a
generalization of a Journey substrate. Section 16 is the section it
serves. Journey steps: none added, one protected. Islands, config
registry: none. Section 9: neutral, and stated precisely, because
knowing which frontend a result belongs to is NOT knowing who asked for
it --- that is the worker-identity arc and the two should not be
conflated just because both concern async continuations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth 71ef951535
docs: frame a general destination capture (revision 1)
PR #227 review found that git async completions surface in whichever
frame is active when git exits, and named the right mechanism:
commit_to exists for exactly this continuation boundary, built by
Journey Stage 1a Q#JR14 because the work settles a tick or more later,
by which time the ambient frontend, window and buffer may all name
something else.

The fix is not available to git, which is why this is a lane rather
than a line in #227. commit_to takes a DirectoryDestinationLua that is
nonconstructible from Lua by deliberate design, and the only site that
mints one is inside the path.open-directory listener dispatch, from a
pub(crate) capture. Any async Lua continuation that is not a directory
open has no way to say where its result belongs.

The captured data is already generic --- frontend, window, buffer, with
nothing directory-specific in it. Only the name and the capture site
are, and the rename is 8 references across 4 files, counted rather than
estimated.

The substantive question is Q#DC-2, and scouting is what surfaced it.
Git two continuations are different in kind. *git-status* goes to the
bottom panel, because listview.open resolves display with a "panel"
default. *git-diff* replaces a document window, deliberately, so the
status panel it was invoked from stays visible beside it. The
stale-intent check that commit_to preflight runs --- the window still
shows the captured buffer --- is right for the second and wrong for the
first: the panel never touches that window buffer, so refusing because
the user switched files there is a refusal with no relationship to what
the continuation does. One shape either over-refuses the panel case or
under-checks the document case, and the framing votes for a
parameterized preflight while holding that vote loosely.

No adopter in this lane. Git adoption is #227 work after this lands; a
prerequisite that also converts its first consumer makes the two
impossible to review separately.

Verification carries a stop signal rather than a target: if any
existing dired test needs editing, the generalization changed Journey
Stage 1a semantics and that is cause to stop, not to adjust a test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 14:11:43 +02:00
Levi Neuwirth 5cf1d61b92
fix(discovery): clip command descriptions at the single-row surfaces
PR #228 review found a correctness gap this lane made reachable. The
GPU dropdown derives its height, its visible window and its
selection-highlight offset from `rows.len()` — ONE logical row per
candidate — while a detail carrying a line break shapes into more
physical lines than that. One such row misaligns every row below it
and the highlight with it. The grid TUI has the same exposure from the
other side: it writes the description into a single-row suffix on the
minibuffer band.

## Why not reject CR/LF at registration

That was the obvious fix. It was implemented, measured, and abandoned
on evidence.

MCP tool registration renders a whole schema block into
`Command.description` — tool text, blank line, `Arguments:`, then one
line per argument (`tests/fixtures/pmacs-mcp-tools/init.lua:272`, a
`table.concat(lines, "\n")`, used at `:496`). And
`tests/m9_6_acceptance.rs:583-598` ASSERTS four of those lines. A
one-line guard in `CommandRegistry::define` fails 36 tests across
`m9_6` (19/25), `m9_7` (16/19) and `m9_8` (1/17), in both feature
configurations, and could only be made green by deleting a shipped
acceptance criterion.

So the one-line constraint goes where the constraint actually is: the
surfaces that have one row. `Command.description` stays free-form,
which it legitimately is.

## The change

`Command::description_first_line` clips to the first CR **or** LF — a
lone CR ends a line too, and an LF-only clip would pass a bare `\r`
straight through to the same surface. Both single-row consumers call
it: the semantic producer filling `MinibufferRow.detail`
(`src/semantic_render.rs`) and the TUI suffix (`src/editor.rs`). A
first line that is empty ships as `None` rather than `Some("")`, which
would draw trailing padding.

No ellipsis or truncation marker, matching the in-tree precedent and
the minibuffer's own width rule.

`describe-command` and `help.list-commands` are untouched and still
report every line. That is what makes this a rendering decision rather
than data loss, and it is asserted, not assumed.

## Precedent, already in this tree

The same MCP fixture clips a tool RESULT to its first line because
"a multi-line set_status would corrupt the row layout"
(`init.lua:277-285`), leaving width clipping to the frontend. Same
hazard class, same resolution.

## Verification

`src/command.rs`: a schema block registers AND clips, in all three
break forms; a single-line description is byte-identical after the
clip; an empty first line clips to empty.

`tests/discovery_stage2_acceptance.rs`: an MCP-shaped description
reaches the TUI band and the GPU row as one line, through the real
prompt path — with the full text still reachable via
`describe-command` asserted alongside, so a clip that deleted the
schema block everywhere would fail rather than pass.

`pmacs-gpu`: one physical shaped line per logical candidate row — the
geometry invariant the dropdown depends on.

Mutation-checked: neutering `first_line` to the identity fails all
four new break-handling tests
(`a_multi_line_description_registers_and_clips_to_its_first_line`,
`a_description_whose_first_line_is_empty_clips_to_empty`,
`a_multi_line_description_reaches_the_tui_band_as_one_line`,
`a_multi_line_description_reaches_the_gpu_row_as_one_physical_line`)
and leaves the two "did not tighten past purpose" tests green.

`Command.description`'s doc comment claimed "one-line", which the MCP
path openly violates. It now states the real contract and records why
a registration guard must not be re-proposed.

`m9_6`/`m9_7`/`m9_8` pass COMPLETELY UNTOUCHED, and are now named
gate suites so that stays on the record.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 13:52:32 +02:00
Levi Neuwirth bf561ff296
docs: record PR #228 and its merge block
The lane heading said "no PR yet". It also needs to carry WHY the PR is
merge-blocked, because a reader who finds only "blocked" will treat it
as backlog hygiene and unblock it by rerunning the gate.

The problem is gate integrity. --protocol promises the CRDT workspace
sweep, that sweep has a documented precondition (handoff section 5),
and the script does not run it --- confirmed by reading the plan
emitter, not inferred from the failure. So a --protocol result can be
decided by whether the build directory happened to contain pmacs-gpu
rather than by the diff under test.

It was latent until #225 gave each worktree its own target directory. A
shared target dir usually already had pmacs-gpu built, which satisfied
the precondition by accident and hid the omission.

Unblocking needs both halves recorded: the scripts/gate repair as its
own framing and PR, and then a fresh-target rerun of this branch
protocol gate under the repaired script. A rerun alone would reproduce
the same accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 13:52:32 +02:00
Levi Neuwirth 70e5781420
feat(discovery): M-x rows carry descriptions — protocol v22 -> v23
`Command.description` has always been required and has always been
rendered by `help.list-commands`. It was missing at the one moment it
would change a decision: the M-x row. This carries it there.

COHERENCE.md §5's clause "M-x rows are still bare names", per
docs/discovery-stage2-framing.md revision 3.

## The wire half is additive, and the old variant is FROZEN

postcard is not self-describing: enum variants encode by index and
fields by position. Widening `MinibufferPrompt.candidates` in place
would make every v12–v22 peer MIS-DECODE the bytes rather than ignore
them — and gating the widened form at `>= 23` would not rescue them
either, because with only one variant to gate they would receive no
minibuffer message at all. Compatibility requires the old shape to
still exist AND still be sent.

So `MinibufferPrompt` is retained unchanged for `12..=22`, and
`MinibufferPromptRows { prompt, input, cursor, rows, selected, total }`
is APPENDED as the final variant, carrying `MinibufferRow { label,
detail: Option<String> }`. A new row type, not `CompletionPopupRow`,
whose `kind` is an LSP `CompletionItemKind` code with no honest value
for a command (Q#D2-1).

Exactly one of the two reaches any peer, ever. The producer selects on
the session's negotiated version, so the CLOSE necessarily uses the
same family as the OPEN — a rows session closed by a legacy clear
leaves the dropdown on screen forever. The daemon's write loop gates
both directions again, with the legacy gate written as a RANGE
(`12..MINIBUFFER_ROWS_MIN_VERSION`) rather than a floor, so a v23 peer
cannot receive both and double-render.

`ADVERTISED_PROTOCOL_VERSION` stays 20, untouched.

## The TUI half involves no wire at all

`src/editor.rs` contains zero references to `MinibufferPrompt`:
`paint_minibuffer` reads `core.minibuffer` directly. So it reads
`Command.description` from the registry in-process, which is why this
half is independent of the bump.

Clipping is three ORDERED steps (§3.4), and the guarantee is "never a
PARTIAL name", not "the name always survives" — the prompt and typed
input consume the budget first, so the remainder can be too small even
for the bare name. If the whole name does not fit, the suffix is
omitted entirely; only once it fits is a description attempted; a
description that does not fit whole is dropped, leaving today's
`[name]`. No ellipsis stub, and no prefix of a name is ever emitted.

## Verification

`src/protocol.rs` gains this repo's FIRST literal postcard byte
fixtures: `minibuffer_prompt_v12_wire_bytes_are_frozen`, open and
cleared. A round-trip freezes nothing — it encodes and decodes with
the same types, so a field addition leaves it passing while every
shipped peer breaks. Bite-verified: reordering two fields of
`MinibufferPrompt` leaves `minibuffer_prompt_round_trips_through_postcard`
green and fails the fixture.

`line_wrap_facts_encoding_is_unchanged_by_the_v23_build` pins the
PREVIOUS final variant, per the handoff §4 rule that an appended
variant's own round-trip cannot detect a discriminant shift.

`tests/discovery_stage2_acceptance.rs` runs ONE daemon serving a v22
and a v23 session simultaneously, through the real M-x key path, and
asserts each receives its own variant AND ONLY its own — open and
close alike — by collecting every minibuffer message rather than
filtering for the expected one.

No cross-version cache test, deliberately (§3.2):
`SemanticRenderState::for_peer` bakes the negotiated version in at
attach and is dropped at detach, so a cache cannot span two versions.
A test for an impossible condition passes forever while teaching the
next reader that the hazard is real.

Five version assertions updated, each read before editing:
`src/protocol.rs` (the `PROTOCOL_VERSION` tripwire, renamed; and the
v6-floor ladder's accepted/rejected ranges),
`tests/statusline_segments_acceptance.rs`,
`tests/bottom_panel_stage2b_gpu_acceptance.rs`,
`tests/vterm_stage3_acceptance.rs`. No `ADVERTISED_PROTOCOL_VERSION`
assertion fired.

Gates: `scripts/gate --protocol --acceptance discovery_stage2_acceptance`
— all ten green, including the strengthened two-configuration sweep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 13:52:32 +02:00
Levi Neuwirth 6eecbfcca1
docs: record discovery Stage 2 framing approval
Revision 3 was approved 2026-08-09 at 6a9a25d, after the three stale
internal references were cleared. The lane heading and the framing
status line both still said "in review", which is the kind of
contradiction that makes a reader trust whichever line they hit first.

The lane entry now also records WHAT the three review rounds found,
rather than only how many there were. A count says the doc was
reviewed; the list says what class of error it was reviewed for, which
is the part a later reader can act on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 13:52:32 +02:00
Levi Neuwirth 0e260e0377
docs: clear three stale references revision 3 left behind
Revision 3 corrected the cache model in section 3.2 and the clipping
rule in 3.4, then failed to propagate either. Internal contradictions in
a framing document are worse than the original errors: a later reader
resolves them by picking whichever statement they read first.

  - Q#D2-4 still summarised the design as "per-variant cache keys",
    contradicting 3.2's corrected one-per-peer model. Now names the
    `peer_knows_minibuffer_rows` producer gate and the single per-peer
    cache, with a parenthetical recording that the sentence was stale
    rather than silently rewriting history.
  - Section 6 still required the cross-version cache test that
    revision 3 removed as unfalsifiable. Replaced with what IS
    assertable --- a v22 and a v23 peer attached simultaneously each
    receive their own variant and only their own --- and the bullet
    says why the other test is absent, so it does not get "restored"
    later by someone who notices the gap.
  - The active-work lane still called the framing revision 2.

Swept for the same class of residue from the other two corrections
("the name must survive", "both frontends render"); the remaining hits
are the notes ABOUT those corrections, which are deliberate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 13:52:32 +02:00
Levi Neuwirth b3a6da2416
docs: Discovery Stage 2 framing, revision 3
Three corrections, each to something revision 2 asserted without
checking the mechanism it was reasoning about. All three verified in
the tree.

A ROUND-TRIP FREEZES NOTHING. Revision 2 proposed pinning the legacy
variant with an encode/decode round-trip. That test uses the SAME types
on both sides, so adding a field to `MinibufferPrompt` leaves it
passing while every v12-v22 peer in the field breaks. The existing
`minibuffer_prompt_round_trips_through_postcard` (src/protocol.rs:2363)
is precisely that shape, and there are NO literal byte fixtures
anywhere in the protocol tests today --- checked, not assumed. Only
`assert_eq!(encoded, LEGACY_BYTES)` against bytes captured now can fail
when the encoding changes. Two fixtures, open and clear, matching the
shapes the semantic test already covers so the corpus is not a fresh
judgement call.

THE CACHE HAZARD I DESCRIBED CANNOT HAPPEN. Revision 2 argued for a
per-variant cache key on the grounds that a peer reconnecting at a
different version could have its first message suppressed as a
duplicate. `SemanticRenderState::for_peer(frontend_id,
negotiated_protocol_version)` is created PER PEER with its version
baked in on attach (src/daemon.rs:2080) and removed on detach (:1591),
so a cache never spans two versions and reconnecting creates a fresh
state. One per-peer cache stands; the selection is a producer gate,
`peer_knows_minibuffer_rows`, alongside the four such gates that
already exist (:1410-1435). The test for the impossible condition is
REMOVED rather than written --- a test that cannot fail passes forever
and teaches the next reader that the hazard is real.

The matched open/close family requirement is independent of caching and
stands unchanged.

"THE NAME MUST SURVIVE" IS NOT ACHIEVABLE. Prompt plus typed input
consume the width budget first, so the remaining suffix space can be
too small for even the bare name --- and revision 2's rule would then
have forced a partial name, which reads as a DIFFERENT command. The
rule is now ordered: if the whole name does not fit, omit the suffix
entirely; only once it fits is a description attempted; if the
description does not fit whole, drop it. The guarantee becomes "never a
partial name", which is achievable, rather than "the name always
survives", which is not. Witnessed at three widths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 13:52:32 +02:00
Levi Neuwirth e9bfe9a14c
docs: frame Discovery Stage 2 (revision 2) — M-x rows
COHERENCE.md section 5 grades discoverability Partial after Stage 1 and
names "M-x rows are still bare names". The descriptions ALREADY EXIST
--- `Command.description` is required, and `help.list-commands` renders
them --- so this is substrate without surface in its purest form: the
information is present, surfaced elsewhere, and absent from the one
moment it would change a decision.

TWO REVISION-1 CLAIMS WERE WRONG, both checkable in the tree:

  - "Change `candidates` in place and gate at >= 23." Postcard is NOT
    self-describing: fields encode positionally, so a v22 peer decoding
    `Vec<MinibufferRow>` where it expects `Vec<String>` mis-reads the
    bytes rather than skipping them. And gating would not have rescued
    it --- with only one variant to gate, a v12-v22 peer would have
    received NO MINIBUFFER AT ALL. That variant goes to every peer
    negotiated >= 12 (src/daemon.rs:1472).

    Revision 2 is additive: `MinibufferPromptRows` APPENDED to the enum
    (indices are positional; inserting renumbers everything),
    `MinibufferPrompt` frozen for v12-v22, per-peer selection, per-
    variant cache keys, and close matching the open's family --- a
    mismatched close is how a popup stays on screen forever.

  - "Both frontends render label + detail." The grid TUI never reads
    `MinibufferPrompt`; src/editor.rs contains ZERO references to it.
    It paints from `core.minibuffer` and renders the selected candidate
    as `format!("  [{cand}]")` (src/editor.rs:5484). The rich wire
    reaches pmacs-gpu only.

    So the TUI half is a LOCAL formatting change --- it is in-process
    with the core and reads `Command.description` from the registry
    directly, with no wire involvement. The contract is pinned
    including clipping: THE NAME SURVIVES AND THE DESCRIPTION IS
    DROPPED at narrow widths, because a clipped name is strictly worse
    than today's bare one. A multi-row TUI chooser is explicitly not
    this lane.

This lane HOLDS THE BUMP SLOT. Git Stage 1 is no-wire and runs beside
it; git Stage 2 needs a bump and must wait.

Q#D2-5 records a trap that arrives with the feature: richer rows make
M-x LOOK like a closed set, inviting someone to make acceptance reject
unmatched input. Completion is assistance, not validation ---
`resolve_accepted_value` returns literal typed text by design --- so
that would be a behaviour change, not a rendering one, and it is out of
scope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 13:52:32 +02:00
Levi Neuwirth 9a1e11c0c8
fix(lsp): a `/` search boundary no longer disables the LaTeX marker walk
`latex_within_boundary` answered a question about path COMPONENTS with
string arithmetic:

    dir:sub(1, #boundary + 1) == boundary .. "/"

With a `/` boundary the needle is `"//"`, which no canonical path
begins with. Every ancestor was therefore judged out of bounds, the
marker walk never examined a single directory, and each chapter of a
thesis got its own root — two texlab processes for one document tree.
The lane's headline behaviour, silently off, with all fourteen shipped
tests still green because every one of them clamps the boundary to its
own tempdir.

Fixed by comparing segments rather than characters, so the root is a
boundary with zero segments — containing everything by construction
instead of by a special case, and tolerating a trailing separator for
free. The same root-is-special trap sat at two other points on the same
path and is closed with it:

  * `latex_parent_of` returned nil for a top-level directory, making `/`
    the one directory the walk could never examine — the identical bug
    from the far end. It now yields `/`, matching `walk_for_marker`'s
    `Path::ancestors` on the Rust side, and still terminates because `/`
    has no component to strip.
  * `latex_root_for` sliced `/paper.tex` to an EMPTY directory, which
    canonicalizes to nothing and made the resolver DECLINE — and a
    decline is the one path that falls through to `pmacs.project.detect`,
    whose walk includes `.git`. A document at the filesystem root now
    roots at `/`.
  * `latex_marker_in`'s join is guarded for `dir == "/"`, which this
    change makes reachable for the first time; the naive form produces
    `//name`, the one spelling POSIX leaves implementation-defined.

Two new pins plus a strengthened one, 16 tests:

  * `two_chapters_share_one_server_under_a_root_search_boundary` — the
    defect end to end through ATTACH, not on the predicate, because the
    symptom is two servers rather than a wrong string. Restoring the old
    comparison fails exactly this test, with the two-server output.
  * `latex_root_walk_stops_at_the_search_boundary` now asserts BOTH
    directions. "Stops at the boundary" is also satisfied by a walk that
    never runs — which is precisely what a `/` boundary produced — so the
    hermeticity property (R8's shape: a stray `latexmkrc` above the
    tempdir must stay invisible) is now paired with the walk still
    climbing to, and examining, the boundary directory itself.
  * `latex_root_for_a_document_at_the_filesystem_root_is_the_root`.

Also corrects `docs/active-work.md`: §3 no longer awaits a revision 3 —
`b5eaf27` IS revision 3 — and the lane entry now records that boundary
handling has been this resolver's interesting part twice, so a reader
weighing whether to trust it knows where to look first.

Gates: ALL GREEN via `./scripts/gate --acceptance lsp_latex_acceptance`.
`/tmp` on this machine is a near-full tmpfs and three grep tests fail
there with `QuotaExceeded`; the green run used
`TMPDIR=/home/jeans/build/pmacs-gate-tmp/lsp-latex`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 10:48:49 +02:00
Levi Neuwirth 12b7ee58d8
docs: LaTeX framing revision 3 --- step zero corrected the premise
The .texlabroot caveat is discharged, and discharging it falsified the
reasoning behind it.

.texlabroot is real, and texlab marker set is wider than this document
listed --- the bare texlabroot and latexmkrc spellings count too, and
the walk takes the innermost. The implementation ships texlab own set.

But the caveat framed marker 1 as conditional on texlab honouring the
FILE, which was the wrong question. Every arm of texlab ancestor walk
searches documents already loaded, and that workspace is built from the
folders the CLIENT supplies. Observed: with rootUri at chapters/, no
ancestor marker widened texlab view and its dependency graph never
reached the parent document; with rootUri at the marker directory the
parent resolved either way.

So texlab honours the root it is handed and never corrects a too-narrow
one. config.latex.root IS the project scope, which makes the resolver
the whole value of the lane for a multi-file thesis rather than a
nicety --- the opposite of how section 2 "Slice 1 is one config entry"
reads.

Second correction, and this one was a real trap. Revision 2 said .git
is deliberately excluded from the walk and stopped there. Omitting it
is not sufficient: project_root_for falls through to
pmacs.project.detect when a resolver returns nil, and that walk lists
.git among its markers at src/project.rs:184. A resolver that politely
declined on a markerless file would hand texlab the monorepo by the
back door, with the exclusion looking correct at every line you would
think to read. The resolver never declines for a file with a directory,
and the fixture asserts the shared detector really would have answered
the repo root so the pin cannot pass vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 10:48:49 +02:00
Levi Neuwirth 22d72fa7f7
docs: record the LaTeX lane as implemented, step zero discharged
Section-local edit to the lane's own block in `docs/active-work.md`.

The heading moves from "BRANCHED, framing in review" to "IMPLEMENTED,
gates green, no PR yet", and the §3 `.texlabroot` caveat is written up
as discharged rather than merely resolved: what texlab actually does,
how it was observed, and the one place the framing now reads stale.

The substantive finding recorded here is not "the marker works". It is
that texlab's own root walk only sees markers belonging to documents
already in its workspace, and the workspace comes from the folders the
client supplies — so texlab honours the root pmacs hands it and never
corrects a too-narrow one. That makes `config.latex.root` the project
scope rather than a hint, which is worth carrying forward whether or
not anyone rereads the framing.

Also recorded: the shared-`CARGO_TARGET_DIR` trap, because a bare
`cargo test` in this worktree fails with compile errors from a sibling
lane's code and reads exactly like a broken branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 10:48:49 +02:00
Levi Neuwirth 75f5f97582
docs: authorize LSP LaTeX implementation, precisely
Recorded as "implementation authorized" rather than "approved", because
that is what happened: the user authorized dispatch after a summary of
revision 2 four corrections, not after returning findings on the
document the way they did for the other four lanes.

The distinction matters for one reason. Section 3 .texlabroot caveat is
unverified and blocking --- texlab 5.25.1 is installed and its version
and CLI were checked, but the CLI exposes only run and inverse-search,
so its LSP-level behaviour was never established. A framing marked
plainly "approved" invites a reader to treat that caveat as settled
prose. It is step zero of the work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 10:48:49 +02:00
Levi Neuwirth 7992054153
docs: LSP LaTeX coverage framing revision 2, on a branch at last
Revision 1 was untracked, on main, in a single checkout. By the handoff
own rule --- work is portable only after it is committed and pushed ---
it did not travel. Committing it here is the first fix.

Three factual corrections, all from checking rather than reading.

haskell-language-server IS installed on this machine, along with its
wrapper. Revision 1 said it was not, and that claim was the entire
basis of its Slice 1 / Slice 2 split. Only OCaml still lacks a server.
The argument for leaving Haskell out survives, but on use evidence ---
the .hs files are a rarely-edited Hakyll generator --- not on
dependency cost.

Slice 1 is smaller than framed. Revision 1 proposed filetype mappings
for .tex/.latex/.sty/.cls "so highlighting and LSP agree on what a
LaTeX file is". They already agree, by construction: the grammar
carries exactly those extensions at src/syntax.rs:1111,
grammar-extension detection sits AHEAD of the LSP filetype map in the
precedence chain per the merged grammar framing at :166-171, and
lsp.lua:267-270 calls the filetype map "mainly the LSP-only fallback".
The real missing piece is one config entry.

Q#LX3 deferral argument read a stale line. COHERENCE.md:124 and :867
both record multi-root LSP affinity as merged in #161; only :1669 still
says "first slice in flight", contradicting the same document twice.
The one item revision 1 said could justify deferring therefore
dissolves. The COHERENCE.md inconsistency is real and is left for
whoever next touches section 20 rather than smuggled in here.

Q#LX2, which revision 1 called the question most likely to make the
entry wrong in practice, is now answered rather than shrugged at. An
upward marker walk through config.latex.root, which already accepts a
resolver function. .git is deliberately excluded: a repository root is
the wrong answer for LaTeX, since texlab wants the document root, and
this is the one place where copying the other fourteen entries
instinct would be actively wrong.

The .texlabroot marker is marked UNVERIFIED and blocking. texlab 5.25.1
is installed and its version and CLI were checked directly, but the CLI
exposes only run and inverse-search, so its LSP-level behaviour was not
established. Confirm against a live session before implementing. Same
discipline the gate-protocol-build lane applies to its own
precondition: the thing the design rests on gets observed, not
reasoned about.

Also renumbered Q#HS1 to Q#LX4, because revision 1 Q#LX2 collided with
a live question ID in the merged grammar framing for the same language.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 10:48:49 +02:00
Levi Neuwirth 3b10f9d916
docs: both U4 and U5 controls are asymmetric, and said so only one way
Three corrections. The first two are the same error in two rows.

I wrote each control as if it DECIDES, when each is informative in only
one direction.

U4: extending the deadline establishes "emitted late" IF the clear
arrives. If it does not arrive, that establishes only "not observed by
the longer deadline" --- not "never emitted" --- because transport loss
produces the same absence. No deadline, however long, separates
non-emission from transport loss. That needs producer-side emission
evidence, did pmacs write the clear, cross-checked against the
collected stream. The row now states both branches and names what the
negative branch cannot conclude.

U5: one isolated run cannot decide whether the gate suite is
implicated. A matching isolated RED proves the gate suite is not
necessary for the failure. An isolated GREEN proves nothing beyond that
run, because the failure is intermittent and absence under one run is
not evidence of dependence. I had written it as though either outcome
settled the question.

This is worth naming as a class rather than two typos: a control whose
positive branch is conclusive and whose negative branch is not, written
up as though both were, is how an inconclusive result gets recorded as
an exclusion. Two rows in this file had it.

Third, minor: "three docs" was accurate at the occurrence tip and is
not now --- #229 has since added this registry file. Replaced with
"documentation" so the claim does not rot again with the next commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 10:26:07 +02:00
Levi Neuwirth 5029915ae3
docs: normalize U4/U5 fragments, and drop three claims the evidence does not carry
Four review findings, all mine.

Normalization. The byte count and the :LINE suffix are
occurrence-specific --- the count is the collected suffix length, which
varies per run, and the line moves with the file --- so neither can be
a required fragment. Both rows now carry stable fragments and an
explicit NOT-fragments field naming what must not be matched on.

The LuaJIT-pass argument was the rejected overreach again. I used a
passing sibling leg as a STRUCTURAL exclusion; a deterministic defect
can be Lua-flavour-specific, so it is corroboration only. The row now
says so in those words. The real grounds are stronger anyway and were
sitting there: the workflow never invokes scripts/gate, and
full_grid_resync_acceptance runs BEFORE the changed gate suite, which
closes even the leaked-state path.

Three contradictions inside U4, each removed.

"It never emitted the blank" asserts a mechanism the next field
simultaneously calls open. Now: no blank was OBSERVED after the mark
within the deadline.

The 25,362 bytes were not a capped window. suffix.len() is the ENTIRE
post-mark output; only the displayed head is truncated, to 400 bytes.
Verified in the test source. So my control --- capture the full stream
rather than the window --- was solving a gap that does not exist. The
gap is arrival TIME, and the control now instruments that instead.

The ~20s failure duration IS the fixed Duration::from_secs(20) timeout,
so the ratio against a fast pass is mechanically determined and is not
independent timing evidence. Also verified in the source.

U5 control relabelled. Running m5_8_acceptance alone decides whether
the gate suite is implicated --- cross-suite attribution --- and
nothing more. It cannot separate injected-before-raw-mode from
raw-mode-lost from a third cause, and another isolated pass cannot
either however often it is repeated. Mechanism discrimination needs
readiness and raw-mode state observed AT INJECTION, which is now a
second, separately labelled control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-10 00:02:53 +02:00
Levi Neuwirth ae6a815435
docs: two unresolved macOS signatures from #229 CI, as separate rows
PR #229 first CI run went red on Test (macos-latest / lua54) with
a_pty_resize_blanks_the_host_before_repainting. The rerun turned that
selector green in 0.50s against 20.26s failing, and went red on a
DIFFERENT selector, ctrl_c_during_reconnect_sleep_yields_clean_exit.
Per this file matching rule that is a new incident, not the resize
signature occurring twice, so they are two rows.

U4 records the resize failure with its exact fragments. The diff is
excluded on two structural grounds that do not depend on a rerun: #229
touches no src/ at all and no test but gate_script_acceptance, and the
sibling luajit leg passed on the same commit. A deterministic platform
defect fails both flavours --- that is how #227 non-UTF-8 fixture
presented.

U5 records the Ctrl-C failure at deliberately WEAKER exclusion
strength, and says so in its own field. The changed gate suite ran
earlier in the same job and creates worktrees and directories. No
leaked child or persistent signal-state mutation was observed, but "the
diff touches no src/" is not the same argument here as for U4, because
cross-suite leaked state is a path reachability reasoning does not
close. Its control is to run m5_8_acceptance alone, without the gate
suite ahead of it, before attributing anything either way.

Neither row claims a mechanism. The exit status shows only that Ctrl-C
arrived as SIGINT rather than as the raw-mode key event the test
drives; whether injection preceded raw mode, raw mode was lost, or
something else happened is open, and the fragment does not separate
them.

Also flagged: worker-identity-stage1 independently defines its own U4
and U5. This lane merges first, so that branch must renumber on rebase
--- a conflict resolved textually without renumbering would leave two
different incidents sharing an id, which is the failure the matching
rule exists to prevent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 23:30:53 +02:00
Levi Neuwirth a9544fa959
docs: narrow the copy-detection claim to the fixture it was measured on
The lane asserted that real git emits no 2 C record even under
status.renames=copies, and a test MEASURED it. The measurement was
real; the proposition drawn from it was too broad.

git-status(1) documents C as "copied (if config option status.renames
is set to copies)", so git does emit it. What the fixture establishes
is narrower: with ITS copy source left unchanged, git reports 1 A. and
emits no 2 C record. That is a fact about the fixture.

It is still sufficient reason to craft the row through _deliver_status
--- a weaker and true justification replacing a stronger false one ---
so no behaviour changes and no test changes. The score-based runtime
fix was never in question.

Six sites narrowed, not the two that review cited: the framing bullet,
the ledger bullet, the g6_4b doc comment, the premise comment, the
assertion message, and the rename_and_copy_repo fixture doc. Each also
records the retraction, so the old claim cannot be reconstructed from a
stale copy that outlived the correction.

What is deliberately NOT claimed anywhere: WHY an unchanged source is
not offered as a copy candidate. There is a plausible mechanism and it
was never established, and replacing one overreach with a smaller one
is how this class of error survives.

The root cause is worth recording: this claim entered the lane as a
dispatch instruction stated as settled fact, and the implementing agent
did exactly what it was asked --- measured one fixture. A measurement
cannot be broader than its fixture, however carefully it is run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 22:34:19 +02:00
Levi Neuwirth e94b256cc6
fix(git): say "copied" when a 2 record is a copy, not "renamed"
Porcelain v2's `2` record covers renames AND copies --- the `<Xscore>`
field leads with `R` or `C` --- and the diff header said "renamed from"
for either. A copied file was therefore reported to the user as a
rename, which is a different fact about their tree.

The information was already retained: `parse_status` captures `score`.
Nothing new is parsed; the header reads the byte it already has.

`kind` stays `"rename"` for both, deliberately. Every BEHAVIOUR keyed on
it is identical --- including the two-path `git diff HEAD -- <orig>
<current>`, which is right for a copy as much as for a rename. Splitting
the kind would oblige every consumer present and future to spell
`kind == "rename" or kind == "copy"`, and an arm forgotten anywhere
silently drops copies back to the one-path diff: the exact regression
this fix exists to avoid. The consumers are few and all were checked ---
`diff_plan` is the tree's only `kind == "rename"` branch,
`status_line_text` keys off `row.orig`, and the two tests that name the
kind are `g6_1`'s corpus and `g6_8`'s unborn-unreachability assertion.
`score` has no other reader anywhere.

Read from `score` rather than from `row.x`: the score names
rename-vs-copy whichever side detected the change, while `X` carries the
letter only for an index-side one.

The status ROW is UNCHANGED, and that is a decision rather than an
omission. Its `XY` prefix already reads `R.` against `C.`, out of the
same byte, in the porcelain vocabulary every other row in the panel is
read in --- so the distinction is already on screen, and a second
vocabulary beside it would be a wider surface for no new fact. `g6_4b`
asserts both prefixes so the claim is checked.

Unborn `HEAD` needs nothing, confirmed rather than assumed: `diff_plan`'s
rename branch sits inside `if not unborn`, and `g6_8` already pins that
no `2` record can occur there.

`g6_4b` is a parser/presentation test and says so. Real `git` emits no
`2 C` record --- the test MEASURES that under `-c status.renames=copies`
rather than recalling it --- so the copy row is supplied through
`_deliver_status`, the seam `g6_2b`/`g6_17`/`g6_21` already use.
Everything downstream is real: repository, panel, `d` dispatch, spawned
`git diff`, rendered buffer. Both crafted rows name paths that exist in
the fixture, so each drives a real two-path diff. Both classes are
asserted, and so is the argv --- a fix to what the user is TOLD must not
reach what the module DOES.

Mutations, each caught: header always "renamed" fails only the copy
half; header always "copied" fails only the rename half; dropping
`row.orig` from the steps fails the argv equality.

Gate: `scripts/gate --acceptance git_status_stage1_acceptance
--acceptance listview_acceptance --acceptance config_registry_acceptance`
--- all eleven steps green, acceptance 34/34.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 22:12:56 +02:00
Levi Neuwirth f53cf4f0fd
docs: record the macOS legs going green on the git Stage 1 lane
Run 31330601204 at `e816812`: all 14 jobs green, including both
`Test (macos-latest / …)` legs — the two that were deterministically red
on `g6_2`. The macOS half of the fix is therefore OBSERVED, not inferred.

What stays reasoned about is only the explanation — `EILSEQ` itself and
the `lstat`-vs-`ENOENT` argument for why `g6_2c` cannot be made portable
— which a green run can neither confirm nor refute. Kept separate on
purpose: conflating "the suite passes" with "the cause is understood" is
what put an unportable fixture in the suite in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 21:19:35 +02:00
Levi Neuwirth e816812d65
docs: record the macOS non-UTF-8 filename fact on the git Stage 1 lane
PR #227's CI round, in the lane entry: both macOS legs failed `g6_2`
deterministically while Linux stayed green, the fix at `4b82d1e`, and
the durable portability fact behind it — APFS/HFS+ validate pathnames as
UTF-8 and reject an invalid one with errno 92, EILSEQ, so a non-UTF-8
filename is a Linux-only fixture, and it cannot be reached around the
filesystem either because `git status` lstats every index entry and
EILSEQ is not ENOENT.

Also records the coverage split (parse+display and gesture refusal
everywhere; provenance Linux-only and loudly gated), the new
`lua_bytes`/`z_payload_bytes` fixture mechanism and its three-digit
escape rule, what was verified locally versus reasoned about, and the
latent sibling at tests/gpu_invocation_acceptance.rs:621 — which writes
a non-UTF-8 filename but sits behind `#[cfg(feature = "crdt")]`, and the
`crdt-test` job is ubuntu-only, so it is not red today and would be the
day that job gains a macOS leg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 21:02:13 +02:00
Levi Neuwirth 7e546aae78
docs: record PR #227's third review round on the git Stage 1 lane
The lane entry now carries round 3's single P2 --- a repository root
ending in a carriage return, truncated by round 2's own `\r?\n$` --- and
the tip moves to `39ad43d`. Section-local; nothing outside the Git
integration Stage 1 block is touched, and the P1a merge block is
unchanged and still the reason this lane cannot merge.

What is worth carrying beyond the fix itself is the `-z` finding, so the
next reader does not re-derive it: `git rev-parse` has no `-z` option on
git 2.55, and asking for one makes rev-parse echo a literal `-z` line
ahead of the toplevel at exit code 0. It was checked against the
installed git rather than assumed, which is the whole reason the fix is
a correct strip rather than a different output representation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 18:42:13 +02:00
Levi Neuwirth 93d557a527
docs: record the witness as closed, and what the audit found next door
Revision 5's ledger edits were written while the witness was still the
open blocker, and landed alongside the commit that closed it --- so the
entry asserted both at once: an OPEN BLOCKER bullet saying this lane
"currently ships without the regression guard it was created to
provide", and, further down, that same gap closed at 677fd25. A
recovering machine reads the top of an entry first, so the stale half
is the half that gets acted on. Reconciled in place: the heading, the
framing bullet and the blocker bullet now say re-opened by review and
CLOSED at 677fd25, and point at the bullet that closed it.

The script header cited framing revision 4; it is revision 5.

Also recorded, from auditing whether any OTHER assertion in that suite
is detached from the thing it names: renaming every other plan step ---
fmt, clippy, lib, m4, gpu, sweep, diff-check, acceptance-<suite> ---
leaves all 20 tests green. For most that is only a log filename and a
FAILED: entry. `sweep` is not: the runner's end-of-run listing globs
*-sweep.log and *-sweep-crdt.log, so renaming that step silently
empties the "read these, do not re-run and grep" listing that is the
U2/U3 remedy, with the suite still green. Left open deliberately and
said so --- that listing exists only on the RUN path, and every test in
this file is no-gates by design, so there is no cheap witness for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 18:18:34 +02:00
Levi Neuwirth 74d735ff8d
docs: gate framing revision 5 --- pin both emitter pairs, correct print-plan
Revision 4 said it would close the rename hole for the sweep as well as
the build, and then did not: its verification bullet required only a
singular real-plan pair in the context of build-crdt. The hole is
symmetric --- renaming sweep-crdt slips through exactly the same gap
--- so section 7 now pins BOTH emitter pairs explicitly, name and exact
command, asserted from the emitter where the name still exists.

Two older bullets also still claimed named steps appear in
--print-plan. They do not; that mode prints commands only, which is the
wording that let the attribution witness drift away from the step it
names in the first place. --print-plan is now described as the command
and order witness, and nothing more.

The ledger recorded revision 4 as approved and implemented, and
presented the synthetic self-test as the attribution witness. Both were
read as done. A recovering machine or a PR preparation from that entry
would have shipped this lane without the regression guard it exists to
provide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 18:11:14 +02:00
Levi Neuwirth 48120f2a80
docs: gate framing revision 4 --- the witness does not reach the step
Review mutated the REAL build step name to sweep-crdt and both existing
witnesses still passed. That is the gap: --print-plan strips names
before printing, so the order assertion sees only commands, and
--self-test hardcodes build-crdt inside its own synthetic plan. Neither
witness is connected to the step it claims to describe, so this lane
shipped without the regression guard it exists to provide.

Section 7 now requires asserting the real emitter (name, command) pair
together, so a rename cannot pass. The synthetic failure and
continuation test stays --- it tests the runner, which is a different
thing --- but it can no longer stand in for attribution of the actual
step.

Also fixed the header, which read "Pre-implementation. Awaiting
approval" through three revisions while the ledger recorded this lane
as approved and implemented. That is the same contradiction class this
project keeps correcting elsewhere, left standing in the document that
keeps correcting it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 17:55:13 +02:00
Levi Neuwirth 49bc141500
docs: record the acceptance run, and the red that came with it
The acceptance criterion is witnessed: `scripts/gate --acceptance
gate_script_acceptance --protocol` on a target root that did not exist
beforehand goes green in all eleven steps, with `09 build-crdt ok`
producing `debug/pmacs-gpu` and `gpu_invocation_acceptance` at 15
passed / 0 failed where the same suite is 3 / 12 without the build
step. No manual build anywhere, which is the thing that was false.

An EARLIER attempt at that same cold run went red, and it is recorded
rather than dropped once a later run was green. Fifty failures across
m5_5/m5_6/m5_7/m5_8 --- all real-daemon suites --- with the signature
"daemon exited with exit status: 101 before socket appeared;
socket=/tmp/.tmpXXXX/pmacs.sock" and an EMPTY daemon stderr. Not the
pmacs-gpu signature, and no row in docs/ci-red-signatures.md matches
it.

Re-running the same test binary from the same target directory gave
36/36. Per that registry's own rule a green rerun establishes
INTERMITTENCE ONLY, never environmental cause, so this is left open
rather than blamed on the load it happened under.

What DOES rule out this lane's change is a construction argument, not
the rerun: the root crate declares `default = ["luajit"]`, so
`--no-default-features --features luajit,crdt` enables exactly the same
feature set as the sweep's `--features crdt`. `build-crdt` cannot hand
the sweep a differently-featured binary, so it has no mechanism by
which to break a daemon suite.

Also records that the new assertions were mutation tested --- wrong
features, wrong position, unconditional emission, an aborting runner,
and the build folded into `sweep-crdt` each fail the suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 17:15:56 +02:00
Levi Neuwirth 7fe32e43f6
docs: record round 2's third P2 and the async-continuation census
The diff channel's missing ticket, and --- because this was the fourth
recurrence of one shape --- a census of every async continuation in
`git.lua` rather than another instance-by-instance note. Three
continuations, one dispatcher, one synchronous impostor, each with
whether it carries an invocation-time ticket, whether it needs one, and
what shared state it writes.

Also states why the fix is two channels sharing one mechanism rather
than one shared counter: a single counter would make `d` cancel an
in-flight `g`. And records that `state.diff_buffer` is deliberately
still read at continuation time --- "do I already have a live diff
buffer?" is a question about now, not about the invocation --- so it is
not a fifth instance.

P1a's citations re-pointed at `723afa7` and the untouched claim
tightened from "no diff line reaches these names" to something
checkable: `show_diff_buffer`'s body and `open_status_panel`'s
`listview.open` are byte-identical to `4002734`, and no commit on this
branch adds a `commit_to` call anywhere.

Section-local: nothing outside this lane's entry is touched or
reflowed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 16:50:11 +02:00
Levi Neuwirth 898a98120c
docs: one normative home for the crdt sweep's build precondition
The durable half of this lane is a boundary question, not the missing
line. `scripts/gate`'s header names handoff section 3 as the owner of
its reasoning, and this precondition lived in section 5's hazard
register --- a coherent cause for the omission rather than mere
oversight. A requirement the script was never told to encode is one it
will keep not encoding.

So section 3 gains it NORMATIVELY: the build joins the protocol-bump
block as a third line, with its own load-bearing bullet covering the
mechanism (pmacs-gpu has no tests/ directory, so cargo never uplifts
its bin), the measurement that makes it conditional, and why it was
latent until per-worktree target directories stopped hiding it.

Section 5 keeps the INCIDENT and its signature, which is history rather
than contract, and now says so: twelve
`gpu_invocation_acceptance::crdt::*` failures on a target directory
with no `debug/pmacs-gpu`, first seen on PR #228's first gate run.
Recast so that seeing the signature again reads as "the script was
bypassed", not "the requirement moved".

The script's header keeps citing section 3 and ONLY section 3. Citing
both would split one executable contract across two homes and weaken
the script's only clean boundary at the same time as Q#GR-4 declines to
build any automated check for prose drift. A boundary that is neither
enforced nor singular is not a boundary.

The ledger records Q#GR-1's observed answer rather than the question:
both sweeps run alone from the same cold disposable target with
`debug/pmacs-gpu` asserted absent beforehand --- default exit 0 with
the binary still absent afterwards, crdt exit 101 with exactly twelve
failures --- plus the silent-skip finding, which is the part nobody was
looking for: a54 reported `ok` in that cold crdt sweep because its only
non-spawning path is its skip branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 16:39:35 +02:00
Levi Neuwirth afe79bd7dc
docs: record PR #227's second review round on the git Stage 1 lane
Two more P2s, both the same shape as round 1's P1 --- module-level
mutable state read at continuation time instead of captured at
invocation time --- plus the third instance of that shape, which is
still open and which no review finding covers: the diff path has no
generation counter at all, so two `d` presses in flight together are
last-writer-wins on the single `*git-diff*` buffer.

Also corrects a citation round 1 got wrong. The P1a block names two
lines that must not be touched, and its second one (`:854`) pointed at
`local unstaged = …` inside `diff_plan`, not at a display call. The site
was always `show_diff_buffer`'s `pmacs.window.display`. A stale pointer
in a block whose entire purpose is "leave these alone" is worse than
none, so it is corrected rather than silently re-numbered.

Section-local: nothing outside this lane's entry is touched or
reflowed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 16:35:38 +02:00
Levi Neuwirth 12affd78e1
docs: gate framing revision 3 --- a witness that can fail, and the ledger
Two findings, both about a claim that could not be falsified.

The --self-test plan put the failing step last. With the failure last,
a runner that ABORTS on failure and one that CONTINUES produce
identical output, so the witness for Q#GR-2 policy --- the suite keeps
going --- would have passed on a runner doing the exact opposite. The
plan is now three lines with a passing SENTINEL after build-crdt,
asserted to have written its own log. That is the only thing that
distinguishes the two behaviours, and it turns Q#GR-2 from a declared
policy into an observed one.

The plan test also now pins the EXACT command, not only the step name
and its position. A build-crdt running plain cargo build would leave
the gate exactly as unsound while looking repaired --- the crdt sweep
needs those specific features, which is the whole defect.

The ledger still recorded the superseded boundary decision: "section 3
gains it, section 5 keeps the incident, and the script cites both".
Revision 2 replaced that with section 3 as the sole normative home and
the script citing section 3 alone. active-work.md is the volatile
cross-machine record, so a recovering machine reading the stale entry
would have rebuilt revision 1 wrong boundary. Now updated, and it says
which decision it supersedes rather than silently replacing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 16:10:09 +02:00
Levi Neuwirth a70ee5fdc0
docs: record review round 1 and the P1a block on the #227 lane
The lane said "normal review; no merge authorization", which is no
longer the state: two of three blockers are fixed and the third makes
the PR merge-blocked behind the destination-capture lane.

P1a is recorded as deliberately NOT fixed rather than outstanding.
commit_to is the right mechanism and is not Lua-reachable outside a
directory open --- DirectoryDestinationLua is nonconstructible by
design and minted only in the path.open-directory dispatch --- so the
fix is a prerequisite lane and this one adopts it afterwards. Recorded
with the mechanical check that no diff line in either fix reaches the
four named symbols, so a later reader does not have to take it on
trust.

The P2 entry keeps the reasoning for teardown over a canonicalized
preflight, because the rejected option is the one that looks obviously
better: a Lua canonicalizer would be a second copy of the Rust alias
table and would go stale the day that table gains a name,
reintroducing this exact bug for the new alias. Keymap::bind is the
authority because it is what decides. Also recorded: there is no
Lua-reachable canonicalization to build on, verified, and no binding
was added to invent one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 16:01:58 +02:00
Levi Neuwirth 4e02cb09b4
docs: gate framing revision 2 --- one normative home, a real witness
Three review findings.

The normative build requirement goes entirely into handoff section 3.
Revision 1 proposed section 3 gaining it while the script header cited
both sections, which splits one executable contract across two homes
and weakens the single clean boundary the script has --- at the same
time as Q#GR-4 declines to build any automated check for prose drift. A
boundary that is neither enforced nor singular is not a boundary.
Section 5 keeps the incident and its signature, which is history rather
than contract.

Q#GR-1 observation procedure was unsafe and insufficient. "Delete
pmacs-gpu from a target directory" mutates a live worktree build
directory, and removing one binary does not establish that the other
artifacts and feature permutations are cold --- a stale dependency
graph can satisfy the run for reasons the experiment never sees. Now: a
disposable target, the binary asserted ABSENT before each run as a
recorded precondition, and the two sweeps run separately so neither can
be explained by the other having built the binary first. That last
point is the same accident that hid this defect for the whole life of
the shared target dir.

The attribution criterion had no feasible witness. gate_script_acceptance
deliberately runs no gates, so plan assertions prove name and order and
nothing about runtime behaviour. The obvious seam is a trap: making
PLAN_FILE injectable would turn the script into a general command
executor through its runner eval --- the same class of defect this
script own review already caught in --acceptance and fixed with a
parse-time refusal. Reintroducing it one lane later, in the tool whose
purpose is to be trustworthy, is not a trade worth making.

Q#GR-5 proposes --self-test over a HARDCODED two-line synthetic plan,
true and false, with the failing one named build-crdt. No injection,
no real gate, and it tests the thing actually under test: whether the
runner names the right gate when a command fails. Whether cargo build
really fails is cargo business.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 15:57:47 +02:00
Levi Neuwirth 01316cbc74
docs: frame the scripts/gate --protocol build step (revision 1)
--protocol promises the CRDT workspace sweep. That sweep documented
precondition is cargo build --workspace --no-default-features
--features luajit,crdt (handoff section 5:532-535), and the plan
emitter at scripts/gate:187-204 has no build step at all --- read from
the source, not inferred from the failure.

The interesting part is why it stayed invisible. Before #225 every
worktree on this machine resolved to one shared CARGO_TARGET_DIR, which
almost always already contained a pmacs-gpu binary, so the precondition
was satisfied by accident on essentially every run. Per-worktree target
dirs start empty. So this is not a bug #225 introduced; it is a
pre-existing gap in the documented procedure that #225 stopped hiding.

That also decides the urgency. A red gate is fine --- it stops you. The
hazard is the reverse: a GREEN --protocol run whose crdt sweep was
decided by what happened to be in the build directory rather than by
the diff. A gate reporting coverage it does not have is exactly what
#225 exists to prevent, so the tool shipping with this gap teaches the
opposite of what it is for.

Observed on PR #228 first gate run: twelve
gpu_invocation_acceptance::crdt::* failures, all "build pmacs-gpu
before this acceptance suite", with debug/pmacs-gpu absent from the
fresh target dir.

The durable half is a boundary question rather than a missing line. The
script header names handoff section 3 as the owner of its reasoning,
and this precondition lives in section 5 --- a coherent cause for the
omission, not oversight. Q#GR-3 proposes section 3 gains it, section 5
keeps the incident and its signature, and the script stops naming
section 3 as its only source.

Q#GR-1 is marked as the one thing this lane will not accept on
reasoning: whether the default sweep also needs the binary must be
established by deleting it and running both sweeps. The whole defect is
a precondition nobody checked, and establishing its replacement by
reading would repeat the error at one remove. The mechanism section
states its own inference (the failing tests are namespaced ::crdt:: and
so are probably feature-gated) and marks it unverified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 15:22:04 +02:00
Levi Neuwirth 0aee97b725
docs: record PR #227 on the git Stage 1 lane
The lane heading still said the PR was not opened. Per the standing
correction from #171 and #215, the PR number belongs in the ledger when
the PR exists, not when review asks for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 15:19:43 +02:00
Levi Neuwirth 40027340df
feat(git): Stage 1 --- *git-status* and *git-diff*, no wire change
Implements docs/git-integration-framing.md revision 5 (approved
2026-08-09). COHERENCE.md section 15's largest named gap gets something
to attach to: a user can now answer "what have I changed?" without
leaving the editor.

  *git-status*  a `pmacs.listview` panel over
                `git --no-optional-locks -C <root> status
                 --porcelain=v2 --branch -z`. RET visits the file, `d`
                shows its diff, `g` refreshes.
  *git-diff*    the file-level diff, in a generated buffer rendered as
                plain text --- there is no bundled `diff` grammar and no
                hunk model anywhere in the tree.

NO WIRE CHANGE: no pmacs-protocol edit, no PROTOCOL_VERSION bump, no
DecorationKind variant. That is load-bearing for scheduling, not a
coincidence --- gutter markers (Stage 2) need all three and must be
scheduled alone, while this lane could run beside another.

One additive `listview` change, and the framing was wrong to say there
would be none: an optional `keys` table on the open spec. `d` cannot be
bound from outside the primitive safely, because a name collision
disambiguates to `<2>` and the name a consumer passed is not necessarily
the buffer it got. Keys are INSTALLED ONCE with the panel's buffer and
COMPARED on reopen: `Keymap::bind` refuses duplicates, and the async
completion model re-opens on every refresh, so a naive implementation
would have errored on every successful refresh.

One config-registry setting, `git.enabled`, through `pmacs.config.define`.

Facts measured against real git rather than reasoned about, each pinned
by a test:

* `ProjectKind::Git` means a BARE repository, and a language marker
  beside `.git` wins --- so pmacs reports `kind = "rust"` for its own
  repository. This module never asks pmacs whether something is a repo;
  it runs `rev-parse --show-toplevel` and lets a non-zero exit answer.
* `git diff --no-index` implies `--exit-code`: exit 1 means it
  SUCCESSFULLY found differences. The untracked predicate is exit in
  {0,1}; only >= 2 is failure. Under the naive predicate every untracked
  diff --- the case `--no-index` exists for --- would render a failure.
* An unborn HEAD makes `git diff HEAD` exit 128. Detected from
  `# branch.oid (initial)` in output already being parsed, never from a
  second `rev-parse`. `AM`/`AD` carry both states and get two labelled
  patches; rename/copy is asserted UNREACHABLE, because `git mv` on a
  staged-but-uncommitted file yields `1 A.`, not a `2` record.
* Under `-z` a rename's origin is the NEXT NUL-terminated field, not a
  tab-joined suffix, so the record tokenizer is new rather than ported
  from `tests/fixtures/pmacs-magit/`. What ports is that fixture's
  SEPARATION --- pure `parse_*` over a string --- and its case coverage.
  The fixture is untouched: it exists to prove the package system can
  host this, and bundled code becoming its dependency would make
  `m8_6_acceptance` test less than it claims.

Coherence impact, stated per CLAUDE.md:

* Section 14: `*git-status*` is the FIFTH `listview` call site and the
  first outside `lsp.lua` --- the evidence P5 asked for that the
  primitive generalizes past its first consumer.
* Section 6: no new interaction island. `d` is an ordinary buffer-local
  binding through the primitive's own path, so `describe-key` reports
  the truth and `init.lua` can rebind it. The count stays at six.
* Section 9: NEGATIVE, and named as such. A spawned process does not
  appear in `*workers*` --- that view is `async.lua`'s job list. This
  adds a fifth background thing with no single place to see it. Every
  spawn is labelled, which is better than anonymous, but a label is not
  attribution. Accepted only because these are short-lived reads.
* Journey: no step added. Git is not a journey step and this does not
  make it one.

Section 15's "no Git integration at all ... anywhere in the tree" is
narrowed here. It was literally false when written ---
`tests/fixtures/pmacs-magit/` is a tracked, installable package that
spawns git and parses porcelain v2 --- and the product gap it described
is what this closes.

Five things found by biting the suite rather than by reading, recorded
in docs/active-work.md: `listview.open`'s `seat_cursor` walks DOWN from
wherever the cursor is (so a re-opened panel lands one row low, and the
completion handler seats unconditionally from line 0); a selection test
that inserts ONE row above the selection is vacuous against exactly that
off-by-one; `{:?}` on a Rust string cannot build a `-z` fixture, because
Lua's decimal escape swallows the digit after `\0` --- which made one
test pass while parsing nothing; a path may contain a newline, so rows
escape it; and untracked rows sort after every tracked row.

Gates: scripts/gate --acceptance git_status_stage1_acceptance
--acceptance listview_acceptance --acceptance config_registry_acceptance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 14:43:21 +02:00
Levi Neuwirth 2d2d63abfc
docs: mark the git Stage 1 framing approved
The lane in docs/active-work.md already recorded revision 5 as APPROVED
2026-08-09; the framing document itself still opened with "Awaiting
approval". Same fact, two files, opposite answers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 14:01:03 +02:00
Levi Neuwirth 9567c0e09e
docs: frame git integration Stage 1 (revision 5, approved)
COHERENCE.md section 15 grades contextual affordances "weak" and says
the Git affordance list "has nothing to attach to yet". For a daily
driver this is the largest remaining gap --- not the deepest (sections 7
and 9 are), but the one a user touches every working hour.

Stage 1 is read-only and panel-based: a `*git-status*` listview over
`--porcelain=v2 --branch -z`, and a file-level `*git-diff*` in plain
generated text. THE STAGE LINE FALLS AT THE WIRE, and that is a
scheduling decision as much as a design one: `DecorationKind` is a
closed enum, so gutter markers need new variants and a
PROTOCOL_VERSION bump. Bumps are a strict serialization point --- this
session recorded eight broken assertions from one --- so Stage 1
touching no wire is what lets it run beside other lanes, and Stage 2
must be scheduled alone.

FOUR REVIEW ROUNDS, and the doc records what each one caught, because
the pattern is the useful part:

  - `ProjectKind::Git` means a BARE git repo; a language marker beside
    `.git` wins, so this very repository reports "rust". A `kind ==
    "git"` gate would have failed on the repo it was written in. The
    rule is now: never ask pmacs whether it is a git repo --- run git
    and let it resolve its own worktree.
  - `tests/fixtures/pmacs-magit/` already exists: 1,914 lines, a
    porcelain-v2 parser, 32 tests. Section 15's "no Git integration
    anywhere in the tree" is literally false; the PRODUCT gap is real.
    The tokenizer is deliberately REWRITTEN for `-z` rather than
    ported --- newline-delimited and NUL-delimited v2 are different
    grammars.
  - `listview.open` resets collapse and always seats line 1, so
    selection preservation is the consumer's job, not the primitive's.
    And `d` is not on its key surface; binding it needs an additive
    `keys` table, which makes "no listview modification" false.
  - `Keymap::bind` REFUSES duplicates, and the refresh path re-opens
    the panel --- so a naive `keys` implementation would have failed on
    every successful refresh.

Two git exit states were measured, not assumed. `--no-index` implies
`--exit-code`, so an untracked diff exits 1 ON SUCCESS --- under the
first predicate, every untracked diff would have rendered a failure row
instead of the diff it had just produced. And `git diff HEAD` exits 128
in an unborn repository, which is exactly a fresh `git init` with the
first files staged.

The unborn policy was then enumerated from a real unborn repository
rather than reasoned about, which closed one case by RULING IT OUT: a
`git mv` of a staged-but-uncommitted file emits `1 A.`, never a `2`
record, so rename/copy is unreachable without a HEAD and needs no
policy. `AM` and `AD` are ordinary there and carry both states, so they
render TWO labelled patches --- `--cached` alone loses the worktree
edit, plain `git diff` alone loses the staged base. The split is
unborn-only: with a HEAD, one total is the question this lane asks.

Section 9 impact is recorded as NEGATIVE and not dressed up: spawned
processes do not appear in `*workers*`, so this adds a fifth
unattributable background thing. The process is labelled; a label is
not attribution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 13:25:52 +02:00
Levi Neuwirth 4bc55e8dd2
build: scripts/gate — a target dir per worktree, and one gate suite (#225)
* build: scripts/gate — a target dir per worktree, and one gate suite

Parallel worktrees do not work on this machine, and the reason is one
exported variable: every checkout builds into one CARGO_TARGET_DIR, and
cargo takes an EXCLUSIVE LOCK on it. Two lanes building at once do not
run in parallel — the second blocks — and they invalidate each other's
artifacts, so alternating between them recompiles from scratch. Parallel
development under that arrangement is slower than serial.

MEASURED, BECAUSE THE FIRST PLAN WAS WRONG. The shared directory is
285G, which drove a proposal to add sccache so per-worktree directories
would not lose artifact sharing. That number is years of accumulation
across TWO projects (pmacs and levcs share it). Measured directly: a
cold `cargo test --workspace --no-run` is 80s and 19G. And sccache
across two target directories hits 50% on C/C++ and **0.00% on Rust** —
rlibs embed their target-dir path, so dependency artifacts are not
bit-identical between directories and `--extern` hashes cascade into
misses. There is no sharing worth buying back. sccache stays configured
and earns its keep on C/C++; it is not what makes parallel lanes work.

The script also owns the FIXED gates, because a procedure living only in
prose gets executed differently each time — twice in the session that
motivated this:

  - a sweep run with `--tests` instead of `--workspace`, silently
    dropping pmacs_protocol and pmacs_gpu, including protocol tests that
    same lane had just written;
  - a sweep piped through `grep` before anyone read it, so an
    intermittent red could not be matched against ci-red-signatures —
    a row needs its fragments. That is registry note U2, and then U3
    when it happened AGAIN.

Hence durable per-gate logs with the sweep paths printed. The remedy is
real: this lane's own run diagnosed its failures from the log without
re-running anything.

WHAT THE SCRIPT IS NOT AUTHORITATIVE FOR. Handoff §3 keeps policy and
keeps CHOOSING the touched acceptance suites, which arrive only via
`--acceptance`. No script can infer those from a working tree, and one
that guessed would report coverage it does not have.

THREE HAZARDS SPECIFIED RATHER THAN LEFT TO CHANCE:

  - `cmd | tee log` reports TEE's status, so a failing gate would exit 0
    and the suite would read green. `pipefail` is not POSIX.
  - `cmd > log; rc=$?` never reaches the assignment under `set -eu`
    (which scripts/bite already uses) — the shell exits at the failing
    command, so nothing prints which gate failed or where its log is,
    destroying the point of capturing it. The runner is therefore an
    `if` condition, the only `set -e` exemption.
  - CARGO_TARGET_DIR (env) OVERRIDES build.target-dir in config.toml, so
    a per-worktree config file silently does nothing. Only a
    per-invocation value beats it.

Pruning is dry-run by default, `--force` to delete, and refuses any
directory without a `.pmacs-gate-target` marker. "Live" means a git
worktree record carrying NO `prunable` line — git keeps listing a
worktree whose directory was deleted without `git worktree remove`, and
treating listed as live would make exactly the reclaimable directories
permanently ineligible.

ONE HONEST FINDING FROM MUTATION TESTING. Three mutations came back
vacuous, and all three are redundant defences rather than test holes:
git already returns resolved physical paths from both
`rev-parse --show-toplevel` and `worktree list --porcelain`, so canon()
is belt-and-braces; and the prune path guards the marker twice. Recorded
in the script and the tests so a later reader does not mistake a
"vacuous" result for a gap — or delete a defence because a test did not
notice.

VERIFICATION. 11 acceptance tests over the no-gates paths (running the
script for real inside the suite would recurse), each pointed at a
tempdir via PMACS_GATE_TARGET_ROOT so the real managed root is
unreachable — a prune bug is unrecoverable. Mutation-tested: `--tests`
in the sweep, an unconditional CRDT sweep, and pruning on a dry run all
fail their intended test.

Observed in a real run, which is how the framing said to confirm the
parts a test cannot: the failed-gate names and log paths print, the
ambient directory is created and reaped by the exit trap, and every log
appears. The run exits non-zero because of R8 — the pre-existing,
merge-base-confirmed listview failure — which means `scripts/gate`
cannot go green on this machine until R8 is diagnosed. That is a
property of the tree, not of this change.

Framing: docs/gate-script-framing.md (revision 4, approved).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* fix(gate): two ways the script could do harm, and four smaller defects

Review round 1 on #225. Neither blocking finding was a design gap ---
both were the implementation failing to honour its own framing, which
is the case a framing document cannot prevent by itself.

PRUNE COULD DELETE EVERY MANAGED DIRECTORY. §2.6 requires the live
worktree set to be ESTABLISHED. The code piped `git worktree list`
straight into awk and the caller masked the result with `|| true`, so
running from outside any repository produced an EMPTY live set --- and
an empty live set means "every managed directory is an orphan", so
`--prune --force` would have deleted all of them, live lanes' artifacts
included. The failure mode was silent and total.

Two refusals now, and they are deliberately redundant: not inside a
worktree, and the enumeration itself failing. `live_worktrees` captures
git's output and returns non-zero rather than emitting nothing, so
"I cannot tell what is live" is unrepresentable as "nothing is live".
An empty porcelain listing counts as failure too --- a repository always
has at least its own worktree.

--ACCEPTANCE WAS SHELL-INJECTABLE. The name is interpolated into a
command the runner evaluates, and nothing validated it, so
`--acceptance 'x; rm -rf ~'` would have run. Now an allowlist of what a
cargo test target can actually be named --- letters, digits, underscore,
hyphen --- refused at parse time, before any gate. Rejection rather than
escaping: there is no legitimate suite name that needs quoting.

FOUR SMALLER ONES:

  - Log directories carried a whole-second timestamp, so two runs in the
    same worktree within one second shared one and could overwrite each
    other's evidence --- reintroducing U2/U3 through a naming choice.
    The PID is now part of the name.
  - The ownership marker is DOCUMENTED as one line, so it is enforced as
    one line instead of read head-first. Acting on the first line of a
    file we did not understand is how a corrupted marker authorises a
    deletion.
  - The `prunable` test returned green when `git worktree add` failed,
    so the only coverage of that rule could silently never run. It now
    fails loudly.
  - Its cleanup ran after the assertions, so a panicking assertion would
    have left the real repository carrying a stale worktree record. Now
    a `Drop` guard.

MUTATION TESTING, HONESTLY REPORTED. The injection and marker fixes bite
individually. The two prune guards do NOT --- each alone satisfies the
outside-repo test, so mutating one at a time reads as vacuous. Removing
BOTH fails the test, which is what establishes that the test detects the
unsafe state rather than being blind to it. Recorded in the test so a
later reader does not delete one guard on the grounds that nothing
noticed.

ALSO: handoff §3's ambient-root caveat still said "until the
ambient-root isolation lane lands". #206 merged; the five variables are
now belt-and-braces for external and integration paths, and `scripts/gate`
sets them regardless.

R8 PROMOTED. `docs/ci-red-signatures.md` gains the reason it stops being
a catalogued curiosity: with the gate suite reduced to one command, R8
makes that command exit non-zero on a clean tree EVERY TIME, and a gate
that is always red is a gate nobody reads. `docs/active-work.md` gains a
lane. It is still not a regression from #223 or #225 --- the merge-base
control says so --- and the lane's first job is diagnosis, because a
change that made the assertion pass without explaining the prefix strip
would convert a visible failure into an invisible one.

15 acceptance tests. Observed run re-confirmed: failed gates named with
log paths, ambient directory created and reaped, distinct log directory,
exit 1 from R8 alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* docs: the #225 lane, and R8 diagnosed to a stray /tmp/.git

TWO LEDGER GAPS, both found by review.

and — the part that matters — an explicit GATE STATUS: NOT GREEN
section. `scripts/gate` exits 1 on this branch and on a clean `main`
because R8 fails m4_acceptance and therefore the sweep. That is a merge
blocker under the standing rule, and #225 is the worst possible lane to
grant a silent exception to: it is the lane that makes the gate suite
authoritative, and a tool shipping with its own gate red teaches the
opposite of what it exists to teach.

The lane also records that it was written after the PR existed, again,
because review asked again. Two lanes in a row now. The correction from
only evidence of that.

R8 DIAGNOSED, and the `TMPDIR` hypothesis was right:

  1. `display_path` (builtin/runtime/lsp.lua:2397) shortens a location
     against the DETECTED PROJECT ROOT before rendering it.
  2. `project.detect` walks UPWARD for a marker; from
     /tmp/.tmpXXXX/r.rs it reaches /tmp.
  3. This machine has a stray `/tmp/.git` — an EMPTY DIRECTORY, not a
     repository. The `.git` marker is directory-only, so an empty
     directory still matches.
  4. Root resolves to /tmp, the prefix is stripped, and the rendered row
     is exactly the observed `.tmpXXXXXX/r.rs:12:3`.

Controlled, not inferred: the same test with TMPDIR outside /tmp PASSES.

THE CODEBASE ANTICIPATED THIS BY NAME. src/project.rs:208 documents
`detect_project_within(start, markers, stop_root)` as existing "so a
stray marker in a temp-dir's ancestor (e.g. a developer's /tmp/.git)
can't leak into a fixture that lives below it." The mechanism exists;
this fixture does not use it.

So the row splits, and the halves need different fixes. The failure is
ENVIRONMENTAL — nothing about pmacs is wrong when a real project root
sits above a file, that is the feature, and removing /tmp/.git makes the
gate green immediately. The fixture being ENVIRONMENT-DEPENDENT is a
real defect, and bounding its detection is what retires the row.

PROVENANCE UNRESOLVED, and I am not going to assume in my own favour:
/tmp/.git is dated 2026-08-07 23:17, inside this session's window, and
may have been created by this session's own work — a stray git
invocation from /tmp would do it. The earlier merge-base control stays
valid as "this tree has it" but says nothing about WHEN the environment
acquired the marker, so "pre-existing" must not be read as
"long-standing".

Nothing deleted: /tmp/.git is outside the repository and I cannot
confirm I created it, so removing it is the user's call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* docs: rebase onto the R8 fix; scripts/gate now exits 0

#226 (`dcb852e`) retired R8 by bounding the LSP fixture's project
detection. This branch rebases onto it, and the thing that was blocked
is now demonstrable: **`scripts/gate` exits 0** --- all nine gates green
in one command, the first time the tool has passed the suite it exists
to run. That is #225's own acceptance criterion, and it could not even
be stated while the script did not exist on `main`.

REBASE RESOLUTION, per the standing rule that #226's R8 documentation is
authoritative. Every conflict was in R8 text this branch wrote while the
row was still an open investigation:

  - two in `docs/ci-red-signatures.md`, both resolved to #226's retired
    row with this branch's pre-fix copy dropped;
  - the framing-doc pair --- e71e1bd added `docs/r8-fixture-boundary-
    framing.md`, 7cfba73 removed it --- both SKIPPED. They are net-zero
    here and `main` owns that file authoritatively; replaying the second
    would have deleted `main`'s copy, which is the one failure mode a
    mechanical "resolve each conflict in turn" would have walked into.

TWO STALE LANES REMOVED. This branch's "R8 --- NEEDS A LANE"
investigation block describes a diagnosis that has since happened and a
fix that has since landed. And #226's own lane arrived through the
rebase still saying "OPEN, HELD FOR REVIEW"; Rule 4 retires it now that
it has merged, its durable facts already being in the retired registry
row and the handoff section 6 census. Leaving either would have left the
ledger asserting that a merged fix was still an open investigation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 09:43:33 +00:00
Levi Neuwirth dcb852e740
fix(tests): bound the LSP fixture's project detection — retires R8 (#226)
* docs: frame the R8 fixture-boundary fix (revision 2)

R8 fails m4_acceptance deterministically on one machine, and the
diagnosis is that the fixture never bounds its own project detection.

`display_path` (builtin/runtime/lsp.lua:2397) shortens a location
against the DETECTED PROJECT ROOT. `pmacs.project.detect` walks upward
for a marker; from /tmp/.tmpXXXX/r.rs it reaches /tmp, where this
machine has a stray EMPTY `.git` directory. The `.git` marker is
directory-only, so an empty directory matches, the root resolves to
/tmp, and the prefix is stripped.

THE PRODUCT BEHAVIOUR IS CORRECT AND IS NOT CHANGING. Shortening a
location against its project root is the feature. The defect is that
the fixture's assertion depends on whether the developer's /tmp happens
to contain a `.git`.

THE MECHANISM ALREADY EXISTS AND THIS SUITE ALREADY USES IT.
`src/project.rs:208` documents `detect_project_within(.., stop_root)` as
existing "so a stray marker in a temp-dir's ancestor (e.g. a
developer's /tmp/.git) can't leak into a fixture that lives below it."
It is exposed to Lua as `pmacs.project.set_search_boundary`; eight test
files make fourteen real calls to it, five of them in m4_acceptance
itself --- one carrying that same hazard as a comment. `open_against_fake`
(tests/m4_acceptance.rs:7985) is one helper that missed the pattern.

THE WITNESS PLANTS ITS OWN HAZARD, so the proof is not a property of
this machine: an empty `.git` in a temporary ancestor, the file one
level below, boundary at the file's parent. With the boundary the row
renders absolute; reverting it strips the prefix deterministically on
every machine, including CI where /tmp/.git does not exist. The
/tmp/.git observation stays as corroboration, not as the bite.

`scripts/gate` is deliberately NOT a criterion: this lane branches from
main, where that script does not exist (it is unmerged on #225). Naming
it would make this lane depend on an artifact absent from its own base.
R8 lands first on its own merits; #225 then rebases and takes "gate runs
green" as ITS criterion.

Q#R8-1 records a limitation rather than discovering it later:
parent-as-boundary is correct only while fixtures put the file as a
direct child of the fixture root. A future nested fixture cannot fix
itself by passing a deeper path --- the boundary is DERIVED from the
parent, so a deeper path clamps sooner, never later.

Provenance of /tmp/.git is left permanently unresolved, and the document
says why no timestamp is treated as authoritative.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* fix(tests): bound the LSP fixture's project detection — retires R8

`open_against_fake` never set a search boundary, so the panel tests'
rendered paths were shortened against whatever project root detection
found ABOVE their tempdir. On a machine with a stray `/tmp/.git` that
meant `/tmp` --- and the assertion that spells a path out failed
deterministically. Registry row R8.

THE PRODUCT BEHAVIOUR WAS NEVER WRONG AND IS NOT CHANGED. Shortening a
location against its project root is the feature; a file that really is
inside a project really should render relative to it. What was wrong is
that a fixture's assertion depended on the contents of the developer's
/tmp.

THE MECHANISM WAS ALREADY THERE. `src/project.rs:208` documents
`detect_project_within(.., stop_root)` as existing "so a stray marker in
a temp-dir's ancestor (e.g. a developer's /tmp/.git) can't leak into a
fixture that lives below it" --- naming this exact hazard. It is exposed
to Lua as `pmacs.project.set_search_boundary`, eight test files make
fourteen real calls to it, and five of those are in this same file, one
carrying that hazard as a comment. This was one helper that missed a
pattern its own file already used.

THE WITNESS PLANTS ITS OWN HAZARD, so the proof is not a property of one
machine. `a_planted_ancestor_marker_does_not_reach_the_rendered_row`
creates an empty `.git` in a temporary ancestor with the file one level
below, and asserts the row stays absolute. Reverting the boundary fails
it with `proj/r.rs:12:3` --- relative to the PLANTED marker, not to
/tmp, because the nearer ancestor wins. That is what makes it bite in
CI, where no /tmp/.git exists; confirmed by also running it with TMPDIR
outside /tmp.

Resting the bite on /tmp/.git would have been the same mistake as a test
that passes only where the developer happens to be standing.

/tmp/.git IS DELIBERATELY LEFT IN PLACE. Deleting it would hide the
hermeticity defect rather than fix it, its provenance is unresolved, and
it is the only thing on this machine that reproduces the row --- which
makes it useful, not merely untouchable. The R8 fix is verified WITH it
present.

VERIFICATION. The R8 test passes on the machine that reproduces it. Full
m4_acceptance 151/0. `--lib` 1920, `--lib --features crdt` 2105,
`-p pmacs-gpu` 241, fmt, clippy, `git diff --check`. The full workspace
sweep exits 0 across 113 targets --- the first fully green local sweep of
this session, R8 having been the only obstacle.

`scripts/gate` is deliberately not a criterion: this branches from main,
where it does not exist. #225 rebases onto this and takes a green gate
run as ITS criterion.

R8 is RETIRED CAUSALLY --- mechanism removed plus a discriminating,
portable witness --- and moved to the retired section with its
disposition. What the retirement does NOT claim is stated there: 113
`new_with_roots` constructions in this suite alone, an unknown number
equally unbounded, harmless only while their assertions do not render a
path. That census is now a named §6 follow-on, because the next one will
otherwise look like a fresh mystery rather than a known class.

Framing: docs/r8-fixture-boundary-framing.md (revision 2, approved).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* docs(tests): reunite the listview doc comment with its test; state the PR

Three review findings, one of which is mine to own plainly.

I REPORTED A SHA I NEVER VERIFIED. The previous message named the PR
head as `21f0ed1`. That object does not exist in this repository. The
true head is `78d8e1c` --- local tip, `githubsucks/r8-fixture-boundary`,
and the PR all agree, and it is what was reviewed. No command in that
turn ever printed `21f0ed1`; I asserted an identifier instead of
reading one, which is precisely the failure a head-SHA check exists to
catch. Verified this time before writing it down.

THE DOC COMMENT DOCUMENTED THE WRONG TEST. Inserting the new witness
anchored on `#[test]\nfn flat_listview_...`, which sits BELOW that
test's 17-line doc comment --- so the comment about outline and flat
listview consumers ended up introducing the planted-marker test, which
touches neither, while the test it was written for was left bare. Moved
back. No behaviour change; both tests still pass.

That is a general hazard of anchored insertion worth naming: anchoring
on the `fn` line silently steals whatever documentation precedes it.

STALE STATE IN TWO DOCS. The framing still said "Pre-implementation.
Awaiting approval" after being approved and implemented, and the ledger
lane said "PR PENDING" after #226 opened. Both now record approval,
implementation, the PR link, and that it is held for review with no
merge authorization.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 08:54:06 +00:00
Levi Neuwirth b833b139e2
docs: retire the QoL arc's three lanes, re-homing their durable residue (#224)
* docs: retire the long-lines lane, the QoL arc having closed at #223

Rule 4, applied in its stated order: durable facts to
`docs/agent-handoff.md` FIRST, then the lane goes. The ledger is for
volatile branch/checkpoint/recovery state, and none of that survives a
merge worth keeping.

What moved to the handoff, because it is architecture rather than
bookkeeping:

  - The GPU is not a grid consumer, so it could not inherit
    `view_left`; `code_scroll_left` is its own, in pixels, local
    viewport state with no wire and no version bump.
  - The work was ONE transform and ONE clip, written before any
    consumer moved. glyphon honors `TextBounds` so the text layers
    clip themselves; the manual quad and squiggle renderers do not,
    and nothing needed them to before this stage because no
    code-relative x could be negative.
  - Washes crop rather than drop — the same boundary Stage 4's review
    caught the TUI painter getting wrong.
  - Both lifecycle resets are observed PRE-MOTION, because a later
    cursor motion repairs the offset anyway.
  - `pmacs_protocol:📜:follow_left`: the follow rule beside
    `classify`, called by both frontends. The approved exception to
    Stage 5's scope, recorded in the framing doc §1.2a.
  - The two witnesses that exist because mutation testing found the
    TESTS wrong rather than the code — the unwitnessed glyph layer, and
    the completion predicate a far-off-left test could not catch.
    "Test a boundary AT the boundary" is the transferable part.

The `## Long lines (QoL arc)` block is removed whole; nothing else in
the ledger referenced it.

NOT REMOVED, and not an oversight: the QoL Stage 1 (#219) and Stage 2
(#220) blocks. Rule 4 would now reach them too — the arc they belong to
is closed — but Stage 2 carries a live named follow-on, capability-aware
keymap resolution (Q#Z3 option A, deliberately deferred), plus the
"first eager state consumer" restore-seam finding. Those need re-homing
before their blocks can go, which is a judgment beyond retiring the lane
that was asked for. Flagged rather than done.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* docs: complete the retirement — Stages 1 and 2, and the stale anchors

The previous commit removed the long-lines lane and stopped short. Two
things were still wrong.

STALE ANCHORS. The handoff was dated 2026-08-06 with `main` at
`db1bbe9` — five merges behind. Now 2026-08-08 at `9a26ac8`, with the
arc's five merges named in the ancestry chain, and the same in
active-work's canonical-base record and recovery floor (a tree at
`db1bbe9` no longer passes; it would lack the whole arc).

**The recovery path was re-exercised, not SHA-swapped.** That file
warns that advancing the base is exactly when the commands are most
likely to have rotted and that a swapped SHA reads identically to a
verified one — so: fresh clone into an empty directory, `githubsucks`
alias added, `git fetch --prune`, `9a26ac8` confirmed an ancestor of
`githubsucks/main`, and a worktree recovered with the three-argument
form. All four steps clean.

STAGE 1 AND STAGE 2 BLOCKS. Keeping them left false live planning
standing: "Stage 3 is long-line wrap/scroll, which is a design round:
no horizontal viewport exists at all" — written before #221–#223 built
one. A merged lane that still describes the future is worse than no
lane.

Re-homed first, per Rule 4's order:

  - **FG-INV is a CONSUMER contract**, and it lives on the protocol
    type because that is where consumer authors read it. It had been a
    doc comment on a PRIVATE PRODUCER FIELD, which is why the one
    consumer never honored it.
  - **Seven tests covered that flag and all seven tested the
    producer.** None asserted a consumer acts on it. "Add a test for
    the flag" had already been done — §5's enforcement/documentation
    drift in a second register.
  - **`install_state_dirs` is the eager-state-consumer seam.**
    Builtins and `init.lua` run before it, so `pmacs.state.read` at
    module load returns nothing, always. `saveplace` and `recentf`
    escape it only because both read lazily. Any future eager consumer
    belongs at the same seam.
  - **A GPU-only binding cannot be expressed**: `Scope` has no frontend
    identity and `FrontendEvent` no command-invocation variant. #220
    shipped commands without bindings for that reason, not preference.

  - **Capability-aware keymap resolution** is now a named §6 backlog
    item: CROSS-CUTTING, NOT STARTED, needs its own framing. It says so
    explicitly, and says not to start it as a half-lane attached to
    another stage's branch — which is how it would arrive by accident.
    No implementation, no lane, no design.

What is deliberately NOT preserved: the recovery commands for
`full-grid-resync` and `gui-zoom`. Those branches are merged; a
recovery command for a branch nobody should check out is the kind of
stale instruction this ledger exists to avoid. The framing docs remain
on disk as the historical record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* docs: the live schema range is v6..=v22, and #224 gets its own lane

Two review findings against the retirement.

THE SCHEMA RANGE. The canonical-base paragraph still said `v6..=v21`.
The upper bound moved to **v22 at #221**, which added
`InstanceMessage::LineWrapFacts` — so that line had been wrong for two
merges, including the one this branch is retiring the lane for.

Verified against `pmacs-protocol/src/message.rs` rather than carried
forward: `SUPPORTED_PROTOCOL_VERSIONS` is `6..=22`, `PROTOCOL_VERSION`
is 22, and `ADVERTISED_PROTOCOL_VERSION` is **20** and did not move.
The paragraph now says so, and says the advertised constant must not be
edited to chase the range — it is a permanent baseline, and the session
version is settled one message later by the frontend's counter-offer.

It also now states which claims it governs: historical `v21` statements
elsewhere describe a stage as it landed and are correct there. Only this
current-state paragraph tracks the live range, so only this one goes
stale when the range moves.

A LANE FOR #224. This file requires a lane for **every open PR**, and
the PR that retires other lanes is not exempt. Added with the branch,
the ref-not-a-SHA recovery command, the docs-only scope, and the
verification — including that the recovery path was re-exercised rather
than SHA-swapped, and that the full gate suite is deliberately not
re-run for a change that cannot reach it.

The entry is honest about its own lateness: it was written AFTER the PR
existed, which is the standing correction from #171 and #215 being
missed again, and it took review asking. Back-dating the block to look
compliant would have destroyed the only evidence that the practice
still slips. It carries its own retirement instruction — next
absorption after #224 merges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* docs: scope the two provenance paragraphs that claimed to be current

Both said "this line is the head-of-`main` anchor". Neither was, and a
provenance note that claims currency is worse than a plainly historical
one — it disagrees with the real anchor at the top of the file while
looking authoritative.

The bottom-panel paragraph keeps its `v6..=v21` facts, which are correct
for the stage they describe. What changes is scope: "a current session"
becomes "a session at that anchor", and the closing clause now says
these statements describe the historical `6c9e765` anchor, with the
live range pointed at "Repository authority" in `docs/active-work.md`
(`v6..=v22` since #221; advertised baseline still v20). The
counter-offer mechanism is called out as still current independent of
which numbers it carries, since that part did not go stale.

The second was MY inconsistency, introduced earlier on this branch. I
rewrote that bullet's opening to "Beneath the QoL arc, at `db1bbe9`"
and left its closing claiming to be the head-of-`main` anchor, so the
bullet contradicted itself. It now names what it actually is — the
`db1bbe9` ancestry chain — and points at the top of the file.

Both cross-references name the file they point into. "Repository
authority" is a section of `docs/active-work.md`, not of this one, and
an unqualified "above" would resolve to nothing here.

Noted, not acted on: the bottom-panel arc has its own currency drift in
this file (§1 says Arc 7 COMPLETE at #213, two later paragraphs still
call Stage 3 the remaining step). Pre-existing, unrelated to the QoL
retirement, and not this PR's to fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 13:10:24 +00:00
Levi Neuwirth 9a26ac8f9f
feat(gpu): horizontal scroll — QoL Stage 5, closing the long-lines arc (#223)
* docs: frame QoL Stage 5, GPU horizontal scroll

Stage 4 merged as #222, so the lane advances to its last stage. Rule 4
still does not apply — the arc closes when Stage 5 merges, not before.

THE FRAMING'S FIRST FINDING CORRECTS STAGE 4'S. §1.3 there said the GPU
"needs a mechanism that does not exist", named it the fact most likely
to invert the cost estimate, and I endorsed the Stage 4/5 split partly
on that basis.

Half of it holds: `Scroll::horizontal` really is discarded throughout,
because glyphon 0.11 never applies it when placing glyphs — three
doc sites and three asserting tests. But that is not the only
mechanism. The document `TextArea` already carries an explicit `left`
origin and a `TextBounds` clip whose `left` is `gutter_clip_left`, and
horizontal scroll is `left: text_left - offset_px` with the clip
unchanged. glyphon then drops what falls left of the gutter — the same
"paint from column 0, clip at the edge" shape the grid renderer uses,
expressed in pixels. It is machinery the file already depends on, not
new machinery.

The split stays right for the reason that survives: the three consumers
Stage 4 named — caret (`code_byte_px`), decoration geometry
(`push_glyph_extent_rects`), hit testing (`gutter_aware_rel_x`) — each
produce x relative to `text_left()` and each need the same offset,
applied ONCE or they disagree. Shipping that inside Stage 4 would have
made one reviewable change into two unreviewable ones. But it was
justified partly by an overstatement, and saying so is cheaper than
letting a future reader inherit it.

No wire, no version bump: the GPU owns its viewport locally, exactly as
it owns `scroll_top` and `code_scroll_residual`. The parallel with
`ui.line-wrap` is misleading and the doc says why — the MODE is buffer
state and needed v22, the OFFSET is viewport state and needs nothing.

Five questions, each with my vote. Q#G3 is the one I am least sure of:
the GPU can resolve a proportional family, where "column" has no fixed
pixel width, so column-for-column parity with the TUI is unachievable.
I lean to defining the behavior in pixels and accepting imprecise
correspondence rather than gating a navigation feature on a font
choice — but that is a product call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* docs: Stage 5 revision 2 — a clip, not just an offset

Two functional findings and two record repairs.

Q#G3 WAS BUILT ON A FALSE PREMISE, and the correction makes the lane
stricter rather than looser. Revision 1 said the GPU can resolve a
proportional family and proposed accepting a new TUI/GPU divergence to
accommodate it. It cannot: `family_is_monospace_everywhere` gates the
family across all four weight/style combinations,
`apply_font_facts` falls back when that fails, and
`unresolvable_and_proportional_families_fall_back` REQUIRES the
fallback. Answered as monospace-only by the font contract that already
exists — and the consequence is that the TUI-parity witness becomes
UNCONDITIONAL for every font the GPU supports. Revision 1 would have
introduced a font-dependent behavior difference to solve a problem the
codebase had already solved, in the lane whose purpose is removing
unchosen divergence.

"THREE CONSUMERS" WAS INCOMPLETE IN A WAY THAT WOULD HAVE SHIPPED A
DEFECT. Shifting the `TextArea` clips glyphon's text because glyphon
honors `TextBounds`. The manual quad and squiggle renderers have no
code-area scissor at all — nothing stops them painting into the gutter,
and today nothing needs to, because no code-relative x can be negative.
Scrolling makes that false.

So the framing now requires TWO shared things: one screen↔code
transform, and one code clip rectangle every code-relative painter
intersects with. The paths are tabulated with sites — caret rect
(`:9698`), caret-painted predicate (`:9734`), glyph extent rects
(`:9766`), inline math origins (`:9434`), completion anchor (`:7606`).

The two caret sites are the sharpest, and one of them falsifies a claim
revision 1 made: `:9734` has no left-edge test, so "the scroll
indicator inherits the fix" was false — `code_byte_painted` reuses it
and would call an off-left byte painted. And `:9698` does not merely
lack a check, it DOCUMENTS the absence as safe ("the caret x can't
precede `text_left`"). A comment asserting an invariant this lane
deletes is worse than silence.

Q#G2: "inert under wrap" was too weak. The offset must be RESET to zero
on the wrap transition, as the TUI already does — `horizontal_follow`
assigns `view_left = 0` on the wrap branch. Inertness hides a stale
value that reappears the moment the buffer toggles back to `truncate`,
before any cursor motion. G5 gains a witness that an inertness-only
implementation fails.

RECORDS. Rule 4's Stage-5 removal precondition was not actually met:
the handoff still described Stage 4 as upcoming work. Stage 4's durable
facts are now transferred — the unsnapped per-window column with a
per-line effective edge, the line-absolute walk, the three-way cell
designation, `Viewport::visible_cols` and its five adopters, the
wrap-branch reset, the `#[serde(default)]` persistence, and the absence
of any wire. The ledger's "Stage 4 ahead" / "Stage 4 plan" text is
corrected to Stage 5, and its Rule 4 note now says the removal is
legitimate BECAUSE those bullets exist.

And the journey-step claim is withdrawn. Revision 1 said this lane
completes journey step 4; step 4 is scored on welcome/help/tutorial
discoverability and COHERENCE.md:395 holds it Partial for reasons this
lane does not touch (`C-h` deletes a word, no tutorial). Restated as
preserving interface comprehension with no scorecard movement. §16 is
the direct target. Writing an unearned mark into a scorecard is how a
coherence document stops being ground truth.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* docs: Stage 5 revision 3 — four corrections, one of them impossible

Q#G1 CONTRADICTED THE Q#G3 ANSWER IN THE SAME DOCUMENT. It still said
the GPU's font "need not be monospace" and that Q#G3 makes "column"
ill-defined — both falsified by the answer two sections below, in the
same revision that wrote it. The pixel-storage vote is unchanged, but
its reasons narrow to the ones that survive, and the conversion is now
stated as EXACT: columns × the supported monospace advance. That is
what makes the unconditional parity witness checkable at all.

Also removed `follow_cursor`, which I invented. The GPU's pass is
`ensure_caret_painted`, and it is now named rather than cited by line —
robust against the transposition that put these two sites at each
other's line numbers in review.

Q#G2 WAS MISSING THE BUFFER-SNAPSHOT RESET. The GPU zeroes `scroll_top`
and `code_scroll_residual` when a snapshot installs a new buffer; the
horizontal offset must reset there for the same reason. Without it a
buffer switch INHERITS the previous document's leftward viewport,
showing the new buffer scrolled sideways until a cursor motion repairs
it — a worse symptom than the wrap case, because nothing about the new
buffer explains it.

THE GUTTER ASSERTION WAS IMPOSSIBLE, not merely imprecise. Revision 2
proposed asserting that nothing paints left of `gutter_clip_left`. With
line numbers on, the gutter DELIBERATELY holds digit glyphs and
diagnostic-sign quads, so that assertion fails on a correct
implementation — a test that can only be satisfied by removing the
gutter. Replaced with the checkable form of the same intent: the gutter
rectangle is byte-identical before and after a horizontal scroll, and
the left-edge rule is checked against code-relative geometry only. It
still catches a code painter bleeding into the gutter, because that
changes those pixels.

THE COMPLETION ANCHOR HIDES, IT DOES NOT CLOSE. `completion_anchor_px`
already returns `None` when the anchor scrolls out, so nothing draws
while the daemon-owned completion state and its key handling are
retained; actual closure is `CompletionPopup { anchor: None }`, which
is the daemon's to send. Revision 2 said "closes", which would have had
a viewport-geometry lane quietly redefining when a completion ends.
Specified as: no completion paint while the anchor is off-left, popup
reappears when it scrolls back, session semantics unchanged.

Ledger drift fixed: it still called the framing revision 1 with five
questions open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* docs: Stage 5 revision 4 — witnesses for the two rules that had none

Both additions cover requirements the framing had already stated and
then left untested, which is how a rule becomes a comment.

THE SNAPSHOT RESET (Q#G2). Revision 3 added the buffer-snapshot reset
and tested only the wrap one. The witness now scrolls buffer A to a
non-zero offset, installs a buffer B snapshot, and asserts the offset
is zero and B renders at its code origin BEFORE any `CursorByte`
arrives.

The pre-cursor scoping is the entire test. A later cursor motion
repairs the offset regardless, so a witness that waits for one cannot
distinguish "reset on snapshot" from "repaired on first motion" — and
the second is the defect. Same shape as the wrap witness, which is also
scoped to before any motion, and for the same reason.

THE MINIMAP (Q#G4). The vote is "no movement", and the implementation
already supports it: the minimap derives from the summary, the surface
dimensions and `scroll_top`, with no horizontal input. So the witness
pins an existing property rather than requesting work — which is
exactly why it is worth writing. An offset threaded one seam too far
would break it silently, and nothing else in G5 would notice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* docs: Stage 5 approved, five questions resolved

G1 pixels with exact conversion via the supported monospace advance; G2
automatic cursor-follow only, zeroing on both the wrap transition and
BufferSnapshot; G3 monospace-only by the existing font contract; G4
minimap unchanged; G5 accepted whole, including the snapshot-reset and
minimap-stability witnesses.

The scope boundary is restated in both documents because it is what
keeps this lane small: local GPU viewport state, no wire message, no
protocol bump, no command surface, no minimap movement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* feat(gpu): horizontal scroll — the transform, the clip, and both resets

Stage 5, partial. The mechanism and lifecycle are in; two consumers and
the whole verification set are NOT yet done — see the tail of this
message, which is a status, not a summary.

WHAT IS IN.

The offset, `code_scroll_left`, in pixels (Q#G1). Column parity stays
exact because the code font is monospace by contract, so
`columns × advance` is a definition rather than an approximation.
Local viewport state: no wire, no version bump.

One screen↔code transform (`code_x_to_screen` / `screen_x_to_code`) and
one code clip (`code_clip_left` / `survives_code_clip_left`), which is
the pair framing §1.1 requires. Written before any consumer moved,
because five sites deriving the same offset independently is how the
caret and the glyphs it sits among come to disagree.

The glyph-side mechanism is one line: the document `TextArea`'s `left`
shifts while its `bounds.left` stays at the gutter, so glyphon clips
and the gutter keeps its own pixels.

BOTH LIFECYCLE RESETS (Q#G2), which were the two rules most likely to
be left as comments. The wrap transition zeroes the offset in
`apply_line_wrap` — inertness would park a stale value that reappears
the instant the buffer toggles back to `truncate`. The buffer snapshot
zeroes it beside `scroll_top` and `code_scroll_residual`, or a buffer
switch inherits the previous document's leftward viewport and shows the
new buffer scrolled sideways until a cursor motion repairs it.

`code_caret_rect_in_clip` gains its left-edge test, and its comment is
REWRITTEN rather than extended: it used to assert "the caret x can't
precede `text_left`", an invariant this stage deletes. A comment
asserting something a later stage falsifies is worse than silence. That
also repairs `code_byte_painted`, which reuses it — revision 1's claim
that the scroll indicator "inherits the fix" was false precisely here.

`gutter_aware_rel_x` is now the exact inverse of the transform, with
the gutter clamp applied in screen space first: a click in the gutter
band means "the first visible column", which after scrolling is the
offset, not column 0.

The completion anchor HIDES when scrolled off-left and does not close —
the daemon owns completion state and its key handling, and closure is
`CompletionPopup { anchor: None }`, which is the daemon's to send.

`horizontal_follow` mirrors the TUI's: automatic only, scroll just far
enough, so a caret already visible never moves the view. It runs after
`normalize_code_scroll` because it reads the caret's laid-out x, which
vertical normalization can change.

WHAT IS NOT IN, and must land before this is reviewable:

  - `push_glyph_extent_rects` — washes, squiggles and selection extents
    still paint at unshifted x and are not cropped at the gutter.
  - Inline math origins (`:9434`) — same.
  - Every Q#G5 witness. The 228 existing GPU tests pass, which says
    only that nothing regressed at offset 0; not one of them exercises
    a non-zero offset.

Gates so far: fmt; clippy --workspace --all-targets -D warnings;
PMACS_REQUIRE_GPU=1 -p pmacs-gpu 228/0; git diff --check. The full
two-configuration sweep is deliberately not claimed — the lane is not
finished.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* feat(gpu): the last two painters move, and twelve witnesses say so

Completes Stage 5. `62fb93e` landed the transform, the clip and both
resets but left two code-relative painters at unshifted x and the whole
Q#G5 witness set unwritten; its 228 green tests said only that nothing
regressed at offset 0.

The two painters:

  - `push_glyph_extent_rects` — selection/search washes, peer presence
    and diagnostic squiggles. Shifted through `code_x_to_screen`, then
    CROPPED at the gutter rather than dropped: a selection running in
    from off the left edge must paint the part that is visible. That is
    the same boundary Stage 4's review caught the TUI painter getting
    wrong, and it would have been easy to reproduce here.
  - Inline math. The glyph mini-buffers only needed their origin moved —
    their layer already carries the code area's `TextBounds`. The
    fraction rules are quads in the background batch with no scissor of
    their own, so those are cropped by hand.

`crop_to_code_clip_left` is the crop, and `survives_code_clip_left` now
delegates to it, so a caret the crop would discard is never painted.
One boundary rule, not two that agree today.

TWELVE WITNESSES, EACH MUTATION-TESTED. Eleven production mutations —
unshifted wash x, uncropped wash, unshifted math origin, uncropped math
rule, untested caret left edge, missing snapshot reset, missing wrap
reset, unhidden completion anchor, unscrolled glyphs, inverted hit-test
sign, pixel-instead-of-column snap — each fail the intended witness as
an ASSERTION failure, not a compile error. The minimap-stability
witness was mutation-tested separately by threading the offset into
`minimap_vertex_bytes`.

That battery earned its keep immediately. The gutter byte-identity
test's "the code area must actually have moved" assertion is satisfied
by a decoration wash and the caret alone, so it PASSED with
`TextArea.left` pinned to `text_left` — the entire glyph-side mechanism
was unwitnessed and nothing in review would have shown it. Its
replacement isolates the glyph layer: no decorations, and a source line
carrying no caret, whose band is blank at offset 0 and inked after.

ONE DELIBERATE STEP OUTSIDE THE APPROVED SCOPE, and it needs a ruling.

Q#G5 asks for frontend agreement that is "checkable rather than
asserted". Two tests in two crates asserting the same literal is not
that; it is the structural duplication `pmacs-protocol::scroll`'s own
module docs condemn, and that module exists because THIS ARC already
shipped that defect — the scroll indicator, fixed in one copy and left
wrong in the other. So the follow rule moved to
`pmacs_protocol:📜:follow_left`, beside `classify`, and both
frontends call it: `src/editor.rs::horizontal_follow` delegates, and the
GPU converts px <-> columns around it, exact by Q#G3.

The cost is that Stage 5 now touches `src/editor.rs`, which "local GPU
viewport state" does not cover. No wire message and no version bump —
the same argument `classify` already makes. If rejected, reverting is
small: restore the four-line conditional, drop `follow_left` and its
four protocol tests, rewrite the parity witness as a two-sided pin.

GATES, both configurations, five ambient roots isolated: fmt; clippy
`--workspace --all-targets -D warnings`; `--lib` 1920 and `--lib
--features crdt` 2105; horizontal_scroll 11, long_line_readable 3,
line_wrap 6, full_grid_resync 1; `PMACS_REQUIRE_GPU=1 -p pmacs-gpu`
239; `-p pmacs-protocol --lib` 29; both full workspace sweeps;
`git diff --check`.

TWO SWEEP FAILURES, NEITHER THIS LANE'S, both logged:

  - R8, new row: `flat_listview_consumers_render_byte_identically...`
    fails DETERMINISTICALLY, and the merge-base control is done — it
    fails identically on `main`. The row renders with a leading
    directory stripped; it is a prefix strip, not width truncation, and
    the mechanism is NOT diagnosed. Deliberately not fixed here.
  - U3: the R7 selector failed once and passed on rerun. Recorded as a
    new incident, NOT an R7 match — different flavor, and its fragments
    are unverified because I filtered the sweep output before reading
    it. U2 records me making that exact mistake already. Sweeps go to a
    file from now on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* fix(gpu): the completion anchor is a point, and the witness now says where

Review round 1. One defect, and a lesson about the witnesses that
missed it.

THE DEFECT. `completion_anchor_px` reused `survives_code_clip_left` and
passed `line_height` as the horizontal extent — a VERTICAL dimension
standing in for a horizontal one. The predicate is
`screen_x + w > code_clip_left()`, so an anchor up to a whole line
height left of the gutter "survived". `completion_dropdown_rect` bounds
`ax` against the right margin only, so that x reached the popup's left
edge and painted over the line numbers.

An anchor is a position between glyphs. It has no width, and the popup
it places is drawn to its right. So the predicate is a point:
`screen_x < code_clip_left()`.

The absent left clamp downstream stays absent, deliberately. This
predicate is what guarantees `ax >= code_clip_left()`; a second clamp
would be a duplicate of the same rule, which is the failure mode this
stage's shared-transform design exists to avoid. It is witnessed
instead.

THE LESSON, which is the more useful half. The existing test placed the
anchor 200px off-left — and 200px off-left fails a width-based
predicate too, so it stayed green straight through the defect. The
mutation battery agreed with it, because every mutation asked only
whether REMOVING a check was caught, never whether the check had the
right shape.

A boundary must be tested AT the boundary. The new witness straddles it
by ±0.05px — the same anchor either side of the edge, which no
width-based predicate can separate — and additionally asserts the
popup's own left edge stays out of the gutter, making "no left clamp
needed downstream" a checked claim rather than a comment. Verified both
ways: the new witness fails against the original predicate, the old one
passes against it.

THE AUDIT that finding prompted. Stage 5 has one other left-edge
predicate, the caret's. Its use of `survives_code_clip_left(rect.x,
rect.w)` is correct — a caret quad genuinely is `CARET_WIDTH` wide —
and it was also only tested far from the edge. It is now walked ACROSS
the boundary a column at a time, asserting painted carets are wholly
inside the code area and hidden ones wholly outside.

That pins an argument that was load-bearing and invisible: because
`horizontal_follow` snaps to whole columns, a caret is never partly
behind the gutter, since `CARET_WIDTH` (2px) is far below any code
advance. Substituting `rect.h` for `rect.w` — the exact error above —
fails it. An over-width smaller than one advance does not, and that is
the invariant rather than a gap.

SCOPE. `follow_left` recorded as the one approved exception to "local
GPU viewport state" in the framing doc, new §1.2a: what it is, why the
Q#G5 parity witness cannot be real without it, and what it does not do
— no viewport state moved, no wire message, no version bump.

GATES, both configurations, five ambient roots isolated, sweeps
redirected to files per U3's lesson: fmt; clippy `--workspace
--all-targets -D warnings`; `--lib` 1920 and crdt 2105;
`-p pmacs-protocol --lib` 29; `PMACS_REQUIRE_GPU=1 -p pmacs-gpu` 241;
horizontal_scroll 11, long_line_readable 3, line_wrap 6,
full_grid_resync 1; both full workspace sweeps; `git diff --check`.

The only sweep failure is R8, confirmed by its recorded fragments —
pre-existing, deterministic, merge-base controlled against `main`, and
not this lane's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* docs: Stage 5 is PR #223, head 55faa45

The ledger said "no PR opened yet", which stopped being true the moment
it was. Records the PR, its head SHA, and the standing do-not-merge.

Rule 4 still applies at merge, not now: the long-lines lane stays until
#223 lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* docs: the tip is the ref, not a SHA the commit itself invalidates

The previous commit wrote "head 55faa45" into the ledger and, by
existing, made it false — recording the PR moved the head to 4902048.
A SHA pinned in a document that the act of writing it stales is a trap,
not a record.

The ledger already states the correct convention two paragraphs down
("the authoritative tip — the ref, not a SHA"); this follows it, and
says to verify CI against the PR's live headRefOid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 10:55:49 +00:00
Levi Neuwirth b29e94d29b
docs: the long-lines lane records PR #222
Stage 4 is open as #222; Stage 5 (GPU) still closes the arc, so the
Rule 4 exemption above stands unchanged.

Records the two things review added after the framing was approved: the
`Viewport::visible_cols` single clip rule with its five adopters, and
the corrected `ui.line-wrap` description. Both are lane facts rather
than framing ones — the framing decided the coordinate contract, and
these are what implementing it against a real frame turned up.

Also notes R7, so a reader of this lane finds the unrelated red without
having to reconstruct why a sweep in this window went 112/113 once.

No SHA — `githubsucks/horizontal-scroll` stays the authoritative tip,
per this ledger's own rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-07 22:44:49 +02:00