Commit Graph

332 Commits

Author SHA1 Message Date
Levi Neuwirth 8fd8f585aa
docs: the zoom lane records PR #220
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 16:22:05 +02:00
Levi Neuwirth aa99ab39d2
feat(zoom): GUI zoom over the font preference that already existed
Ctrl +/- had no effect whatsoever in the GPU frontend. Stage 1 (#219)
fixed what zoom did to the TUI; this is the other half.

NO RENDERING WORK. FontMetrics::scale already derived every GUI
dimension — code size, line height, status band, divider, menu rows,
minibuffer dropdown, gutter advance — and apply_font_facts already
re-metriced all seven buffers in one transaction. This drives the
preference that existed: two settings, three commands, and a restore.

Q#Z1 = (c). Relative zoom needs an origin and the daemon is built never
to know one — font_pref.rs is explicit that it "never learns metrics,
advances, or what resolves". Hardcoding 16.0 would put a pixel constant
on the daemon side; always sending a size would destroy the `None`
state for everyone who never zooms. A configured base is the only
option where the daemon still infers nothing, and the untouched path
stays byte-identical.

THREE THINGS REVIEW CAUGHT THAT REVISION 1 HAD WRONG.

Q#Z3 was not implementable as framed. `keymap_stack::Scope` is
Buffer | Mode | Global and carries no frontend identity, so "bind on
GPU frontends only" does not exist; and FrontendEvent has no
command-invocation variant, so the GPU cannot ask for a command by name
either. A global binding would capture the chord in the TUI and take
away the terminal's own zoom — the very thing the user is pressing it
for. Commands ship; the binding waits on capability-aware keymap
resolution, which is now a named follow-on rather than something
smuggled in here.

The restore seam did not exist. Builtins and init.lua both run BEFORE
install_state_dirs, so a pmacs.state.read at module load returns
nothing, always. saveplace and recentf never meet this because both
read lazily inside functions; zoom must apply with no user action,
which makes it this project's first eager state consumer. Restore lives
at the end of install_state_dirs — by definition the moment state
becomes readable, so it cannot be ordered wrongly and a future third
startup path gets it without knowing it had to ask.

Every size write clobbered the family. set_font replaces both fields
unconditionally, so { size = n } alone silently cleared a configured
family until restart.

BITTEN, THREE WAYS. Dropping family preservation fails 3 tests.
Reverting to the framing's own first parser `^(%d+)$` fails 4 including
the seam restore — it anchors to end-of-subject and rejects the
newline-terminated file the writer emits, which is the contradiction
review caught in the framing before it reached code. Hardcoding the
16.0 origin fails the base test.

Also recorded: a loaded crdt run failed two m6_1 PTY tests with
`stty -a output was: ""`. That is R4/R6's empty-content readiness
family, and it means the readiness-helper audit's scope is wider than
three wait_for_file copies under tests/ — src/process.rs's own tests
carry the shape. Undiagnosed, load-sensitive, green isolated and on a
quiet full run; a scope note for that lane, not a registry row, since
the registry judges red CI runs and these were local.

Verified: fmt, clippy, diff-check, --lib 1900/0, crdt 2085/0,
gui_zoom 13/13, journey 47/0, m4 150/0, gpu 221/0, full_grid 1/1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 16:20:46 +02:00
Levi Neuwirth 3361da08df
docs: the full_grid lane records PR #219
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 15:08:45 +02:00
Levi Neuwirth 899aaf2249
fix(frontend): honor full_grid — the flag existed and nothing read it
Zooming a terminal with Ctrl +/- left the TUI showing the previous
frame through the new one. Q#FG1 = A, as approved.

THE RULE WAS ALREADY WRITTEN DOWN, ON A PRIVATE FIELD.
src/instance_render.rs:36 says remote frontends "must blank their local
buffer before applying the deltas" — the binding contract, in the one
place a consumer author will never look. The protocol type said only
that full_grid marks "the initial sync ... versus an incremental
frame": a label, from which no obligation follows. So FG-INV now lives
on InstanceMessage::CellDelta, where whoever writes the next frontend
reads it. A resync is a picture of the screen's INK, not of the screen.

The producer diffs against a blank grid, so a cell that should be blank
produces no span. src/frontend.rs then took `CellDelta { spans, .. }`
and discarded the flag. That was correct for exactly one frame — the
fresh-attach frame, which follows Frontend::new's Clear — and wrong for
every resize after, which follows nothing. A font-size change is the
worst case because the terminal reflows in place rather than dropping
content, so the maximum number of stale glyphs survive.

emit_cell_delta joins emit_span and emit_status_overlay as a pure
helper over a writer; apply_message routes through it. No struct
change, no generic parameter, no new pattern.

WHY SEVEN TESTS MISSED IT. Every one asserts the producer SETS the
flag; none asserted a consumer ACTS on it, and no runtime reader
existed workspace-wide. "Add a test for the flag" had already been
done and did not help. Handoff §5's enforcement-vs-documentation drift,
in a second register.

Three unit witnesses, each bitten independently. The empty-spans case
earns its own test rather than folding into the others: under the
plausible `spans.is_empty()` early return the ordering test still
PASSES and only that one fails — and an empty resync is exactly the
frame whose entire content is the blanking.

The PTY acceptance drives a real SIGWINCH, and its mark is anchored to
CONTENT rather than time. A time-based settle was written first and is
unusable: a settled pmacs screen emits per-frame bytes forever, so
"output stopped growing" never becomes true. Anchoring just past the
first painted byte excludes both startup clears by construction —
Frontend::new clears before any frame exists, and the first frame is
itself a resync whose clear precedes its own spans. Bitten against the
original defect: 34,831 bytes after the first painted frame, no CSI 2 J
anywhere in them.

What it does not prove, stated here rather than found in review: the
suites assert on raw bytes, with no screen model and no vt100/termwiz/
vte dependency. This shows pmacs emitted a blank at the right moment,
not that the screen ended correct.

Verified: fmt, clippy, diff-check, --lib 1900/0, crdt 2085/0, m4 150/0,
gpu 221/0, and the grid-driving suites — full_grid_resync 1/1, vterm
1/2/3 9+9+5, m5_5 15, m5_8 5, bottom_panel_stage1 47.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 15:05:59 +02:00
Levi Neuwirth b172347945
docs: four review defects, and a documented error that never happens
1. THE CENTRAL RULE LACKED ITS TIME QUALIFIER. It said any red matching
a retired row is a recurrence — under which this PR's own R2 finding
reopens R2. Now: a match POSTDATING the retirement challenges the
disposition; a match predating it corroborates. That is not a
technicality. An occurrence scan reaches backwards by construction, so
most matches it finds are the earlier kind, and the old wording would
reopen every retired row the first time anyone scanned.

2. R5 AND R6 SAT UNDER "RETIRED ROWS" while declaring themselves live
and undiagnosed. I inserted them before R2's heading, which put them in
the wrong section — presentation contradicting classification in a file
whose whole job is classification. Moved under Live rows.

3. RECOVERY ANCHORS STILL DECLARED 12f2970, so the check accepted a
checkout lacking #216 and #217 while the same file described both as
complete. Advanced to db1bbe9, with the ancestry verified rather than
assumed.

4. THE HANDOFF'S DURABLE TREE FACTS STILL SAID IDS ARE OPAQUE AND
COMPARED BY EQUALITY — the contract ef99b64 deliberately narrowed. A §5
lesson explaining a correction does not fix a summary that still states
the uncorrected fact; the summary is what a new agent reads first.
Corrected there and in the header, which also still called Stage 2 in
flight and anchored main at f186253.

AND ONE FINDING FROM RE-EXERCISING THE RECOVERY PATH RATHER THAN
SWAPPING ITS SHA. This file claimed `git worktree add <path>
githubsucks/<branch>` fails with "fatal: invalid reference". It does
not fail. On git 2.55.0 it SUCCEEDS and leaves a detached HEAD — no
branch, no upstream.

Still use -b, but the reason is the opposite of the one recorded: the
hazard is not an error that stops you, it is that nothing stops you.
Work committed there sits on no branch and is not pushed by a bare
`git push` — the "uncommitted work does not travel" hazard wearing the
shape of committed work. A documented error message that never appears
is worse than no documentation, because the reader waits for a signal
that is not coming.

Verified: fmt, diff-check, --lib, listview 26/26. Recovery path re-run
from an empty directory at the new base; all four steps clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:40:54 +02:00
Levi Neuwirth d4d7ea605c
docs: the absorption lane records PR #218
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:18:43 +02:00
Levi Neuwirth d4931acba6
docs: this lane's own block, written before its PR
The ledger requires a lane for every open PR. #171 drifted 153 commits
while invisible here and #215 had no lane until review caught it, so
the block goes in with the lane's own commits rather than after someone
asks for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:18:05 +02:00
Levi Neuwirth 2c59fac510
docs: retire two arcs, and file two reds the scan turned up
RULE 4 REMOVES A LANE AFTER MERGE, and two were overdue: the tree
primitive (#217) and macOS CI signal integrity (#215, #216).

The macOS arc could not simply be deleted. It owned R1 and R3, and a
lane removed while it still owns undone work does not close that work,
it hides it. R1 goes to a new async-runtime block; R3 goes to the
reap-ledger lane, which already parks every disposition change pending
exactly its question. Re-homing first is why this did not happen at
merge.

AN OCCURRENCE SCAN (last 25 main runs: 23 green, 2 red) found both reds
unrecorded, and I misattributed both on the first pass — by theme
rather than by required fragment, which is the exact error the registry
exists to prevent.

- Run 30710662474 is NOT R3. Same test, same EPERM, same
  measured_group=unobservable(ESRCH...) — but R3 requires `leader=live`
  and this reads `leader=exited(signal SIGUSR1)`, R2's exact fragment.
  It is a second R2 occurrence, four days BEFORE R2's retirement, so it
  corroborates the row rather than falsifying its disposition. It also
  adds something: macOS luajit where R2's evidence was lua54, so the
  mechanism was never flavor-specific. Filing it as R3 would have
  attached a live possible product defect to an occurrence of a fixed
  test race.
- Run 30555667095 is NOT R1. Different test, different module,
  different assertion; they share only "supersede under a deadline on
  macOS". Sharing a subject is not sharing a signature. Filed as R5,
  undiagnosed.

R6 is the acc28 readiness timeout from #217's CI. Its scope is the
AUDIT, not the call site: three independently written readiness helpers
now exist and they disagree, with bottom_panel_stage1's carrying only
the zero-byte half of #216's hardening. R4's disposition predicted this
recurrence under a new selector. Patching acc28 alone repeats the
mistake this arc already made once, when the empty-file predicate was
fixed in one helper and left in its neighbour.

The registry carries a rate for the first time — a floor, not a
measurement: main only, 25 runs, readable reds only.

Four durable lessons to the handoff, two from the tree review: an
optional field the shape depends on is not optional, and a contract two
mechanisms must honour is only as strong as the weaker one.

Verified: fmt, diff-check, --lib, listview 26/26. Docs-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:17:46 +02:00
Levi Neuwirth 3f6a2837de
Merge githubsucks/main into tree-primitive-framing
#216 landed while this branch was open. Both conflicts are in the
ledger and the handoff, and both sides had independently written up the
same shared-CARGO_TARGET_DIR hazard.

THE TWO WRITE-UPS ARE NOT ABOUT THE SAME OCCURRENCE, and merging them
carelessly would have been a real error. Stage 2's is established: seven
failures against a clean baseline, failure text naming its own cause,
pgrep confirmation, and a dedicated-target-dir re-run at 41/41. This
lane's is the one whose signatures were destroyed before being read —
it has no captured text to match against Stage 2's, and it keeps two
non-causal hypotheses. A mechanism established in one occurrence is not
evidence about a different occurrence that was never characterized, so
the merged bullet says so explicitly rather than letting proximity
imply it.

The ledger records #216 merged and stops saying Stage 2 is in flight.
It does NOT retire the arc, though rule 4 now would: R1 belongs to the
async-runtime lane and R3 is an unresolved possible product defect for
the process-signal lane, and neither has a block yet. Re-homing them is
an absorption pass — not something to fold into a feature PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:42:03 +02:00
Levi Neuwirth f36b1fcf20
docs: the tree lane records PR #217
The ledger requires a lane for every open PR, and this one described a
hold that has ended.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:29:27 +02:00
Levi Neuwirth d92f0ad4aa
docs: the tree lane was calling a pushed branch unpushed
It said "Unpushed while held" while githubsucks/tree-primitive-framing
sat at the same tip as HEAD. Held means no PR is open; it does not mean
the work is stranded locally, and the handoff's portability rule cares
about the latter.

The commit list also claimed to be exhaustive and then excluded the
commit that updated it — a list of that shape is wrong the moment it is
written. It is now the substantive arc, with `git log` named as the
place to get the complete one, and the remote ref rather than a pinned
SHA named as the authoritative tip: any edit to this lane advances past
whatever SHA the lane records, including this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:23:22 +02:00
Levi Neuwirth 62993e068f
docs: pin the tree lane to the review-round commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:17:41 +02:00
Levi Neuwirth c59de959e7
fix(listview): flat panels keep their TAB, and a selection test that bites
FOUR REVIEW FINDINGS, and the first two were right about the tests.

1. THE SELECTION ACCEPTANCE WAS VACUOUS. `tr_1` toggles the selected
root, which sits on line 1 before and after collapsing — so it passes
unchanged under the line-based re-seating that id-keyed re-seating was
built to replace. It proves collapse hides descendants; it proves
nothing about selection. `tr_4` adds the case that discriminates: an
`on_refresh` inserts a row ABOVE the selected node, so the node moves,
and the assertion is that selection follows the NODE. Bitten by
restoring `seat_cursor(p, saved)`: `tr_4` fails with left "  kid2",
right "sibling", while old `tr_1` passes — which is the finding,
reproduced.

The substantive assertion is deliberately ordered first. It was second
at one point, behind the fixture check that the node moved, and a
regression then reported as "the insert must move the selected node" —
reading like a broken fixture rather than a broken re-seat.

2. FLAT PANELS WERE NOT BEHAVIOUR-IDENTICAL. `bind_local_keymap` binds
TAB on every listview, so a depthless panel that previously fell
through to the global binding — and to Q#P3's read-only intercept —
began answering "listview: no node here". `listview.toggle` now
delegates to `buffer.tab` when no row carries an id, restoring the
prior path exactly; leaf feedback is kept for panels that really are
trees. `tr_3` asserts the absence of both tree messages rather than
merely that the panel still renders.

3 and 4 are documentation. The lane now lists 38e94dc, and no longer
says the PR is held "pending review of the documentation" that the same
commit supplied — it is held pending the decision to open it. §20 said
to BUILD the tree primitive while §14 already carried ◐; it now says
what actually remains, which is adoption: dired's `i` is the next
constraint source, DAP's variables view is why this was worth building
before them.

ONE RED, CLASSIFIED RATHER THAN RERUN AWAY. The crdt lib gate failed
`composition_overhead_under_ten_percent` at 30.7%. It is an incumbent
handoff hazard, and the branch cannot reach it — the diff versus main
touches no src/, no crate, no manifest. Alone it ran 5/5 green at
-0.6% to +0.2%; the next full run was green. Recorded in the handoff as
a MEASUREMENT, not a cause: five isolated greens establish that the
ratio is nowhere near the threshold when alone, not that contention is
what pushed it over. Not a registry row either — that file judges red
CI runs, and this was local.

Verified: fmt, clippy, diff-check, --lib 1896/0, --lib --features crdt
2081/0, listview 22/22, m4 150/0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:17:33 +02:00
Levi Neuwirth 38e94dc33f
docs: give the tree lane its entry, and move Tree off ✗
Three updates, one of which was a broken cross-reference of my own
making.

THE DAEMON-LEAK ENTRY POINTED AT A LANE THAT DID NOT EXIST. It said the
unclassified failure was "recorded in the tree lane below"; this branch
had no tree lane. A pointer to nothing is worse than no pointer — it
reads as though the record exists and sends the next reader looking. The
tree lane is added, and since it now sits ABOVE that entry the direction
is corrected too, with a second pointer to the framing §6a where the
occurrence is recorded in full.

The lane carries branch, base, framing revision, every commit, the gate
table with both sweeps and their exact reconciliations, the bite
verification for both behavioural claims, the held-PR state and the
recovery command. It also names what is NOT in scope, because the four
unadopted §14 consumers and dired's `i` will otherwise read as omissions
from this stage rather than as later ones.

COHERENCE.md §14: Tree moves ✗ -> ◐, implemented with ONE consumer. Not
✓, and the row says why: the LSP outline is the only adopter, dired's
`i` remains the deferral in its §13, and the other four named consumers
have not adopted. The organising fact goes in the section rather than
only in the framing — folding is LOCAL PROJECTION STATE, NOT A REFRESH
PROTOCOL, which is why a consumer with no `on_refresh` can fold at all.
The §0 scorecard row moves with the body; a grade table disagreeing with
its own section is the defect this document keeps correcting in others.

The framing's status moves from "approved" to implemented-and-gated,
held for PR review, and four durable facts go to the handoff §1: folding
as projection state; identity being consumer-supplied and compared by
equality, with `line:col` chosen because the `::` parent chain collides
on overloads; `has_children` having to read the full row array rather
than the rendered subset, since a collapsed node's children are absent
from it by construction and the bug would look like fold working and
unfold silently not; and that a bite which passes validates the pair
rather than the test.

The daemon leak stays a separate reap-ledger candidate. It is not tree
scope, it predates this work, and folding it in would make a lane
responsible for a leak it did not cause.

Verified: fmt, diff-check, listview 21/21, --lib 1896/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 16:50:29 +02:00
Levi Neuwirth 8f64c3b2a1
test(listview): byte-identity for the flat consumers, and two findings
ACCEPTANCE 5, and it needed a real test rather than a weakened claim.
`listview_acceptance` says in its own header that the references panel
"needs a live LSP and is validated manually / via the m4 harness", so it
does not exercise `*references*` at all; the m4 hover test asserts
content PRESENCE, not exact output. Neither would notice a flat consumer
silently gaining an indent column — the regression a tree extension can
introduce. So the coverage is written against the real entry points
through the fake language server.

`*references*` is pinned EXACTLY: the row is the location string and
nothing else. `*lsp*` formats its own two-space indentation, so
"starts with a space" is not a violation there; what must hold is that
the primitive reproduces the consumer's text verbatim, matched as a
WHOLE LINE — a substring would still be found inside a further-indented
copy of itself. Volatile parts (pid, elapsed) are deliberately excluded,
the same normalization reasoning the CI registry uses.

THE FIRST BITE PASSED, AND THAT WAS THE FINDING. Injecting
`string.rep("  ", row.depth or 0)` did not fail the test — flat rows
carry no depth, so it added nothing. I had simulated a regression the
flat path is immune to and would have recorded the test as verified.
The regression this criterion actually guards is an UNCONDITIONAL
column, a fold gutter on every row; with that injected the test fails on
"the flat references row renders verbatim". A bite that passes validates
the pair, not the test — and injecting the wrong defect teaches nothing
while feeling like assurance.

A VERIFICATION RECORD, including one unclassified occurrence. The first
local crdt sweep of this branch reported 7 failures and its SIGNATURES
WERE DESTROYED before being read, piped through an aggregation that
emitted only totals. That is the failure the CI registry exists to
prevent, committed one lane after writing it, and it is why the cause
cannot now be established rather than merely being unknown.

It is recorded in this lane's own framing and deliberately NOT as a
registry row: that registry keys on a normalized signature, and an
occurrence with none would be granted a recognisability it cannot
support — the same reasoning that made the unevidenced incumbents audit
notes rather than rows.

Four re-runs are tabulated with what each supports. Two were not
isolated, including one where my own guard printed "aborting" and did
not abort. TWO GENUINELY ISOLATED RUNS ARE BOTH CLEAN, which supports
repeatability under isolation and establishes nothing about the cause.

Two mechanisms are recorded as NON-CAUSAL hypotheses, because both were
present and neither can now be tested: a shared CARGO_TARGET_DIR (whose
reciprocal case another lane observed independently, with `pgrep`
evidence and failing text that named its own cause), and ~40 resident
leaked daemons. Having two plausible mechanisms and no way to
discriminate IS the result; naming either would repeat the reasoning
this project has rejected — concluding something about an occurrence
from something that was not about that occurrence.

Both mechanisms are recorded as standing hazards in the handoff, and the
daemon leak gets its own candidate lane: 42 orphans, oldest four days,
reparented to systemd with deleted sockets, from
`gpu_invocation_acceptance`'s one-command tests, leaking 3-4 per sweep
as measured rather than estimated. It predates this work and belongs to
the reap-ledger family — a process outliving its supervisor with nothing
watching it — but the existing ledger arms only for `spec.group` and so
does not cover it.

Verified: fmt, diff-check, luajit sweep 3453/0 and crdt 3722/0, each
exactly +4 on its baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 16:31:31 +02:00
Levi Neuwirth 8ab20b5d68
docs(ledger): the second CI checkpoint, and the row a table cannot carry
Both heads that carry code are 14/14 green — `2d9c678` (the fixes and
the framing) and `668fc72` (this block). The tip row is explicit rather
than missing: a checkpoint table can never record the head that adds
the checkpoint, and #215's lane used the same convention. The branch
tip stays authoritative over any row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 14:51:28 +02:00
Levi Neuwirth 668fc72f23
docs(ledger): the lane's PR and CI checkpoint, and the sweep that could not attribute itself
Fills in what could only be filled in after the fact: PR #216, the
opening head's CI run, and the four-way sweep table. Both branch totals
are exactly +4 on their baseline — the four witnesses, and nothing else
moved.

**A shared `CARGO_TARGET_DIR` makes a local sweep unattributable, and it
bit this lane.** The branch `crdt` sweep first reported seven failures
in three suites while the baseline `crdt` sweep was clean. All three
spawn the REAL `pmacs` binary out of the target directory, and the
failure text named its own cause — "daemon does not advertise required
capabilities … start the daemon built with the `crdt` feature". A
concurrent `cargo test --workspace` in a DIFFERENT WORKTREE, at default
(non-`crdt`) features and the same `CARGO_TARGET_DIR`, had overwritten
`target/debug/pmacs` mid-sweep. Confirmed with `pgrep` while it was
happening, and discriminated by re-running the same three suites from
the same tree with a dedicated target directory: 41/41 green, then the
whole configuration swept again there. Recorded in the handoff's
standing hazards, beside the feature-blindness rule it rhymes with: a
feature-flavored binary is a shared mutable file, not an artifact
private to your invocation.

The clean re-sweep left ONE failure, and it is recorded rather than
rerun away: `lsp_dispatch_seams_acceptance acc33_...`. It is a new
incident by the registry's rules, and it is not attributable to this
branch on a STRUCTURAL argument rather than on its green rerun — the
only Rust change lives in `#[cfg(test)] mod tests`, which compiles into
the lib test target alone, so an integration-test binary linking the
non-`cfg(test)` lib is exactly what `main` builds. CI's `Test (crdt)`
job passed at the same head and runs that suite. The 15/15 repetition
set is the weakest of the five points, not the argument. No registry
row is opened: the registry judges red CI runs and keys on linked CI
occurrences.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 14:33:36 +02:00
Levi Neuwirth bd532dde6b
fix(test): retire R2 and R4 — two readiness predicates weaker than their assertions
Stage 2 of `docs/macos-ci-signal-integrity-framing.md` revision 3
(acceptance 6–9). Two test races, both the same shape: the thing waited
for was weaker than the thing asserted, so the wait could return inside
the window the assertion needs closed.

R4 — `wait_for_file` returned as soon as `fs::read` succeeded, which
succeeds on a ZERO-BYTE FILE. The probe publishes with
`open(path,'wb').write(b'1')` and `open()` creates the file before
`write()` fills it, so the helper handed `[]` to a caller asserting
`== b"1"`. It now takes the expected bytes and waits while the file
holds a STRICT PREFIX of them — the states a write in flight can be
observed in — returning anything else immediately so the caller's
`assert_eq!` stays the discriminating assertion rather than becoming a
timeout inside a helper that does not know what was expected.

All four callers pass their expectation. `wait_for_published_file`, one
function away in the same suite, gated the real-TUI smoke's
`assert_eq!(…, b"1")` on the identical predicate and is fixed with it:
leaving it would have let R4 recur under a different selector, which the
registry would then have had to judge a new incident.

R2 — the USR1 fixture waited on `ProcessEventKind::Started`, emitted at
SPAWN, not when `/bin/sh` has parsed `trap '' USR1`. SIGUSR1's default
disposition is terminate, so a signal inside that window kills the
child. The child now publishes a marker AFTER the trap and the test
waits for that marker's CONTENT (the same zero-byte trap applies to a
shell's `>` redirection). `exec` replaces the forked `sleep`, so the
group holds exactly one process and the ignored disposition survives by
POSIX rather than by the shell's fork-suppression optimization — an
unstated dependency the old fixture had, since these signals are
group-directed and a forked `sleep` is an untrapped group member.

Four witnesses, each verified by REVERTING the fix and observing the
failure rather than by reasoning about it:

- `wait_for_file_does_not_return_a_zero_byte_readiness_file` fails
  `left: []`, `right: [49]` — R4's two required fragments, verbatim;
- `wait_for_file_does_not_return_a_partial_write` fails on the torn read
  a length check alone would admit;
- `wait_for_file_returns_divergent_content_rather_than_timing_out` fails
  against an over-strict helper that waits for an exact match;
- `usr1_readiness_waits_for_the_trap_not_for_the_spawn` fails
  `left: Some("SIGUSR1")`, `right: Some("SIGTERM")` with the readiness
  wait removed. Its fixture sleeps before `trap` so the pre-trap window
  is deliberate rather than load-dependent, and it proves survival by
  the child's EXIT DISPOSITION rather than by an absence observed within
  a window.

R1 is NOT touched — referred to the async-runtime lane (Q#MCI3), because
widening its budget would make it pass and measure nothing more. R3 is
NOT touched and remains UNRESOLVED, owned by the process-signal /
reap-ledger lanes.

`docs/ci-red-signatures.md` moves R2 and R4 to a "Retired rows" section
with their dispositions and adds the rule the file needed and lacked: a
red matching a retired row is a RECURRENCE that puts the retirement in
question, never a known flake. `docs/active-work.md` carries this lane
from its first commit rather than after review asks for it.

Repetition sets, not single runs: the two `--lib` process tests 15/15,
the whole `vterm_stage2_acceptance` suite 15/15 at default parallelism.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 13:41:05 +02:00
Levi Neuwirth 2e0617fddd
review round 2: precision fixes, and a checkpoint table that cannot go stale
Three corrections.

"NO OCCURRENCE WAS EVER OBSERVED" OVERSTATED WHAT THE AUDIT CAN SAY.
Someone may well have seen one of these fail and simply not recorded it;
what is established is the absence of a RECORD. The framing now says
"linked or captured" and states the distinction explicitly, because an
audit that claims to know what nobody saw is making the same kind of
unfounded assertion this lane exists to remove — one level up.

A missing closing quotation mark in acceptance 3.

THE LANE CHECKPOINTED THE REVIEWED HEAD, WHICH GOES STALE ON THE NEXT
PUSH — the exact mechanism by which #171 became invisible while it
drifted 153 commits. Recording one head is a snapshot; what a resume
ledger needs is the sequence. It is now a table of head -> CI run ->
result, newest last, with the rule stated: the branch tip is
authoritative over any row, verified by `git rev-parse`, because the
table is written by hand and the tip is not.

Verified: fmt, diff-check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 11:11:57 +02:00
Levi Neuwirth f76897c284
review round 1: audit notes are not rows, and R2's signature was weakened
Five corrections, one blocking.

BLOCKING — the contract, not the implementation, was what needed
changing. Revision 2's acceptance 3 offered a binary: carry an incumbent
in with a signature and evidence, or remove it as never substantiated.
Stage 1 shipped a THIRD state because both incumbents are neither. The
framing is now revision 3 and names all three states, because a
governing criterion that says "two" while the branch does five is the
framing describing something that does not exist.

"MECHANISM NAMED" OVERSTATED THE EVIDENCE, and the phrase is retired.
The a33 audit proves an assertion string exists; the m6_8 audit proves a
test is timing-based. NEITHER ESTABLISHES A FAILURE MECHANISM — no
occurrence was ever observed, so nothing is known about how, or whether,
either fails. They are now AUDIT NOTES A1/A2 rather than registry rows,
with `R`-numbers reserved for signatures carrying linked evidence. The
distinction is not row-versus-weaker-row: a row says "this was seen,
here is the evidence", a note says "someone recorded a belief and no
occurrence backs it". Both remain unmatchable, so a red in either test
is a new incident.

R2's SIGNATURE WAS WEAKENED AND IS RESTORED. Splitting
`leader=exited(signal SIGUSR1)` into `leader=exited(` plus `SIGUSR1`
would match a child that exited by some OTHER disposition while SIGUSR1
appeared elsewhere in the output — precisely the name-style
over-matching this registry exists to refuse, reintroduced one level
down as fragment-style over-matching. It is one exact fragment again,
and the row says why.

THE HANDOFF STILL ISSUED LIVE IMPERATIVES. Its historical block opened
with "rerun isolated before treating a sweep failure as a regression"
and closed with "rerun the test alone before investigating", so the
supersession note I added sat between two instructions it contradicted.
Both are rewritten as record: the block now reports what that lane
OBSERVED, and the retired instruction is marked retired with its reason
— an isolated green reproduces nothing about a load-sensitive failure.

#215 HAD NO LANE, in the file that requires one for every open PR and
records why: #171 drifted 153 commits while invisible there. That is the
same defect, caught in review rather than 153 commits later. The lane
now carries branch, base, PR, reviewed head, what Stage 1 ships, the
verification, what Stage 2 owes, and the recovery worktree command. Both
snapshot headers are bumped.

Also narrowed a claim the PR body overstated: the registry is NOT
macOS-only. All four EVIDENCED rows are macOS, which is a property of
these occurrences; A1's job is `GPU Render (headless)`, which runs on
Ubuntu, and A2's job was never recorded. A future row from any job
belongs in the same table.

Verified: fmt, diff-check, clippy, --lib 1896, --lib --features crdt
2081.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:52:07 +02:00
Levi Neuwirth d33bf4df78
docs(ci): the red-signature registry, and an audit that found no immunity
Stage 1 of the macOS CI signal-integrity lane. `docs/ci-red-signatures.md`
is now the single authority for judging a red CI run.

NOT NAMED "FLAKES", DELIBERATELY. One of its rows is a possible product
defect, and a filename calling it a flake would confer immunity the
evidence does not support.

A ROW MATCHES ON SIGNATURE, NEVER ON TEST NAME. All three of selector,
job/flavor and every required fragment must hold. Fragments are
normalized rather than pasted: PIDs, elapsed times and rendered
OS-error suffixes vary between runs, so a verbatim key would match
nothing, and where a fragment lists alternatives (ESRCH / "No such
process") those are one condition rendered differently by platform. A
failure in a listed test WITHOUT that row's fragments is a new incident.

The process test is why that rule exists: it produced TWO signatures
with different mechanisms and different causal status, and only one is a
test bug. Four incidents, three tests, four signatures — the registry
counts signatures.

THE RERUN RULE IS REPLACED, NOT SOFTENED. A green rerun establishes
INTERMITTENCE ONLY — never environmental cause, harmlessness, or
retirement. The same signature again is a second occurrence and stays
blocking pending investigation or a merge-base control. A different
signature is a new incident.

RETIREMENT IS CAUSAL. A test race retires by hardening that removes the
mechanism plus a discriminating witness; a measurement-design row by its
owning lane replacing or justifying the measurement; an unresolved row
by diagnosis and disposition. Main-branch greens accumulate as
occurrence evidence and retire nothing.

THE AUDIT FOUND A THIRD CATEGORY the framing's acceptance 3 did not
anticipate. It said each incumbent is either carried in with a signature
and evidence, or removed as never substantiated. Both incumbents are in
between: the tests are real and the mechanisms plausible — a33's "blue
pixels" is a genuine assertion string at pmacs-gpu/src/main.rs:17973,
and m6_8 exists and is timing-based — but NEITHER HAS A LINKED
OCCURRENCE. Deleting them would discard a real observation; carrying
them as peers of the evidenced rows would grant exactly the reputation
this lane exists to deny.

They are recorded as "mechanism named, no occurrence recorded", with it
stated that nothing there confers known-flaky status and that a red
matching one is a FIRST recorded occurrence to be investigated. R6 goes
further: with no signature ever captured it CANNOT BE MATCHED AT ALL, so
a red in that test is a new incident by default. That is the correct
outcome for an entry that never carried evidence.

LIVE POLICY IS CENTRALIZED; HISTORICAL EVIDENCE IS NOT MOVED. The
handoff's hazards rule becomes a pointer. Its landed-lesson block at
§5 keeps its own evidence and gains a note that the registry's rerun
rule supersedes "rerun isolated". active-work.md's two mentions are
verification records for the reap-ledger and bottom-panel lanes and are
left alone; only the CI-weakness block's triage half points at the
registry, because the job-cost question there is genuinely separate.

Verified: fmt, diff-check, clippy with and without crdt, --lib 1896,
--lib --features crdt 2081, pmacs-protocol 19, m4 149, required GPU 221.
All three tests the registry names pass locally on Linux, which is
consistent with every row being macOS-only and is not evidence about
any of them.

Docs only. Stage 2 (hardening) is a separate PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 22:55:25 +02:00
Levi Neuwirth b2aa16d6ed
docs: retire the bottom-panel lane — Arc 7 is done
Rule 4 removes a lane when its ARC is done AND its durable facts are in
docs/agent-handoff.md. Both hold: #213 merged, and §1 carries the five
facts Stage 3 produced. The lane goes; the distribution and CI-CRDT
lanes stay, rewritten, because each keeps named follow-ons.

The canonical base and the RECOVERY FLOOR both advance to f186253. The
floor moving with the base is this file's own rule, and the reason is
worth restating: a check that accepts an older commit than the declared
base passes on a tree the rest of the file does not describe. The three
places that named cfc1710 — the base line, the prose, and the `git log`
verification step — now name f186253 together, because a floor that
advances in one place and not the others is worse than one that does not
advance at all.

TWO CI WEAKNESSES STAGE 3 EXPOSED, recorded in the crdt lane that owns
that job rather than lost with the lane being retired:

  * CI does not pass --no-fail-fast, so a multi-suite break reports as a
    single-suite one. #213's first crdt failure showed ONE suite where a
    local --no-fail-fast sweep of the same tree showed THIRTEEN. The
    Stage 3 census hit the identical trap and recorded it; CI has it
    too.

  * The crdt job pairs the heaviest build — lavapipe, full workspace
    with crdt, the largest test count — with real-PTY smokes on
    5-second deadlines. #213 saw two different such suites fail on two
    runs of the same commit; one was a real regression, the other was
    load. That ambiguity is the actual problem: a job where noise and
    signal look alike trains people to rerun rather than read.

Neither is a defect and neither is fixed here. They are decisions about
what the crdt job should cost, and they belong to whoever picks that
lane up.

Two ops lessons move to the handoff §5, both learned the expensive way
on #213:

  * A LOCAL SWEEP IS BLIND TO WHICHEVER FEATURE CONFIGURATION IT DOES
    NOT BUILD. Stage 3's census and every verification sweep ran
    `--features luajit` without `crdt`, so compile_mode_crdt_acceptance
    reached CI broken. Sweep both configurations before calling a corpus
    green.

  * "MY CHANGE MADE THIS FRAGILE" IS A DIFFERENT FINDING FROM "THIS WAS
    ALWAYS FLAKY", and only one of them is yours to fix. Two CI runs on
    one commit failed different PTY/GPU suites — the load-sensitivity
    signature, on suites the flake list already names. The tell that it
    was neither: the failures kept landing on GPU TERMINAL tests, and
    terminal placement was what the PR changed. Ask which tests and why
    those, before reaching for a rerun.

Docs only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 19:08:39 +02:00
Levi Neuwirth 480158b460
docs: record Arc 7 complete, and what Stage 3 taught
Branch-plan step 6. The lane, the handoff, COHERENCE, and the framing all
still described Stage 3 as ahead.

COHERENCE §14's scorecard row and §20's Priority 5 both said the adopter
default flip was what remained on the panel. It is done, so the panel
half of P5 is closed and the TREE PRIMITIVE becomes the arc's successor —
§14 grades Tree ✗, and DAP's variables view is its next would-be
inventor. That sequencing is now stated where the next framing doc will
read it rather than living in a conversation.

The lane is RETAINED, not removed, because the PR has not landed; rule 4
removes it once Stage 3 merges and its facts are in the handoff. Both
conditions are half-met, and recording that honestly is cheaper than
discovering later which half.

Five durable facts move to the handoff §1, each of which cost a review
round or a defect to learn:

  * A VISIT FROM A PANEL MUST NEVER USE THE RAW SWITCH.
    `pmacs.window.switch_buffer` replaces the buffer in the ACTIVE
    window, so from a panel it clobbers the panel itself. Q#BP11c is the
    contract: after RET, `M-,` must FOCUS the still-present panel rather
    than clone its buffer into the document — and an assertion on the
    active buffer NAME alone cannot distinguish those two outcomes.

  * AN OPT-OUT THAT DOES NOT SURVIVE REPLAY IS NOT AN OPT-OUT. Anything
    that replays a stored invocation must store the escape hatch with
    it, or the escape hatch is a one-shot that silently reverts.

  * Compile's chords are PANEL-LOCAL, deliberately, and `M-x
    compile.kill` still reaches the slot from anywhere. A global chord is
    a command-surface decision framed separately.

  * Two `q` mechanisms coexist by design — presentation history chains in
    the side slot; `p.prev` prevents raw-switch and capability-fallback
    loops. Neither supersedes the other.

  * A CAPABILITY FALLBACK MUST STRIP THE QUIT ACTION TOO, not just the
    side parameters. A quit action stranded on a document window makes a
    later `q` try to restore a presentation that never happened.

The framing reaches revision 4 with the three defects the flip exposed
(§1.7a–c) and the census lesson (§1.6d): it counted FAILURES, not
CAUSES, and the two differed by an order of magnitude — thirteen listview
failures were one missing frame-geometry declaration. Read a census as
"how many assertions move", never "how many decisions are required".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 11:18:04 -04:00
Levi Neuwirth 8d14e6a334
docs(stage3): make the lane portable and retire its superseded state
Review finding P1: Stage 3 was neither portable nor accurately
recoverable. `bottom-panel-stage3` had no upstream and no
`githubsucks/` ref, while docs/active-work.md still described Stage 3 as
"not started" — so a reader on another machine would have found a lane
claiming nothing existed, and no way to fetch the two commits that did.
That is precisely the failure this ledger records as its own rule:
"When a PR is opened, give it a lane", learned when #171 drifted 153
commits while invisible here. A branch without a PR is the same hazard
with less warning.

The lane now carries branch, base, the two completed steps with their
hashes, what remains, the verification at this head, and a recovery
command.

Review finding P2: the framing retained state the census superseded. It
still said "Nothing has been implemented or measured" and that the
blast-radius measurement "has not been taken", directly contradicting
§1.6b two screens below. Both bullets are rewritten to point at the
measurement rather than deny it, and the dired question is marked
DECIDED rather than left open.

Also records, in both documents, the thing step 2's original commit
message overstated: it is DEFAULT-PRESERVING WITH ONE INTENTIONAL
NORMALIZATION, not "behaviour-preserving". Every adopter kept its
default and the suite is byte-identical to baseline, but terminal's
invalid-input behaviour moved on purpose — and the sweep could not have
caught it, because every existing assertion passes an unknown STRING,
which takes the same path under both designs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:35:05 -04:00
Levi Neuwirth 7bfb238474
docs: absorb the v1.1.0 release, and correct what it made stale
Two parts, one intent: record the release, and fix the user-facing
surfaces it falsified.

ABSORPTION. v1.1.0 shipped and nothing recorded it. docs/active-work.md
had no distribution lane, the handoff anchor still named c5f7501, and
COHERENCE.md — a REQUIRED framing input — still asserted "zero release
machinery exists" and graded journey step 1 as source-build-only. Both
are now false, and a framing doc written against them would have been
written against a lie.

  * §0 scorecard: step 1 Partial -> Works; §17 Missing -> Partial; the
    journey row moves off "steps 1, 11 and 12 remain the thin end" to
    name 11 and 12 only.
  * §17 ground truth rewritten: what Stage 1 shipped, and the nine
    things it explicitly did not.
  * §20 Priority 8: "State: zero" -> Stage 1 shipped, with the blocker
    it named ("every other priority's value is invisible until this one
    exists") recorded as LIFTED. Its next increment is a DECISION about
    channels/update/signing, not a queued plan — worth stating so nobody
    treats Stage 2 as pre-approved.
  * A distribution lane in active-work.md, rewritten-not-removed because
    the arc is not done.

Five durable facts move to the handoff §1, each of which cost something
to learn:

  * a release build can produce FIVE binaries and three must never ship;
    layer-2 exclusion is load-bearing, demonstrated when target/release
    still held all three after building only two;
  * `env!("CARGO_PKG_VERSION")` expands in the crate being COMPILED, and
    three correct tests could not fail while two crates shared a number;
  * pin release runners, and assert the glibc floor from the binary
    rather than trusting the pin;
  * a tag pushed before its workflow reaches the default branch does
    nothing, SILENTLY;
  * verify from the downloaded artifact, with a negative control — the
    1,576 `loro` strings mean nothing without the control build's zero.

STALENESS, found by reading the surfaces a new downloader hits first.

`pmacs --help` claimed the TUI was "currently the only frontend;
reserved for the M4 GUI rollout, where `pmacs` will default to the GUI".
That is not merely dated — it is false in a release that SHIPS the GPU
frontend as a second binary. Rewritten to say what -nw actually does
(name the default explicitly for scripts and wrappers), and --gpu now
states its two real preconditions: a `crdt` build, and pmacs-gpu either
beside the binary or on PATH. Both are things a downloader can get
wrong and neither was documented where they would look.

Also in main.rs: the TLS attach line said "activation in v0.2" and four
doc comments dated themselves "v0.1" or "M4+" while describing behaviour
that is still current. The behaviour claims were accurate; only the
version labels lied, so the labels are gone rather than the sentences
rewritten. One comment gained a correction it needed regardless —
FrontendChoice is IN-PROCESS dispatch and the GPU frontend is not a
value of it, which the old text implied it eventually would be.

README status block: v1.0.0 -> v1.1.0, protocol v20 -> v21, and it
pointed at docs/roadmap-2026-07.md for "current direction" — a file that
opens by calling itself a historical snapshot and redirecting to the
handoff. It now points at COHERENCE.md and the handoff, and mentions the
arcs that landed since it was last touched.

Verified: fmt, diff-check, clippy with and without crdt, --lib 1896,
--lib --features crdt 2081, pmacs-protocol 19, m4 149, required GPU 221,
and `pmacs --help` rendered and read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 18:09:59 -04:00
Levi Neuwirth 3fade5411b
docs: absorption pass at c5f7501#209 landed, and six weeks of drift
Two jobs: absorb the CI CRDT coverage lane now that #209 has merged, and
clear documentation drift that predates it and was found while reading
in on a new machine.

ABSORPTION. docs/active-work.md's lane is REWRITTEN, not removed —
rule 4 removes a lane when its ARC is done, and three named follow-ons
remain. It shrinks from 10,441 to 3,037 characters because rule 3 puts
durable architecture in the handoff and leaves only volatile state here.
The follow-ons, each with its reason:

  * the macOS crdt leg, deferred pending first-run evidence that NOW
    EXISTS and is favourable — the non-crdt macOS legs pass at 3,474,
    thirteen fewer than ubuntu's 3,487 purely from cfg-compilation of
    the Linux-gated process tests, and no crdt-specific failure appeared
    anywhere;
  * the --lib --features crdt flake, which did NOT reproduce in #209's
    runs — but every run was --test-threads=1 and the trigger was seen
    under PARALLEL load, so that is consistent with the drain_until
    hypothesis rather than evidence against it. Its own PR: a
    product-defect hypothesis, where all of #209 was configuration;
  * the crdt_replica serde default, a third default mechanism that
    disagrees with the Default impl in non-CRDT builds, untestable
    without a self-describing format this crate does not depend on.

Five durable facts move to the handoff §1, each of which contradicted
something previously recorded: gpu-render runs a DIFFERENT PACKAGE than
the suites the old fix-shape wanted moved there; PMACS_REQUIRE_GPU is
absent from two of the four GPU suites; m10_10_perf is a CI-default
tripwire rather than a bench; --keep-going is what makes a clippy run an
inventory instead of a lower bound; and a feature can matter to a crate
a per-test census scores as unaffected.

DRIFT, all of it predating this lane and all found while reading in:

  * COHERENCE.md §20's Priority 1 still read "runs to step 5; thin from
    step 6" and called Stage 1b-3 "in flight", while §0's scorecard said
    step 10 and 1b-3 had landed. §20 is a REQUIRED FRAMING INPUT per
    CLAUDE.md, so a stale priority annotation there does not just sit
    wrong — it misdirects the next framing doc written against it. Its
    arc-cuts list carried the same two errors.
  * docs/agent-handoff.md §1 described Discovery Stage 1 and Journey
    1b-3 as "IMPLEMENTED, PR open" while §1a, twenty lines above,
    correctly recorded both as merged. A file that contradicts itself is
    worse than one that is merely behind.
  * docs/side-quest-backlog.md, compiled 2026-07-14, listed #123 as an
    open PR (merged 2026-07-21), #137 as "in review" (merged), and
    HTML/CSS as an unshipped grammar (#146 shipped it AND its injection
    consumers). It is the cross-cutting index, so a stale entry sends
    someone to build something that already exists. Its north-star
    section now says plainly that every original item has shipped and
    that the file no longer sets direction — COHERENCE.md §20 and the
    handoff §1a do, and both postdate it.
  * docs/json-yaml-framing.md's status line advertised "PR #123 open and
    awaiting review" for six weeks after that PR merged. Corrected in
    the status line only; the revision-4 body is the as-reviewed text
    and is left unedited.

No code, no workflow, no protocol change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 13:56:23 -04:00
Levi Neuwirth 9f28816ce2
docs: confirm the predicted CI counts, and re-record the head_sha trap
Acceptance 8 in its strongest form. The ledger predicted the post-round-2
job totals from the census — 3,766 for Test (crdt) and 3,487 for
Test (ubuntu/luajit) — and run 30706324644 @ 71a1ebd reported exactly
those. Predicting the count before the run and matching it says more
than reconciling one afterwards: a vacuous leg would have landed near
the non-crdt job's figure.

Also records two readings that would otherwise be misread:

The macOS legs report 3,474, thirteen fewer than ubuntu's 3,487. That is
cfg-compilation, not lost coverage — the Linux-gated process tests
(setsid, the `bash -m` job-control corroboration) are not built there
rather than skipped.

And the head_sha trap bit again. Round 1's run (30705916037 @ 6519bc3)
was CANCELLED, not green: round 2's push superseded it, which is the
concurrency group working exactly as its comment in ci.yml describes. A
`gh pr checks` summary polled around that moment reported the PREVIOUS
run's results — with entirely plausible timings — and round 1 was
briefly reported as passing on that basis. This ledger already carried
the lesson from #178 ("verified by head_sha, not by the check summary").
It recurs because the wrong answer looks exactly like the right one, so
it is now recorded against this lane too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:58:38 -04:00
Levi Neuwirth 71a1ebd4b7
review round 2: assert the CRDT capability defaults, not their round-trip
Review finding (P2), and it is this lane's own defect class one level
down. Round 1 added a `-p pmacs-protocol --features crdt` step so that
crate compiles both ways. That step EXECUTED
`InstanceCapabilities::default` in both configurations and asserted
NOTHING about it: the crate's only use of that value is a transport
round-trip, and a round-trip is invariant to the values. An all-false
default — or one whose three fields disagreed with each other — encodes,
decodes, and passes identically in both builds.

Running code is not testing it. That is the same sentence this whole
lane is about, and round 1 committed the smaller version of it while
fixing the larger one.

Three tests now pin the defaults, and the split is the point:

  * under `crdt`: multi_frontend, crdt_replica and semantic_render all
    default true. Advertising false on a CRDT build would strand every
    frontend in single-frontend mode.
  * without `crdt`: all three default false. Advertising true would be
    wire-protocol false advertising — those code paths are
    conditionally compiled out.
  * FrontendCapabilities::default is all-false in BOTH builds, and this
    test is DELIBERATELY NOT feature-gated.

That third one pins an asymmetry nothing else did.
FrontendCapabilities derives Default and is feature-INVARIANT, while
InstanceCapabilities is feature-DEPENDENT. It is load-bearing rather
than an oversight: an instance advertises what it can do, a frontend
OPTS IN through the negotiation handshake, and a v1 frontend has no
local CRDT state regardless of how the crate it links was compiled.
Making the frontend side track the feature would have frontends
claiming support they do not have. A future edit that "makes them
consistent" now fails a test that says why not to.

All three fields are asserted separately rather than by comparing whole
structs, because they track one `cfg!` and a change flipping only some
of them is exactly the regression worth catching.

Bite-verified rather than assumed: mutating `multi_frontend` to a
literal false gives `FAILED. 18 passed; 1 failed` with the expected
assertion message; restoring returns 19/19. Both configurations now
report 19 tests, up from 17, with the correct cfg-gated test running in
each.

Left untested and recorded instead: `InstanceCapabilities::crdt_replica`
carries `#[serde(default = "default_true")]`, a THIRD default mechanism
that is unconditional and therefore disagrees with the `Default` impl in
a non-CRDT build. Exercising it needs a self-describing format and this
crate's only serde dependency is postcard, which is not one. Adding
serde_json as a dev-dependency to test a divergence this lane did not
introduce is scope creep.

Also corrects a note that round 1 made stale: the ledger's "do not
subtract the two jobs' totals" figures (3,485 and 3,747) were measured
BEFORE round 1 added the protocol step, and round 2 adds two tests to
that crate. Expected totals are now 3,487 and 3,766. The root-package
census is untouched at 3,467 / 3,746 — the new tests live in a sibling
crate, which is precisely the region scripts/feature-census cannot see.

Verified: fmt, diff-check, clippy on pmacs-protocol in both feature
configurations, and workspace clippy --features crdt --keep-going.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:37:19 -04:00
Levi Neuwirth 6519bc3461
review round 1: record PR #209's state, and close a census blind spot
Review finding (P2): docs/active-work.md and docs/agent-handoff.md both
still said "PR not yet open". This file's own rule is that an open PR
gets a lane recording its state — "When a PR is opened, give it a lane",
written after #171 drifted 153 commits while invisible here. Both now
carry PR #209, its six commits, and the first CI run.

That run is worth recording rather than merely citing, because it
discharges the one risk the framing could not close locally. ALL 14
CHECKS GREEN, including both new jobs (Test (crdt) 12m20s, M10 Perf
Gates (crdt) 5m40s) and the macOS/luajit leg that is the documented
flake surface. This was the first time in the project's history that
any of these tests executed in CI.

ACCEPTANCE 8 HOLDS AGAINST THE REAL RUN. Test (crdt) reported 3,717
passed / 0 failed / 30 ignored: the 3,746 all-targets census (with
basedpyright NOT skipped, as it is locally) plus one doc test, less the
30 ignored. The job demonstrably compiled and ran the crdt corpus rather
than reporting green over nothing — which was the whole point of writing
that criterion while the local sweep was already green.

A trap for the next reader is recorded with it: DO NOT SUBTRACT THE TWO
JOBS' TOTALS. Test (ubuntu/luajit) reports 3,485 and Test (crdt) 3,747,
a difference of 262 rather than 279, because the jobs run different
SETS — the non-crdt job adds pmacs-protocol's 17 tests. The dark count
is the all-targets comparison, 3,746 - 3,467. The wrong number looks
entirely plausible.

Chasing that discrepancy found a real gap, and one this lane's own tool
could never have surfaced: PMACS-PROTOCOL HAS ITS OWN `crdt` FEATURE.
It gates no tests, so a per-test census reports 17 either way and scores
the crate as unaffected — but it changes `cfg!(feature = "crdt")`
EXPRESSIONS inside InstanceCapabilities::default and
FrontendCapabilities::default, so those same 17 tests exercise different
runtime values under it. CI had only ever run the non-crdt ones. Closed
with an explicit `-p pmacs-protocol --all-targets --features crdt` step,
verified 17/17.

The blind spot is structural, not an omission, so it is documented at
the tool rather than fixed in it: scripts/feature-census censuses the
workspace DEFAULT MEMBER, because that is what a bare
`cargo test --all-targets` builds. Sibling crates are invisible to it no
matter what configs are passed. The durable lesson: A FEATURE CAN MATTER
TO A CRATE A PER-TEST CENSUS SCORES AS UNAFFECTED. Check siblings by
hand.

Verified: ci.yml parses at 10 jobs, script syntax clean, fmt,
diff-check, and the new protocol step green locally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:26:28 -04:00
Levi Neuwirth 57abcd940e
docs: record the CI CRDT coverage lane and its corrections
Framing revision 3, plus the ledger and handoff updates the lane owes.

docs/active-work.md's "NEEDS A LANE" block is replaced rather than
annotated: the lane now has a branch, a framing, an implementation and
verification, so the old text's premise ("no branch, no framing yet")
was the part that needed removing. Its stale figures go with it — the
273-dark reading at 74301d1 and the seven-item clippy list, both
superseded and both explicitly labelled do-not-quote in the new block.

The corrections this lane produced, all now recorded where the next
reader will hit them:

  * m10_10_perf is a CI-DEFAULT REGRESSION TRIPWIRE, not a bench. Its
    bounds are deliberately generous "to catch catastrophic regressions,
    not to verify a tight perf claim", so #[ignore]ing it to give it a
    perf job would have reduced coverage inside a coverage lane. The
    framing had this backwards through revision 1 and says so.

  * gpu-render runs `cargo test -p pmacs-gpu` — a DIFFERENT PACKAGE from
    the root-package GPU suites. The long-recorded fix-shape of "move
    the GPU-requiring crdt suites onto gpu-render, it already has
    lavapipe" does not work as written.

  * PMACS_REQUIRE_GPU is not uniform: absent from
    gpu_invocation_acceptance and gpu_initial_target_acceptance
    entirely, so it cannot serve as blanket proof the GPU suites ran.

  * The dark-test classification found THREE dispositions, not two:
    benches awaiting a job, deliberately-manual operator tests, and
    known-defect markers. Collapsing the second into the first gives a
    CI job to tests whose #[ignore] reason says not to.

  * §1.1's own target-column claim was wrong, and scripts/feature-census
    caught it. Eleven targets run with zero tests under CI's flags;
    eight of those gain tests under crdt and three are helper binaries
    with no tests in either. The framing had merged two true statements.

Acceptance 9 is revised rather than quietly met at a lower bar. The
planned deliberately-broken-test bite is replaced by the --covers
coverage assertion plus the CI count reconciliation, and the doc states
what that trades: stronger on the claim specific to this lane (the old
job STRUCTURALLY cannot see these tests), and no longer proving that a
failing crdt test turns the job red — which is generic cargo and Actions
behavior, not anything this lane changes.

The handoff gains a census hazard beside the existing sweep note,
because every trap in it was hit while writing the script and one of
them survived two framing revisions: libtest prints `name: test` with no
space before the colon, a zero-test target prints only its Running line,
and both configurations need an --ignored pass or pre-existing ignores
get attributed to the feature.

The ledger also records what is NOT established, which is the lane's
whole remaining risk: the sweep is green SERIALIZED ON A DEVELOPER
MACHINE, and the failures this lane expects are hosted-runner timing and
concurrency. That green removes the "tests are wrong" explanation and
leaves the expected one untested. It must not be quoted as evidence the
CI leg will be green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 11:00:12 -04:00
Levi Neuwirth c8f111413f docs: absorption pass at cfc1710 — the whole board, before a machine move
Nine PRs landed since the handoff's anchor (#199-#207) and it named
only four of them, so a fresh machine would not have learned that
ambient-root isolation exists, that the journey arc's 1b split
completed, or that the reap-ledger diagnostic landed. The ledger's
canonical base and recovery floor were five merges stale, and three
lanes described merged work as in flight.

Taken now because ZERO PRs are open. The ledger's own rule is never to
open a standalone refresh PR — because with several PRs open a lane
written on `main` re-conflicts at every merge — and this is the one
window where that cost is nil.

The handoff gains §1a, "Outstanding work — the whole board", which is
the point of the pass rather than a by-product: every arc against §20's
priority order with its next step, every open lane, every deferred item
attributed to the framing that parked it, and the standing hazards
someone running gates on a new machine needs — the three known flakes
by name, the basedpyright skip, the crdt sweep's build prerequisite,
and why a green a37 means nothing alone.

Two arcs completed, so per rule 4 their lanes are removed and their
facts are in the handoff: Journey Stage 1 and test ambient-root
isolation. Discovery and reap-ledger merged a stage each, so their
lanes are rewritten to the remaining plan rather than deleted — the
discovery lane now enumerates Stage 2 in dependency order and carries
the two Stage-1 facts a Stage-2 author would otherwise rediscover.

Base and floor advance together to `cfc1710`, per the file's own rule
that a floor accepting an older commit than the declared base passes on
a tree the document does not describe.

The recovery path was EXERCISED rather than asserted: from an empty
directory, clone, alias, fetch, floor check, and a lane worktree all
ran clean. The two-argument `git worktree add` still fails for a
remote-only branch, which is why every lane spells out the `-b` form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 21:03:33 -04:00
Levi Neuwirth 4d4bb22035 Merge remote-tracking branch 'githubsucks/main' into discovery-stage1-commands 2026-07-31 20:10:02 -04:00
Levi Neuwirth b3131dbf95 docs(framing): record the deliberate departure from the §7 branch plan
§7 said the classification comes first and alone, "its answer belongs in
review before any mechanical edit rides on it." The classification came
back at 342 sites across 66 of 97 files, and the whole-corpus migration
rode this PR anyway. That was a decision, not an oversight, and revision
5 records it as one so a later reader does not have to reconstruct it
from the diff.

The reasoning, in short: splitting would either leave 65 suites still
writing the developer's real data root while the seam sat unused, or ship
acceptance 12's ratchet with a ~65-file allowlist — and a ratchet
exempting most of the corpus records rather than ratchets. §7's ORDERING
is honoured (the census is the first commit); its implied SCOPING is not.

Revision 5 also records what review round 1 established about the shape
acceptance 12 needs: "a narrow, named allowlist" is not sufficient by
itself, because narrowness constrains which files are exempt and says
nothing about how far each exemption stretches. Exemptions carry counts.

`docs/active-work.md` picks up the same two facts and the PR number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 19:46:33 -04:00
Levi Neuwirth c0407dc6a7 docs(coherence): §5 substrate-without-surface -> Partial
Per §25 the audited claim this stage falsifies is updated on the
landing PR. §5's scorecard row and ground truth both said the
registration metadata was best-in-class with almost no way for a user
to reach it; eleven `help.*` commands now reach it.

The row names what is still missing rather than implying the concern is
closed: packages and workers have no discovery surface, `Command` still
has no title/category/aliases/flags, M-x rows are still bare name
strings, the predicate is still never evaluated, settings value
provenance is still absent, and there is still no help prefix key. The
original audit grade is kept inline for reference.

`docs/keybindings.md` gains the family table and records that the
commands are reachable by name and bound to nothing — the help-prefix
decision belongs to a later stage, for the reason #205 recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 19:17:22 -04:00
Levi Neuwirth 22925964d9 docs: record the ambient-isolation lane and its five-variable rule
`docs/active-work.md`: the lane moves from FRAMING OPEN to
IMPLEMENTATION OPEN, with the implementation branch and worktree and a
recovery command that names them. The framing-only worktree is spent —
its doc is on `main` (#201).

`docs/agent-handoff.md` §3: a local full-suite run needs all FIVE
storage variables controlled, not four. `PMACS_STATE_HOME` outranks
`XDG_STATE_HOME`, so naming only the XDG four leaves a
higher-precedence state override live; and a run isolating only
`XDG_CONFIG_HOME` stops the `init.lua` reads while still writing through
the real data root — every local gate run in this repo before today had
that hole. `HOME` is deliberately excluded: it is the fallback the XDG
roots already cover once set, and it separately drives `~`-expansion,
which `find_file_acceptance` pins on purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 18:49:00 -04:00
Levi Neuwirth 513a7dfa58 docs(discovery): finalize Stage 1 framing review
Correct the M-x prompt census: six of the eleven canonical commands
need a second prompt, including describe-command. Make the apropos
substring negative discriminate against fuzzy matching with a concrete
non-contiguous fixture and a no-substring precondition. Update the help
index mutation to the twelfth canonical command, and carry all three
corrections into the active-work ledger.
2026-07-31 18:28:35 -04:00
Levi Neuwirth f4359f12b4 docs(discovery): framing revision 5 — decide Q#D2 and Q#D3
No review findings at 1cc9d96; these are the two product choices the
framing deliberately left open.

Q#D2 resolves to `help.*` canonical with two forwarders. The family is
eleven commands under one prefix, so typing `help` at M-x surfaces all
of it — which is the discoverability win this arc exists for.
`editor.describe-command` and `editor.describe-setting` stay registered
as forwarders, so nothing in muscle memory or in
`docs/keybindings.md` breaks. Two forwarders are duplication of exactly
the kind §5 complains about; they are accepted as the bounded price of
not breaking documented names, and they carry a deprecation path. What
is not accepted is the split family revision 3 shipped.

Q#D3 resolves to substring. `fuzzy_score` is subsequence-based and
descriptions are long sentences, so a short query's letters almost
always appear in order — fuzzy would match nearly every command and
destroy the precision that makes apropos worth having. Acceptance 4 now
pins the decision rather than the intent: a subsequence that is not a
substring must find nothing.

Counts and pins follow the decision: the family is eleven throughout,
the `_show_help` counting stub expects eleven (the two renamed commands
are in it precisely because they were the pre-existing direct callers),
and the preservation pin now drives the forwarders through the real M-x
path — dropping them after the rename is the failure a user with muscle
memory would hit first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 18:20:41 -04:00
Levi Neuwirth 1cc9d96ba6 docs(discovery): framing revision 4 — close review round 3
Two factual corrections, both accepted and both verified in the code.

The custom completion source does not control display order. Revision 3
justified sorting the pool by claiming `Custom` candidates appear in
return order; `recompute_candidates` hands the pool straight to
`filter_and_sort`, which ranks by fuzzy score descending and breaks
ties lexically, so the source's order never reaches the user.

The sort is kept, for a reason that is actually true: `filter_and_sort`
applies `.take(CANDIDATE_LIMIT)` to the FILTERED iterator before
sorting, so when more settings match than the limit, pool order decides
which survive truncation. Registration order would make that vary with
an unrelated config edit; sorting makes it reproducible.

Read-only would not mitigate the foreign-`*help*` collision either.
Revision 3 implied it would. A buffer the user created and named
`*help*` carries no intercept of ours, so an intercept on the buffers
we create protects nothing — the renderer still matches on the name and
clears theirs. The missing guarantee is ownership identity: a private
table of buffers this module created, so found-by-name is not adoption.
`listview` carries it as `panels` and dired as its handle table; this
mechanism carries neither. Naming the wrong missing guarantee would
send a later fix at the wrong layer, which is why the correction is
worth its own paragraph rather than a word swap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 18:12:59 -04:00
Levi Neuwirth aa932f9586 docs(discovery): framing revision 3 — close review round 2
Two blocking, two major. All four accepted.

The ledger lane still said revision 1 and still carried the two claims
revision 2 refuted. The cause is worth recording: revision 2's ledger
edit was an assert-then-write block whose later assertion failed, so
nothing before it was written either — while the commit message
reported the whole change as done. This is the second time in this
project that shape has silently dropped edits. The lane is rewritten
from scratch and the result verified by re-reading the file rather than
inferred from an exit code.

`names_from` does not exist. Revision 2's completion source called a
helper nobody has written, over `pmacs.config.list()`'s descriptor
tables where `CompletionSource::Custom` consumes a sequence of strings.
Opening the prompt would have raised on an undefined global. The mapper
is now specified, and sorted — `Custom` presents candidates in the
order returned, and registration order is neither stable nor useful.

`*help*` has no read-only intercept. Revision 2 claimed one while
`show_help_text` writes with plain delete/insert, and #205 had already
recorded that this mechanism has not adopted the generated-buffer write
invariant. §3.4 now names the policies that really are shared —
reuse-by-name, wholesale replacement, the `q` binding, and the
foreign-buffer hazard — and notes that the last is precisely what a
read-only intercept would have mitigated and does not.

The naming was underspecified. With no `help.describe-command` in the
table, calling the existing `editor.describe-*` commands
"aliases-by-retention" was wrong on both halves: nothing forwards to
them and there was nothing for them to alias. They are now explicit
exceptions, the resulting split surface is named as a wart, and Q#D2 is
sharpened to the two ways out — forward the two under `help.*`, or keep
the family `editor.*` throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 18:08:58 -04:00
Levi Neuwirth 9e79b6b9a3 docs(discovery): framing revision 2 — close review round 1
Two blocking, two major. All four accepted and verified in the code
before the doc changed.

Completion does not close the free-text hole, and revision 1 said it
did. `resolve_accepted_value` returns the literal typed text whenever
no candidate is selected, so a non-matching typo still reaches
`on_accept` and the existing error path; separately, a fuzzy near-miss
can silently describe a DIFFERENT setting, which is a new failure mode
revision 1 did not name. Completion is reframed as assistance, the
acceptance pins both outcomes, and closed-set acceptance semantics are
named as Rust work rather than smuggled in as a side effect of adding
a source.

`invoke_interactive` is not the M-x path — the exact error #205
corrected one PR earlier. It rotates the interactive-command boundary
and calls the body; it opens no palette. The acceptance now states the
real path once and drives it: dispatch M-x, type the name, assert the
selected candidate BEFORE RET (accept does `session.take()`), accept,
and — for the five commands that take an argument — drive the second
prompt too. A pin that stops after the first RET has tested the
palette, not the command.

The `_show_help` seam is an output sink, not a migration seam.
`src/help.rs` has semantic renderers for command/key/buffer/mode/hook/
view and none for settings, lists or apropos, and the seam takes
already-flattened text, so a later migration still has to change each
command's subject-specific logic. The claim is narrowed to what is true
— one owner for Lua `*help*` writes — and paired with a per-subject
renderer function so the future Rust work is enumerated rather than
discovered.

Ground truth corrected: the missing-command list was eight and omitted
`list-settings` while §3.1 said nine; the site count said ten where
nine additions make eleven; `pmacs.keymap.lookup` does not return
`description` (it passes `cmd = None`); and the predicate sites cited
were `MenuItem` fields, not `Command.predicate`. The predicate
conclusion survives on the correct evidence — `src/help.rs:76` and one
assertion past `#[cfg(test)]`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 17:52:56 -04:00
Levi Neuwirth 791bde1a0c docs(discovery): frame P4 Stage 1 — the describe/list command family
`COHERENCE.md` §20 Priority 4, which §5 grades "substrate without
surface — the sharpest instance of §1.1" and calls the best
payoff-per-effort in the document. Nine describe/list commands over
introspection that already exists, picking up the family root `M-x
help` that #205 landed and documented for exactly this.

The stage adds no Rust. `pmacs.describe.*`, `pmacs.keymap.list()`,
`pmacs.command.list()` and `pmacs.config.list()` already return
everything the commands render, and `parse_completion_source` accepts a
Lua function as a `CompletionSource::Custom` — so `describe-setting`'s
free-text prompt, which today turns a typo into a status-line error
while `describe-command` has had completion all along, closes with no
new Rust either.

The consequential decision is a single rendering seam. `src/help.rs`'s
cross-referenced renderer is orphaned and the reachable Lua
`show_help_text` renders less, so nine new commands calling it directly
would turn a two-site migration into a ten-site one. Everything routes
through `pmacs.editor._show_help`, and an acceptance pin replaces that
seam with a counting stub — funnelling asserted, not merely intended.

Records that `Command.predicate` is stored and exposed but evaluated
nowhere, and deliberately does not start evaluating it: that makes
commands stop being invocable, which needs its own decision about what
"unavailable" means at each call site. A preservation pin registers a
raising predicate and asserts the command still runs, so a later stage
has to change that pin knowingly.

Adds no keybindings at all — the help-prefix question is taken once,
for the whole family, by the stage that can weigh it against the
constraint #205 recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 17:42:03 -04:00
Levi Neuwirth 08bc1a153c docs: record 1b-2 as landed; collapse the journey lane to one block
Review round 2 on #205, one finding, accepted. #204 is this PR's base,
so three places still describing it as open were simply false.

- `COHERENCE.md` §2's step-6 row: "(PR open) ends the silence" ->
  "(#204) ended the silence", and the "flips only on merge" clause is
  replaced by the reason the row is still Partial at all — a server
  that starts and then crashes is unsurfaced, which that landing did
  not touch.
- `docs/agent-handoff.md`: the 1b-2 bullet says LANDED (#204), with the
  same substituted reason.
- `docs/active-work.md`: 1b-2's per-stage block is removed rather than
  relabelled, per rule 4 — it has merged and its durable facts are in
  the handoff, which is the precondition for removal.

That last one exposed a structural problem the merge had created: 1b-2's
old lane body had come in *underneath* the unified journey header, so
the file carried two journey sections and the first one's text
described the second's predecessor. There is now one arc lane with
1b-3 nested under it as the only open stage.

Also corrects the reap-ledger lane header, which still read
"IMPLEMENTED, PR OPEN" after #202 merged. Same class of staleness,
noticed while editing the same file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 16:50:27 -04:00
Levi Neuwirth 94036774a8 fix(welcome): notify the core after writing scratch; unstale the ledger
Review round 1 on #205, two findings, both accepted.

The greeting was written straight into the registry without calling
`notify_buffer_edit`. The window's `TextView` had been indexed while
`*scratch*` was empty, and newlines are zero-width to a painter working
from a stale line index — so the first TUI frame collapsed the whole
three-line greeting onto row 0. Every buffer-text assertion passed
because the buffer content was correct; only the rendering was wrong.
The edit is now captured, the registry borrow released, and the core
notified.

The pin that would have caught it paints a real frame and asserts the
second line occupies its own row AND that row 0 does not contain it —
both directions, because a one-direction check passes when everything
collapses upward. Bitten by dropping the notify call: row 1 comes back
empty with row 0 holding the lot, and it is the only pin that fails.

Second: the project docs still described the arc as it was two PRs ago.
`COHERENCE.md` §20 called 1b-2 in flight and the welcome buffer
unstarted; its arc list said 1b-3 remained; and the ledger's journey
lane header still read "1b-2 PR OPEN" while the 1b-3 block carried a
mangled "Framing only; no code" line left by an earlier edit. All now
describe the PR-head state per §25.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 16:43:02 -04:00
Levi Neuwirth 35cc9ff0c5 merge: integrate main @ 5376af1; move the §18 and scorecard grades
The journey suite conflicted additively — step 4 from this lane, step 6
from #204 — and both are kept: 44 pins now cover steps 2, 3, 4, 5, 6
and 9.

Per §25 the audited claims this stage falsifies are updated on the
landing PR rather than deferred: the scorecard's row 18 and §18's
ground truth both read "Missing" / "missing entirely", and a welcome
buffer plus a reachable cheat sheet makes both false. They move to
Partial. §2's step-4 row stays Partial, because `C-h` still deletes a
word and there is no tutorial.

§18's ground truth now records WHY `C-h` stays as it is, so the
help-prefix question reaches the discovery arc as a stated trade rather
than an oversight: non-kitty terminals cannot disambiguate
Ctrl+Backspace from Ctrl+H, so rebinding it would break Ctrl+Backspace
on every legacy terminal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 16:01:29 -04:00
Levi Neuwirth 2927d2fec1 docs(journey): framing revision 4 — close review round 3
Two findings, both accepted.

The pinned API was crate-private while the pin was external.
`tests/journey_acceptance.rs` is a separate integration crate and
cannot call a `pub(crate)` function or match a `pub(crate)` enum, so
revision 3's acceptance 1 could not have compiled. `prepare_startup`
and `Startup` are now `pub`, which is consistent rather than expedient:
`run`, `EditorState::new`, `EditorState::open`, `install_state_dirs`
and `restore_desktop_if_armed` are already public, so the startup
sequence is public surface and this was the piece missing from it. The
alternative — keep it private and move the pin into `src/editor.rs`'s
unit tests — was rejected because §19 wants the journey row in the
journey suite.

The isolation that pin needs is now stated rather than left implicit.
`prepare_startup` deliberately calls `install_state_dirs`, which
resolves `PMACS_STATE_HOME` and XDG roots that tests cannot override
(`set_var` is unsafe and forbidden), so the pin inherits the standing
five-variable ambient-root requirement, asserts buffer content only,
and must assert that desktop restore was unarmed — a developer whose
real `init.lua` calls `desktop_mode(true)` would otherwise get a
restored scratch buffer and a silently different result.

The M-x acceptance still left its key assertion as an "or", and half of
it was impossible: `Minibuffer::accept` does `session.take()` and
resolves against the selected candidate, so after RET neither the
session nor the typed contents survive to be asserted. The framing now
specifies one observable, available before RET:
`pmacs.minibuffer.selected()` must equal exactly "help", then accept.
If the completion source selected something else the pin fails there,
naming what was actually selected, instead of passing on a help buffer
some other command produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 14:55:57 -04:00
Levi Neuwirth d8c6328872 docs(journey): framing revision 3 — close review round 2
Two acceptance holes and one doc correction, all accepted.

The real startup wiring was still unpinned, and revision 2 knew it: it
named the gap and then accepted it as residual, which is worse than
missing it. Deleting the sole `run()` call to `finalize_local_launch`
would have left every proposed pin green while shipping no welcome —
pin 1 called the seam by hand and pin 10 only proved constructors were
blank. That is the shape of a guard with no production caller passing
every direct-call test.

The fix is extraction, not disclaimer. Everything in `run()` from
`install_panic_hook` through the end of the attach-dispatch match is
terminal-free; `Frontend::new()` is where takeover begins. That prefix
becomes `prepare_startup`, which `run()` delegates to and which the
welcome pin now drives. Deleting the finalize call inside it turns the
pin red. What stays outside a test is `Frontend::new()` and the event
loop, which is where terminal takeover genuinely lives.

Acceptance 4 was not the M-x path. `pmacs.command.invoke` is the
programmatic API; M-x is `editor.execute-command`, which opens a
minibuffer with the `commands` completion source and calls
`invoke_interactive` only on accept. The pin now dispatches the chord,
enters the name, and accepts — and names the hazard that makes that
non-trivial: a selected candidate shadows typed text, so the pin has to
assert which command ran rather than trusting that some help buffer
appeared. dired refused a completion source on its own prompt for
exactly this reason.

Also corrects the #204 references: revision 2's history said it had
landed while the framing footer and ledger still called it open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 14:45:24 -04:00
Levi Neuwirth 1928a3af20 docs(journey): framing revision 2 — close review round 1
Four findings, all accepted and verified in the tree first. The three
open questions are answered and folded into the design: three lines,
name `C-c c` and `C-c t` verified from structured entries, and the
command is `help`.

The startup seam was wrong, and every acceptance criterion rested on
it. `EditorState::new()` is not the no-argument entry point:
`EditorState::open` calls it before resolving the target, the daemon
constructs one too, user config runs inside it, and desktop restore
happens much later inside `run()`'s `RunLocal` arm. Greeting from
`new()` would greet a daemon, greet before a file argument replaced the
buffer, and precede anything config or a restored session put in
`*scratch*`. The stage now adds a launch-finalization seam called right
after `restore_desktop_if_armed`, using the `had_file` signal already
threaded to that point. Its untestable part — that `run()` calls it —
is stated rather than papered over, and bracketed by pins proving the
seam works and that no constructor greets on its own.

The step-2 pin amendment is withdrawn. Revision 1 analysed a
status-line welcome and then chose `*scratch*` but kept the amendment,
which was an internal contradiction. The status stays empty so the pin
stays true, and "no error text" has no defined predicate over an
unstructured status string — replacing an exact check with a fuzzy one
would weaken the ratchet to buy nothing.

The claim that this stage carried no §25 obligation was false. The
scorecard's row 18 and §18's ground truth both read Missing, and a
landed welcome plus reachable cheat sheet falsifies both: they move to
Partial on merge while §2's step-4 row stays Partial. A stage can be
too small to flip its journey step and still falsify a "missing
entirely" grade.

Acceptance 2 could not have been implemented as written. Scraping key
sequences out of prose is ambiguous — `M-x help` mixes a chord with a
command name, and `C-c c` is two chords whose boundary the text does
not mark. One structured entry list now both renders the text and
drives the `pmacs.keymap.lookup` checks, and `M-x help` is exercised
through the real palette path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 14:38:45 -04:00
Levi Neuwirth 02645a71b0 docs(journey): frame Stage 1b-3 — say something when the editor opens
The last of the 1b split. Journey step 4 and `COHERENCE.md` §18: a
fresh `pmacs` greets the user with an empty buffer, an empty status
line, and no indication that `M-x` exists — the sole discovery
affordance in the product is knowing to press it.

Three findings shape the design:

- The existing step-2 ratchet pin collides with any welcome. It asserts
  `status.is_empty()` while its own message says "reports no error" —
  the same predicate only while nothing writes a non-error status at
  startup. The framing corrects the assertion to its message's claim
  and names that as the one existing assertion the stage changes,
  rather than burying it in a diff.

- `C-h` is not free. It is bound to word-delete because non-kitty
  terminals cannot disambiguate Ctrl+Backspace from Ctrl+H — both
  produce byte 0x08 — so rebinding it to a help prefix would break
  Ctrl+Backspace on every legacy terminal. §2's step-4 row calls it an
  oversight; it is a deliberate trade with a stated rationale. The
  help-prefix decision is deferred to the discovery arc with the
  constraint recorded so it is inherited rather than rediscovered.

- A `*help*` buffer mechanism already exists over `src/help.rs`, so the
  welcome can point at something real. Its two gaps are recorded rather
  than inherited silently: it writes with delete/insert instead of
  `set_generated_contents`, and it is found by name.

The welcome deliberately does NOT use `set_generated_contents`: that
lifts read-only, discards history and marks the buffer generated, all
wrong for the buffer step 5 requires the user to type into immediately.

Step 4 stays Partial — the row names a welcome, a cheat sheet and
`C-h`, and this closes the first plus a minimal second — so this is the
first 1b stage with no landed-evidence obligation on merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 14:28:19 -04:00
Levi Neuwirth bc3a465c9e merge: integrate main @ 1f290d5; discharge #203's doc-flip obligation
Stage 1b-1 landed as #203, so the four places it deliberately left
saying "in flight" are flipped here rather than in a standalone docs
PR: this branch already touches all three files, and a separate PR
would re-conflict on every merge.

- `COHERENCE.md` §2's step-9 row: Partial -> **Works**.
- §2's keybinding-inversion paragraph: all three examples answered. The
  quote itself is deliberately unchanged — it names a bias, and three
  fixes do not retire a bias.
- §20 Priority 1 and the arc list: 1b-1 landed, 1b-2 in flight, 1b-3
  remaining.
- `docs/agent-handoff.md` §1: IMPLEMENTED -> LANDED.

Conflicts were additive on both sides and are resolved keeping both:
the journey suite carries step 9 and step 6 (34 pins), and §24 keeps
both drift entries — the `ProjectKind::Cargo` naming error and §1.2's
wrong frequency note.

The two journey lanes are unified into one arc lane rather than one
being deleted. Rule 4 removes a lane when its ARC is done, and the
journey arc is not: 1b-2 is in flight and 1b-3 is unframed. Stage 1a
and 1b-1 are summarized there with their facts in the handoff, which is
rule 4's precondition satisfied rather than deferred.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-30 22:13:34 -04:00
Levi Neuwirth 2d1812c431 feat(lsp): say when the language server did not start — journey step 6
Implements `docs/journey-stage1b2-lsp-guidance-framing.md` (approved at
revision 4, after three review rounds). Lua, tests and docs; no Rust
change and no protocol change.

`COHERENCE.md` §1.2's canonical silence: a preconfigured server that is
not installed failed with no status message, no record and no modeline
marker, while tree-sitter highlighting kept working and masked it. Now
the status line names the command, the language and the errno; the
modeline reads `LSP:!` instead of nothing; and `M-x lsp.status` renders
a durable `*lsp*` panel.

Half of this was already built. `status_buffer_text()` and
`last_error()` have existed since M4.8, exposed to Lua and tested, with
no production caller and no buffer to render into — several doc
comments already referred to "the `*lsp*` buffer" as though it existed.
The reporting shape was likewise already adopted twice inside
`lsp.lua`; the canonical case was silent because nobody had converted
it.

Three tables with three lifetimes, because one cannot do the job:
`reported` is never cleared and includes the command, so repointing at
another missing executable reports again; `failures` is cleared by a
successful spawn so the panel goes quiet on recovery; and a
buffer-keyed projection feeds the modeline, because that provider runs
for every window on every paint and deriving an affinity key inside it
would invoke root resolvers during painting.

The memo is on the report, not the failure: the spawn is still
attempted on every file open, so installing the binary mid-session
recovers with nothing to invalidate.

Adds `tests/lsp_spawn_guidance_acceptance.rs` (16 pins) and a step-6
row to the journey ratchet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-30 22:07:30 -04:00