Commit Graph

705 Commits

Author SHA1 Message Date
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