From 3fc12ac4c272cbeaa412653f9281add15e273072 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Tue, 11 Aug 2026 19:33:09 +0200 Subject: [PATCH] docs: Stage 0 --- retire five merged lanes, after re-homing what was missing Rule 4 retires a lane only once its durable facts have reached `docs/agent-handoff.md`, so that was CHECKED per lane rather than assumed. Four of the five were already covered --- the `commit_to` capture-at-request-time lesson (#231), per-worktree gate target dirs (#225), the `build-crdt` step and the `gpu_invocation` signature (#228/#229), the v23 frozen-variant lesson (#228). One was NOT: `texlab` appeared ZERO times in the handoff, so #230's lane could not be retired yet and its facts are added first --- LaTeX is served by texlab, its root comes from an upward `.texlabroot` walk verified in texlab's own source, and `.git` is deliberately excluded because a repository root is the wrong answer for a multi-file document. The handoff's Fake LSP mode list was stale in the same breath: it named four modes when the binary has twenty-eight. Enumerated from the binary and grouped, because a stale mode list is precisely how a test ends up covering the shape next to the defect --- which is the failure this lane has now watched happen twice in the file-watcher work. Retired: #224, #225, #228's PR-specific block, #230, #231. That is 737 lines of ledger describing work that has shipped. #228 is the one that is NOT a deletion. Its PR block is retired INTO the standing Discovery lane, which is rewritten to "Stages 1 and 2 merged; later work remains" --- deleting that lane on the strength of one merged stage would have dropped four named pieces of open work. While rewriting it, its first open item turned out to be what Stage 2 actually shipped (richer M-x rows over v23), so it is removed and the rest renumbered: a completed item sitting in an open list is the same staleness this commit exists to clear, one level down. Left deliberately: #229's and #232's lanes, which are already labelled MERGED accurately. They are Rule 4 candidates but not STALE, and widening this commit to them would mix "the ledger lied" with "the ledger is verbose". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 774 ++---------------------------------------- docs/agent-handoff.md | 27 +- 2 files changed, 45 insertions(+), 756 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 2e0dc00..fbea841 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -301,61 +301,6 @@ is listed at the commit that does it, not promised here. suite for its PR is `cargo fmt --check`, `git diff --check`, and nothing else it can meaningfully run. -## `scripts/gate` — PR #225 OPEN (build tooling) - -**PR #225** — https://github.com/levineuwirth/pmacs/pull/225. Written -**after** the PR existed, again, and again because review asked. Two -lanes in a row have now been added late; the correction from #171 and -#215 is not sticking, and recording that is more useful than a -back-dated block that pretends it did. - -- **Branch `gate-script`**, base `githubsucks/main` @ `b833b13` (the - #224 merge). **`githubsucks/gate-script` is the authoritative tip** — - the ref, not a SHA. Recover with - `git fetch githubsucks && git checkout gate-script`. -- **Framing `docs/gate-script-framing.md`**, approved at revision 4 - after four review rounds; revision 5 records two safety defects found - against the implementation, not the design. -- **Scope:** `scripts/gate` (per-worktree `CARGO_TARGET_DIR`, five - ambient roots, durable per-gate logs, the fixed gate suite), - `tests/gate_script_acceptance.rs`, and a handoff §3 rewrite pointing - at the script while §3 keeps policy and acceptance-suite selection. - No `src/`, no crate, no manifest, no protocol. -- **Verification:** 15 acceptance tests over the no-gates paths, each - isolated by `PMACS_GATE_TARGET_ROOT`. Mutation-tested; the two prune - guards are redundant by design and only fail the test when **both** - are removed, which is recorded in the test itself. Observed real runs - confirm failed-gate naming, log paths, ambient creation and reaping, - and distinct log directories per run. - -**GATE STATUS — R8 RESOLVED.** This lane was blocked because -`scripts/gate` exited 1 on a clean tree: **R8** failed `m4_acceptance` -and therefore the sweep. That was never a footnote — #225 is the lane -that makes the gate suite authoritative, and a tool shipping with its -own gate red teaches the opposite of what it exists to teach. - -**R8 was fixed and retired in #226** (`dcb852e`), which bounded the LSP -fixture's project detection. This branch is rebased onto it. - -**RE-GATED 2026-08-09: `scripts/gate` exits 0.** All nine gates green in -one command — fmt, clippy, `--lib`, `--lib --features crdt`, both named -acceptance suites, `m4_acceptance`, `-p pmacs-gpu`, and the full -workspace sweep. That is #225's own acceptance criterion, and it is the -first time the tool has passed the suite it exists to run. - -**Rebase resolution, per the standing rule that #226's R8 documentation -wins.** Three conflicts, all in R8 text this branch had written while -the row was still an open investigation: two in -`docs/ci-red-signatures.md` (both resolved to #226's retired row, this -branch's pre-fix copy dropped), and the framing-doc pair -(`e71e1bd` added it, `7cfba73` removed it — both **skipped**, since they -are net-zero here and `main` owns the file authoritatively; replaying -the second would have deleted `main`'s copy). Two now-stale lanes were -also removed: this branch's "R8 NEEDS A LANE" investigation block, and -#226's own lane, which Rule 4 retires now that it has merged — its -durable facts are in the retired registry row and the handoff §6 -census. - ## Git integration — STAGE 1 MERGED as #227; Stage 2 must be scheduled alone **PR #227 MERGED 2026-08-11** (`main` @ `b867f64`), after five review @@ -390,401 +335,6 @@ serialization rule on wire changes. No branch, no framing yet. fails the day that job gains a macOS leg, the same way `g6_2` did (handoff §1: macOS cannot hold a non-UTF-8 filename). -## Destination capture (Q#JR14 generalization) — PR #231 OPEN, revision 9, cleared to merge - -**PR #231** — https://github.com/levineuwirth/pmacs/pull/231. #227 -blocks on this lane. - -The mechanism landed at `0efc8c0`; review found a correctness blocker; -`ca72461` implemented **revision 7**, which review then **also** -rejected; `469d5c8` replaced it with **revision 8** and its §3 -enumeration is **performed and recorded in the framing**; review then -found a hole in revision 8's guard **scope** and the commit below closes -it as **revision 9**. - -**The macOS red that blocked this lane, and how it was cleared.** Both -CI attempts at `4654b94` failed `a_pty_resize_blanks_the_host_before_repainting` -on `Test (macos-latest / luajit)`. A control experiment was run at the -exact base commit `0190102`: **five valid observations, all green on -both macOS flavours**, against the branch's 0/2 — 1/C(7,2) = 4.8% under -an equal-rate model. That implicates the branch statistically. **The -diff exonerates it mechanically**: grepping this lane's entire `src/` -diff for `full_grid|resize|resync|Geometry|reconcile_panel_layout` -matches an **import line and nothing else**, and -`full_grid_resync_acceptance` (191 lines) has no panel, side-window, -dedication, display or directory surface at all. Merged on that reading, -with the equal-rate model itself in doubt — see the U4 row, and note a -sixth base attempt reddened on a *third, unrelated* macOS selector -(U8), which is what a background platform failure rate looks like. - -**The original blocker:** the panel profile skipped checks 2–4 on the -claim that a panel result never touches a document window. **Panel -placement falls back to an ordinary document window** when the frontend -is not panel-capable or its side slot is dedicated, so a `"panel"` -commit could replace a **newer** document with every stale-intent guard -skipped. Reproduced in review. - -**Four designs, two rejected outright and one corrected — the sequence -is the part worth not re-learning:** - -1. **Revision 6 — predict at preflight.** Rejected: the `await` refusal - stops concurrent interleaving, not the body, which is arbitrary - synchronous Lua and can create the fallback itself. -2. **Revision 7 — enforce at the placement boundary.** Implemented at - `ca72461`, then rejected: `docs/agent-handoff.md:748` requires - `commit_to` to preflight **before** the callback, because - "validating at display time is four mutations too late". A body has - already created buffers, handles and paint by then, so a - placement-time refusal is a partial commit with an error return. -3. **Revision 8 — keep the preflight, REFUSE the scope-invalidating - mutation.** The shape the tree implements. Same as `Handle:await` - being refused inside a commit scope: the fallback never comes into - existence, and refusal stays mutation-free on `(false, reason)`. -4. **Revision 9 — make the refusal hold for the WHOLE body.** Not a new - shape; a correction to revision 8's scope. A nested `commit_to` - **replaced** the enclosing contract and restored it afterwards, so - an outer `"panel"` commit's restriction went out of force for the - inner body's extent: nested `"document"` commit → callback dedicates - the side slot, unrefused → outer commit resumes, falls back, - overwrites a newer document. Reproduced in review. Contracts now - **compose** — the core holds a stack, `commit_to` pushes and pops - rather than swapping, and the guard consults every contract in force, - so the strictest active restriction wins. Nesting itself is **not** - forbidden: only the mutation is refused, so a nested commit that - touches no dedication runs exactly as before. Detecting the - dedication when the outer commit resumed was not available — that is - a late refusal, which is what revision 7 was rejected for. - -**WHAT REVISION 9 DID *NOT* INVALIDATE — read this before re-opening the -enumeration.** The write-site enumeration below survived intact: every -site is real, every one is still guarded, and review of the nesting -defect found no missing route. What was wrong was the *surrounding* -claim — that the guard was in force for the whole outer body. A complete -list of write sites is not a complete argument until the guard's extent -is stated too. The acceptance suite now drives the same rows at **two -depths**, directly and through a nested `commit_to`. - -**THE ENUMERATION IS THE LOAD-BEARING PART, AND IT IS CLOSED AS AN -ENUMERATION OF WRITE SITES — for a structural reason, not because -inspection ran out of ideas.** Full working in the framing §3; the short -form: - -- **Only two pieces of state can matter**, because `resolve_placement` - reaches `Ordinary` from a side request through exactly two branches: - `panel_capable`, and the one side window's `dedicated`. -- **`panel_capable` is unreachable from a body.** It is written only - where a `FrontendView` is constructed, and nothing in - `src/lua_bindings/` constructs, registers or unregisters one — - `register_frontend_view` has callers only in `daemon.rs` and core - unit tests. -- **Eight writes to `dedicated` exist** (`rg 'params\.dedicated\s*=' - src/`); **four are reachable and a fifth is guarded defensively** — - `apply_placement`'s `Side` created / replacing / non-replacing arms - and `set_params` are the reachable four, and `quit_window`'s - `QuitAction::Restore` is the fifth, proved unreachable below and - guarded anyway. **All five are guarded**, which is the count that - matters; listing four under the word "five" is what an earlier version - of this bullet did. Two `Ordinary` arms are harmless (their target is - never a side window; one only ever clears the flag) and one is a unit - test. -- **The guards are sited where the property converges, not per caller.** - All three `Side` arms are reached through `apply_placement`, which has - **exactly one caller** — so one guard in `display_buffer` covers every - request-driven dedication, including spellings that do not exist yet. - `set_params` is a genuinely separate write and is guarded separately; - dedication does **not** converge before the field itself, and that is - stated rather than papered over. -- **Closing the side window is NOT a route**, checked rather than - assumed: with no side leaf `side_window_for` returns `None` and - placement **creates** a fresh panel instead of falling back. Hiding is - likewise irrelevant — `panel_hidden` is not consulted by placement. -- **`quit_window`'s `QuitAction::Restore { dedicated: true }` is - UNREACHABLE**, and this was the surprise. `Restore` is stored only on - a *replacing* side placement, and a dedicated slot can never be the - target of one. Guarded anyway, labelled defensive, because its - unreachability is emergent from two rules in another function. -- **What this does not rule out:** the enumeration is closed over the - current tree, not future edits. `params.dedicated` is a public field, - so nothing but the acceptance rows would catch a new direct writer. - -**Also closed:** an invalid-UTF-8 profile (`string.char(255)`) reached -`to_str()` and surfaced mlua's generic conversion error instead of the -documented message naming the accepted values — the same reachability -class as revision 5's `Option` defect, one layer down. The -comparison is on bytes now. - -**Written with the lane's first commit**, per the standing correction -from #171 and #215. - -**Branch `destination-capture`**, base `githubsucks/main` @ `4bc55e8` -(the #225 merge). **`githubsucks/destination-capture` is the -authoritative tip** — the ref, not a SHA. Recover with -`git fetch githubsucks && git checkout destination-capture`. - -- **Framing `docs/destination-capture-framing.md`, revision 9.** - Revisions 1–5 were approved over four review rounds; revisions 6–9 are - corrections carrying the blocker above, and **revision 8's design as - scoped by revision 9 is what the tree implements**. Revisions 6 and 7 - are described in that document as the record of why *not* those; - neither is in the tree and neither should be restored from it. -- **Implemented in four commits.** `779bb02` is the mechanism - (`pmacs.window.capture_destination()`, the `ViewDestination` rename, - the profile argument); `d5a6170` is - `tests/destination_capture_acceptance.rs`; `469d5c8` is the - revision-8 panel-profile correction plus the invalid-UTF-8 hole; - `394fa43` is revision 9's contract stack and the commit below adds its - cross-frontend pin. **15 pins**, and both preservation suites pass - **unchanged** (journey 47, dired 31) — §7's stop signal not firing - rather than being suppressed. -- **HOW THE PANEL PROFILE IS ENFORCED, in one sentence so no earlier - revision gets reinstated by someone reading only that document:** the - preflight stays exactly where it was, and the mutations that would - invalidate it are **refused at the attempt**. - - `EditorCore::panel_commit_dedication_refusal` is the one rule. It - fires while **any** `"panel"` `CommitContract` for this frontend is - in force — every contract on the stack, not the innermost — and is - consulted from `display_buffer` (before `apply_placement`, so a - refused attempt mutates nothing), `pmacs.window.set_params` (before - its borrow, so `fixed_rows` in the same table is not applied - either), and `quit_window`. - - **This is the same shape as `Handle:await` being refused inside a - commit scope**, and for the identical reason: something that would - invalidate the scope's guarantee is rejected outright rather than - predicted around or caught late. - - The contract (`CommitContract { destination, profile }`) rides on - the core in a **stack**, pushed and popped by the **same** - `ScopedFrontendGuard` that scopes the frontend, so a `"panel"` - profile can never outlive the body that declared it. The field is - private to the crate — Lua cannot claim a profile for a placement it - did not commit to. - - **A stack, not a slot, and the distinction is revision 9 (above).** - The frontend override and the ambient frontend are *substitutions*, - so a nested scope rightly replaces them; a contract is a - *restriction*, and replacing one suspends it. The guard stores a - depth and truncates back to it, so an inner exit removes exactly the - contract it added and leaves every enclosing one in force. - - **Matching is per FRONTEND as well as per profile, and that is a - deliberate exception with its own positive pin.** A nested commit for - a different frontend may dedicate *its* side slot: `resolve_placement` - consults only the requesting frontend's `panel_capable` and its own - one side window, so nothing done to B can change where A's side - request lands. Pinned by - `a_nested_commit_for_another_frontend_may_dedicate_its_own_slot`, - which is the file's only row asserting that something is **allowed** - — every other asserts a refusal, and an exception only the doc - comment knows about is one review round from being simplified out. - - **Prohibiting nested `commit_to` was the other candidate and was - rejected.** It closes the hole by forbidding a construction no rule - objects to — `commit_to` is public Lua API for saying where a - continuation's result belongs, and a body committing to a second - destination (a diff beside a status panel) is where #227's adoption - is heading. Only the restriction needed preserving. **No Lua in the - tree nests today** — `builtin/runtime/dired.lua` is the only - `commit_to` consumer and it does not — so this is a decision about - the API's future rather than about a live consumer, which is why it - is recorded rather than left implicit. - - **`panel_placement_can_fall_back` remains the preflight**, unchanged - in role: it measures whether this frontend places side requests in - the panel *right now*. With the invalidating mutations refused, that - measurement stays true for the life of the body, which is what makes - it a guarantee rather than a forecast. - - The four document checks live once, in - `EditorCore::document_destination_refusal`. - - **Three deliberate limits**, each a different decision rather than a - stricter version of this one: the **document profile is untouched** - (constraining its body would newly refuse dired's own documented - panel path — a preservation-suite stop signal); **dedicating a - document window is still allowed** (it cannot change which of - panel-or-document a side request resolves to); and **falling back is - still allowed** — a frontend that cannot render a panel degrades - gracefully exactly as today, because this refuses the mutation that - *manufactures* a fallback, never the fallback itself. -- **Mutation-checked per guard, and the pattern is the evidence the rows - are independent rather than one assertion repeated.** Deleting the - `display_buffer` guard fails the three `display{side, dedicated}` rows - — verified **individually**, by rotating each to the front of the - table, since the first failure otherwise masks the rest. Deleting the - `set_params` guard fails only that row and leaves the display rows - passing. Both leave every other test in the file green. -- **Audit: nothing else relied on "a panel never touches a document".** - Four doc sites repeated the claim (`ViewDestination`'s own doc twice, - `capture_view_destination`, `ViewDestinationLua`) and were corrected; - no other code depended on it. Dired — the only Lua `commit_to` - consumer — takes the **two-argument document profile**, so all four - checks already applied to it, and it separately documents and accepts - the side-slot fallback (`builtin/runtime/dired.lua`). - `compile.lua`'s `already_in_panel` queries live state rather than - assuming, and the terminal adopter's rollback keys off - `DisplayOutcome::created_side`, already false on a fallback. -- **TWO FRAMING CLAIMS THE TREE DID NOT MATCH.** Neither changed a - decision; both are recorded because the framing says "counted, not - estimated" and a reader will check. - 1. **The rename was 11 references across 5 files, not 8 across 4.** - `src/daemon.rs:1804` also calls the capture (the attaching - frontend's directory open), and `editor.rs` holds six references - rather than the counted total. Mechanical either way. - 2. **Q#DC-4's "a frontend with no document window" is a DEFENSIVE - branch, not a routine one.** The obvious spelling — a frontend - showing only a bottom panel — is asserted impossible: Q#BP6 says a - layout always retains at least one non-side window, and - `EditorCore::non_side_target` carries a `debug_assert!` that fires - under `cargo test` when one does. So with Q#BP6 held a *registered* - frontend always has a live document window. The decision still - stands (capture stays total; an adopter with nowhere to land gets a - refusal naming that rather than permission to fall back to ambient - state), and the two Q#DC-4 pins drive the reachable spelling of the - same condition — a layout whose document window has gone while the - view remains. **#227 should not expect to hit this refusal**; it is - insurance, not a path. -- **Mutation-tested, since a matrix of deliberate omissions is exactly - what passes vacuously.** Retyping the profile to `Option` - fails the table and boolean rows with mlua's conversion error (the - number row survives — Lua coerces it — which is why the closed set is - witnessed by more than one non-string). Applying all four checks in - both profiles fails the panel column; applying only check 1 in both - fails the document column. Defaulting an omitted profile to `"panel"` - fails **`journey_acceptance`'s two preservation pins**, which is the - contract claim being executable rather than asserted. Dropping the - frontend scope for the panel profile fails the survives-a-switch pin's - panel row; dropping the no-document-window arm fails the Q#DC-4 pair. - - **Revision 8's four, each isolating a different way to get it wrong** — - and the pattern of *which* rows survive each is the evidence the parts - are independent rather than redundant: - 1. delete the `panel_commit_dedication_refusal` call from - `display_buffer` → the three `display{side, dedicated}` rows fail, - **verified individually** by rotating each to the front of the - table so the first failure cannot mask the rest. Every other test - passes — which is exactly the hole an implementation guarding only - `set_params` would ship. - 2. delete it from `set_params` → **only** that row fails; the three - display rows still pass. - 3. delete the `panel_placement_can_fall_back` arm from - `commit_destination_refusal` → **only** the two pre-established - fallback rows fail, which is the preflight half. - 4. make `panel_placement_can_fall_back` unconditionally `true` (the - "widen the predicate" non-fix) → the really-lands-in-the-panel pin, - the Q#DC-4 panel pin and the matrix's three panel rows all fail. - That is the two profiles collapsing into one, made visible — the - named fallback design, showing up as a test diff rather than - silently. - - And reverting the byte comparison to `to_str()?` fails the - `invalid utf-8` row with mlua's conversion error, on content. - - **Revision 9's two, each isolating a different half of the rule:** - 1. restore `panel_commit_dedication_refusal` to reading only the - innermost contract (`.last()`, which is exactly revision 8's - swapped slot) → **only** - `a_nested_commit_cannot_mask_an_outer_panel_restriction` fails. - Note the ordinary-nesting pin deliberately survives this — it - exists to fail the *other* candidate fix (prohibit nesting), so the - two are a pair rather than one test written twice. - 2. delete `&& contract.destination.frontend == fid` from the same - scan, making any outer `"panel"` contract **globally** restrictive - → **only** - `a_nested_commit_for_another_frontend_may_dedicate_its_own_slot` - fails. Both single-frontend nesting tests pass under it, which is - the evidence they are independent of the frontend match rather than - merely looking so; the cross-frontend exception had no pin at all - before this row, since every other test in the file drives one - frontend. - - Both were run across all three acceptance suites and the lib: in each - case `journey_acceptance` (47), `dired_acceptance` (31) and - `cargo test --lib` (1920) stay green, along with every other pin in - this file. - - **The counts above are journey 47 / dired 31**, matching the bullet - further up. The mutation paragraph committed at `394fa43` had them - **reversed** in both the ledger and that commit's message; the ledger - is corrected here and the message is left as written, since rewriting - a pushed commit is worse than a footnote. A reader following that SHA - should take these numbers, not those. -- **The public API #227 adopts against (Q#DC-5), pinned so it is a - contract rather than an intention:** - `pmacs.window.commit_to(dest, body [, profile])`. Profile is an - optional trailing argument typed **`mlua::Value`, not - `Option`** — with `Option` mlua rejects a number or - table during argument *conversion*, before the closure runs, making - the promised "accepted values are…" message unreachable. That is the - same trap the existing binding documents for `dest`. Validated in the - body against a **closed** set — `"document"` and - `"panel"`. **Omitted means `"document"`**, so every existing - two-argument caller keeps all four preflight checks *by definition of - the signature*, which is what makes `journey_acceptance` passing - untouched a consequence rather than a hope. An unrecognized or - non-string profile **errors**, naming the accepted values — a silent - fallback would hand a caller different checks than it asked for, - which is the exact failure the parameterization exists to prevent. - Git's mapping is settled here too: `*git-status*` → panel, - `*git-diff*` → document. Revision 2 took three findings: Q#DC-2's parameterization was - incomplete (a panel depends on **none** of checks 2–4, not just check - 3, so the question now carries a full preflight matrix with every - omission testable); `tests/journey_acceptance.rs` joins dired as a - **preservation suite and stop signal**, since it holds the - `commit_to` scope, forged-userdata, preflight and restoration pins - this lane generalizes; and the **coherence-impact section was missing - entirely**, which `CLAUDE.md` and `COHERENCE.md` §25 both require. -- **A PREREQUISITE LANE. PR #227 (git Stage 1) blocks on it.** #227's - P1a review finding is why it exists: git's async completions mutate - and display UI without capturing the initiating frontend - (`builtin/runtime/git.lua:609`, `:854`), so a result surfaces in - whichever frontend is active when git exits. -- **The mechanism existed but was not Lua-reachable** until `779bb02`. - `pmacs.window.commit_to` took a `DirectoryDestinationLua`, which is - **nonconstructible from Lua** by design - (`src/lua_bindings/mod.rs:4256`) and minted only inside the - `path.open-directory` listener dispatch (`src/editor.rs:1311`) from a - `pub(crate)` capture (`:1241`). So no async Lua continuation outside - a directory open could say where its result belongs. Line numbers are - the pre-lane ones, kept because they are what the finding was written - against. -- **Scope:** a Lua-reachable capture, a generic rename - (`DirectoryDestination` → `ViewDestination`; the framing counted 8 - references across 4 files, the tree held **11 across 5** — see the - finding above), and the preflight question below. - **No adopter**: git's adoption is #227's work after this lands, since - a prerequisite that converts its own first consumer cannot be - reviewed separately from it. -- **The substantive question (Q#DC-2)** is that git's two continuations - differ in kind. `*git-status*` goes to the **bottom panel** - (`listview.open` defaults `display` to `"panel"`, - `builtin/runtime/listview.lua:550`); `*git-diff*` replaces a - **document** window. `commit_to`'s stale-intent check (Q#JR14c) is - right for the second and, *when the placement really is a panel*, - irrelevant to the first. One shape over-refuses the panel or - under-checks the document. - - **DO NOT READ THE OLDER FORM OF THIS BULLET, WHICH SAID "the panel - never touches the captured window's buffer".** That is the claim - revisions 6–8 invalidate: panel placement **falls back** to an - ordinary document window when the frontend is not panel-capable or - its side slot is dedicated. The relaxation is conditional, and the - mutations that could make it fall back are refused inside a - panel-profile commit (revision 8) rather than predicted at preflight - (revision 6) or caught at placement (revision 7, which would refuse - after the callback had already mutated). -- **Stop signal recorded in the framing:** if any existing dired test - needs editing, the generalization changed Journey Stage 1a's - semantics, and that is cause to stop rather than to adjust the test. -- **Gates, as the executable line rather than a description:** - - ``` - scripts/gate --acceptance destination_capture_acceptance \ - --acceptance journey_acceptance \ - --acceptance dired_acceptance - ``` - - `--acceptance` is repeatable, so there is no reason for this ledger - to say "plus dired's" and leave the reader to reconstruct it. - **`journey_acceptance` and `dired_acceptance` are preservation suites - and a STOP SIGNAL**: they carry the `commit_to` scope, - forged-userdata, preflight and restoration pins this lane - generalizes, and if either needs editing, the change altered Journey - Stage 1a's semantics rather than closing a gap in them. No - `--protocol` — core and Lua bindings only. - ## Worker identity Stage 1 (§9) — MERGED as #232 (`3cc1b85`) **Written with the lane's first commit**, per the standing correction @@ -1062,258 +612,6 @@ the authoritative tip** — the ref, not a SHA. Recover with each grew a required `purpose` field, and `pmacs.process.spawn` requires `purpose` in its spec table. -## Discovery Stage 2 — PR #228 OPEN, **MERGE-BLOCKED** - -**PR #228** — https://github.com/levineuwirth/pmacs/pull/228. Opened -2026-08-09 at `2d298dd`. **Open for review, not for merge.** - -**The block is a gate-integrity problem, not backlog hygiene.** This -lane's gate is `scripts/gate --protocol`, which promises the CRDT -workspace sweep. That sweep's documented precondition is -`cargo build --workspace --no-default-features --features luajit,crdt` -(handoff §5), and **the script does not run it** — confirmed by reading -its plan emitter. On a fresh per-worktree target directory the sweep -fails on twelve `gpu_invocation_acceptance` tests missing the -`pmacs-gpu` binary, so a `--protocol` result can be decided by the -state of the build directory rather than by the diff. - -Latent until #225 gave each worktree its own target dir — a shared one -usually already had `pmacs-gpu` built, satisfying the precondition by -accident. It surfaced on this branch's first gate run. - -**Unblocking requires both:** the `scripts/gate` repair, in its own -narrow framing and its own PR (explicitly **not** folded into this -feature branch), and then a **fresh-target rerun of this branch's -protocol gate** under the repaired script. - -**Written with the lane's first commit**, per the standing correction -from #171 and #215. - -**Branch `discovery-stage2`**, base `githubsucks/main` @ `4bc55e8` -(the #225 merge). **`githubsucks/discovery-stage2` is the authoritative -tip** — the ref, not a SHA. Recover with -`git fetch githubsucks && git checkout discovery-stage2`. - -- **Framing `docs/discovery-stage2-framing.md`, revision 3, APPROVED - 2026-08-09** after three review rounds. Each round found the previous - one reasoning about a mechanism instead of reading it — an in-place - field change that postcard cannot make compatible, a TUI that never - reads the message at all, a round-trip test that freezes nothing, a - cache hazard the per-peer render state makes impossible, and a - clipping rule unachievable at narrow widths. - Scope: `COHERENCE.md` §5's "M-x rows are still bare names". - Descriptions already exist on `Command` and are already rendered by - `help.list-commands`; they are missing at the one moment they would - change a decision. -- **PROTOCOL BUMP v22 → v23, and this lane HOLDS THE BUMP SLOT.** - Additive: a new `MinibufferPromptRows` variant **appended** to the - enum, with `MinibufferPrompt` **frozen** for v12–v22. An in-place - field change is a wire break — postcard encodes positionally, and - that variant is sent to every peer `>= 12` (`src/daemon.rs:1472`). -- **Git Stage 2 (gutter markers) also needs a bump and must wait for - this to land.** Git Stage 1 is no-wire and runs beside it. -- **Two halves, only one of which is wire work.** `pmacs-gpu` renders - the new variant. **The grid TUI never reads `MinibufferPrompt` at - all** — it paints from `core.minibuffer` and renders - `format!(" [{cand}]")` (`src/editor.rs:5484`), so its half is a - local formatting change reading the registry directly. A multi-row - TUI chooser is explicitly NOT this lane. -- **Gates:** `scripts/gate --protocol --acceptance - discovery_stage2_acceptance --acceptance m9_6_acceptance --acceptance - m9_7_acceptance --acceptance m9_8_acceptance` — the strengthened - two-configuration sweep, which is what `--protocol` exists for. The - three m9 suites are named because the PR #228 review round measured - them as this change's blast radius (see the description-clip bullet); - their continued passing is on the record rather than assumed. - **`--protocol` does NOT run its own documented precondition** - (`cargo build --workspace --no-default-features --features - luajit,crdt`, handoff §5) — run it by hand first or twelve - `gpu_invocation_acceptance` tests fail on a missing `pmacs-gpu` - binary. That omission is the `gate-protocol-build` lane's, not this - one's. -- **IMPLEMENTED.** `PROTOCOL_VERSION` is 23, - `ADVERTISED_PROTOCOL_VERSION` is untouched at 20. New suite - `tests/discovery_stage2_acceptance.rs`; the daemon half is - `crdt`-gated (a semantic session is necessarily a text replica) and - runs one daemon serving a v22 and a v23 session simultaneously. -- **Multi-line descriptions are clipped AT THE SURFACE, and - registration-level rejection was investigated and REJECTED ON - EVIDENCE — do not re-propose it.** PR #228 review found the real - hazard: the GPU dropdown derives its height, visible window and - highlight offset from `rows.len()` (one logical row per candidate), - so a detail carrying a line break misaligns every row below it; the - TUI writes into a single-row band. The obvious fix — reject CR/LF in - `CommandRegistry::define` — was implemented and measured, and it - **fails 36 tests across `m9_6`/`m9_7`/`m9_8`**, because MCP tool - registration renders a whole schema block into `description` - (`tests/fixtures/pmacs-mcp-tools/init.lua:272`, - `table.concat(lines, "\n")`, used at `:496`) and - **`tests/m9_6_acceptance.rs:583-598` asserts four separate lines of - it** — tool text, `Arguments:`, and two per-argument lines. No - single-line rendering satisfies those assertions, so a registry guard - could only go green by deleting a shipped acceptance criterion. - The one-line constraint belongs to the surfaces that have it: - `Command::description_first_line` clips, both single-row consumers - call it, and the full text still reaches `describe-command` / - `help.list-commands` untouched. Precedent already in-tree — the same - MCP fixture clips a tool RESULT to its first line because *"a - multi-line set_status would corrupt the row layout"* (`:277-285`). - **A startup census is not a corpus census**: booting an - `EditorState` and scanning all 180 registered descriptions found zero - offenders, because MCP registers at RUNTIME and builds the string by - concatenation — invisible to both that census and a grep for literals. - The workspace sweep is what caught it. -- **The freeze is enforced by LITERAL byte fixtures**, not a round-trip - — `minibuffer_prompt_v12_wire_bytes_are_frozen` in `src/protocol.rs`, - the first such fixture in this repo. Bite-verified: reordering two - fields of `MinibufferPrompt` leaves - `minibuffer_prompt_round_trips_through_postcard` **passing** and fails - the fixture, which is exactly the hazard a round-trip cannot see. -- **Version assertions updated (five, each read before editing):** - `src/protocol.rs` — the `PROTOCOL_VERSION == 22` tripwire (renamed - `protocol_version_is_twenty_three_for_minibuffer_prompt_rows`) and - `supported_protocol_versions_resume_ladder_on_v6_floor`'s - accepted/rejected ranges; `tests/statusline_segments_acceptance.rs` - (version + supported range + the `!supported` ceiling); - `tests/bottom_panel_stage2b_gpu_acceptance.rs`; - `tests/vterm_stage3_acceptance.rs`. **No `ADVERTISED_PROTOCOL_VERSION` - assertion fired**, which is the pin doing its job. -- **No cross-version cache test, deliberately** (framing §3.2/§6): - `SemanticRenderState::for_peer` bakes the negotiated version in at - attach and is dropped at detach, so a cache cannot span two versions. - A test for an impossible condition passes forever while teaching the - next reader that the hazard is real. - -## LSP LaTeX coverage — IMPLEMENTED, gates green, no PR yet - -**Written with the lane's first commit**, per the standing correction -from #171 and #215. - -**Branch `lsp-latex-coverage`**, base `githubsucks/main` @ `4bc55e8` -(the #225 merge). **`githubsucks/lsp-latex-coverage` is the -authoritative tip** — the ref, not a SHA. Recover with -`git fetch githubsucks && git checkout lsp-latex-coverage`. - -- **Framing `docs/lsp-language-coverage-framing.md`, revision 3 — - IMPLEMENTATION AUTHORIZED 2026-08-09**, after a summary of its four - corrections rather than a findings round on the document itself. - Recorded that way deliberately: the §3 `.texlabroot` verification - caveat was live and binding, and was step zero of the work rather - than a footnote it could be read past. **It is now discharged — see - below.** **Revision 1 was UNTRACKED on `main` in one checkout** and - therefore did not travel; committing it here is the fix. -- **Scope: one `pmacs.lsp.config.latex` entry plus its root resolver.** - `texlab` 5.25.1 is installed and unused; a `.tex` buffer highlights - correctly and offers no completion, diagnostics, or go-to-definition. -- **Revision 2 found Slice 1 is SMALLER than revision 1 framed.** The - proposed `.tex`/`.latex`/`.sty`/`.cls` filetype mappings are - redundant: the grammar already carries exactly those extensions - (`src/syntax.rs:1111`), grammar-extension detection sits **ahead** of - the LSP filetype map in the precedence chain - (`docs/latex-grammar-math-substrate-framing.md:166-171`), and - `lsp.lua:267-270` calls that map "mainly the LSP-only fallback". The - two systems cannot disagree, because the grammar's extension list is - what drives detection. -- **Two other corrections.** `haskell-language-server` **is** installed - on this machine — revision 1 said it was not, which was the whole - basis of its Slice 1 / Slice 2 split. And Q#LX3's deferral argument - read `COHERENCE.md:1669` ("first slice in flight") when `:124` and - `:867` both record multi-root affinity as **merged (#161)**; that - line contradicts the same document twice and wants a separate fix. -- **Q#LX2 (the LaTeX root) is answered.** An upward marker walk through - `config.latex.root`, which already accepts a resolver function - (`lsp.lua:543`), falling back to the file's own directory. - **`.git` is deliberately excluded**: a repo root is the wrong answer - for LaTeX, and it is the one place copying the other fourteen - entries' instinct is actively wrong. -- **STEP ZERO IS DISCHARGED — §3's `.texlabroot` caveat, by - observation.** Marker 1 **ships**, and the framing's premise for it - was corrected in the process. - - **`.texlabroot` is a real texlab marker.** texlab v5.25.1's - `crates/distro/src/language.rs` maps `.texlabroot`/`texlabroot` → - Root, `Tectonic.toml` → Tectonic, `.latexmkrc`/`latexmkrc` → - Latexmkrc; `ProjectRoot::walk_and_find` - (`crates/base-db/src/deps/root.rs`) walks ancestors testing all - three, innermost wins. The shipped marker set is **texlab's own**, - including the bare `texlabroot`/`latexmkrc` spellings the framing - did not list. - - **But texlab cannot apply that walk to fix a root pmacs gets - wrong**, which is the correction that matters. Each arm searches - `workspace.iter()` — documents ALREADY LOADED — and the workspace - comes from the folders the CLIENT supplies. Hand-driven LSP - sessions confirmed it: with `rootUri` at a `chapters/` - subdirectory, no marker above it (`.texlabroot` included) widened - texlab's view and its dependency graph never reached the parent - document; with `rootUri` at the marker directory the parent - resolved, marker present or not. **texlab honours the root it is - handed and never corrects a too-narrow one**, so what - `config.latex.root` returns *is* the project scope. That makes the - resolver the whole value of the lane rather than a nicety. - - **`args = {}` is also observed**, not assumed: bare `texlab` - answers `initialize` with `TexLab 5.25.1` over stdio, so the `run` - subcommand is not needed. - - **§3 said the wrong thing and has been corrected — `b5eaf27` IS - revision 3.** It framed marker 1 as conditional on texlab honouring - the `.texlabroot` *file*, when the operative fact is that texlab - honours the *client-supplied root* and never widens it. The caveat - was discharged by observation, and revision 3 records what that - established. Nothing about §3 is outstanding. -- **`.git` exclusion needed more than omitting it from the list.** - `project_root_for` falls through to `pmacs.project.detect` when a - resolver returns nil, and **that** walk includes `.git` — so a - resolver declining on a markerless file would hand texlab the - repository root by the back door. The resolver therefore never - declines for a file that has a directory. Pinned end to end through - attach, with the same fixture asserting the shared detector really - would have answered the repo root. -- **Commit `a9ef37f`** — `builtin/runtime/lsp.lua` plus - `tests/lsp_latex_acceptance.rs` (14 tests, one per §6 bullet plus the - boundary and decline cases). No `settings`/`init_options` (Q#LX1); no - filetype mappings (§2, asserted both ways). -- **Gates: ALL GREEN** via - `./scripts/gate --acceptance lsp_latex_acceptance` — fmt, clippy, - lib, lib-crdt, the new suite, m4, gpu, the workspace sweep (115 - suites, zero failures), diff-check. No `--protocol` — a config entry, - no wire. -- **Seven mutations each fail the suite**: resolver declining on no - marker (6 tests), no marker walk (4), a redundant `filetypes.tex` - (1), boundary ignored (1), `io.open` truthiness so a directory counts - as a marker (1), marker set narrowed (4), command renamed with - opinionated settings added (1). -- **The boundary has now been the interesting part twice, and the - second time it was a real defect (fixed in review).** First it was - hermeticity — every fixture sets `set_search_boundary` at its own - tempdir because R8's shape (a stray `latexmkrc` above the tempdir) - would make the markerless assertions pass while testing nothing. - Then review found `latex_within_boundary` answering a PATH question - with string arithmetic: `dir:sub(1, #boundary + 1) == boundary .. "/"` - compares against `"//"` when the boundary is `/`, which no canonical - path matches, so a root boundary judged **every** ancestor out of - bounds, ran no marker walk at all, and gave each chapter of a thesis - its own server — the lane's headline behaviour silently off, with - every shipped test still green because each one clamps to a tempdir. - The same trap sat at the other end (`/` was never a walk candidate, - and `/paper.tex` sliced to an empty directory and declined into the - `.git`-aware detector). Now segment comparison throughout: the root - is a boundary with zero segments, contained by construction rather - than by a special case. Pinned by an ATTACH-level test under a `/` - boundary — two chapters, one server, marker root — and the - hermeticity property asserts **both** directions, since "stops at the - boundary" is also satisfied by a walk that never runs. Suite is 16 - tests. **A reader - deciding whether to trust this resolver should read it as: the marker - set and the `.git` exclusion were settled by observation and are - solid; the boundary arithmetic around them was not, and is the place - to look first if roots come back wrong.** -- **Trap for the next agent in this worktree:** this machine exports a - shared `CARGO_TARGET_DIR`, so a bare `cargo test` compiles against a - sibling worktree's artifacts and fails with errors from code that is - not in this tree. Use `scripts/gate`, or - `CARGO_TARGET_DIR="$(./scripts/gate --print-target-dir)"` for ad-hoc - runs. `scripts/gate`'s own header documents this; the failure looks - like a broken branch, which is why it is recorded here. -- **No PR opened**, by instruction. - ## `scripts/gate --protocol` build step — **MERGED as #229** (`7cf4653`) **MERGED as PR #229** — https://github.com/levineuwirth/pmacs/pull/229, @@ -1523,41 +821,6 @@ authoritative tip** — the ref, not a SHA. Recover with sweep step each fail the suite. -## QoL arc retirement — PR #224 OPEN (docs only) - -**PR #224** — https://github.com/levineuwirth/pmacs/pull/224. Written -**after** the PR existed rather than with the lane's first commit — -which is the standing correction from #171 and #215 being missed again, -and it took review asking. Recorded that way rather than quietly -back-dated: this file requires a lane for **every open PR**, including -the PR that retires other lanes. - -- **Branch `retire-long-lines-lane`**, base `githubsucks/main` @ - `9a26ac8` (the #223 merge). **`githubsucks/retire-long-lines-lane` is - the authoritative tip** — the ref, not a SHA, since any edit to this - block advances past whatever SHA it records. Recover with - `git fetch githubsucks && git checkout retire-long-lines-lane`. -- **Docs only.** Two files, `docs/active-work.md` and - `docs/agent-handoff.md`. No `src/`, no crate, no manifest, no test - changes. -- **Scope:** Rule 4 for the QoL arc, closed at #223. Remove the three - merged lane blocks (long lines, Stage 1 #219, Stage 2 #220) **after** - re-homing their durable residue to the handoff; advance the handoff's - date and `main` anchor and this file's canonical-base record and - recovery floor; add capability-aware keymap resolution as a named - handoff §6 backlog item — cross-cutting, **not started**, needs its - own framing. -- **Verification:** `git diff --check` clean. **The recovery path was - re-exercised, not SHA-swapped** — fresh clone into an empty - directory, `githubsucks` alias, `--prune` fetch, `9a26ac8` confirmed - an ancestor of `githubsucks/main`, worktree recovered with the - three-argument form; all four steps clean. Swept for dangling - references to the removed lanes and their branches. The full gate - suite is **not** re-run for a change that cannot reach it, and that - is stated rather than left as a gap. -- **Retire this block in the next absorption after #224 merges.** It - describes a docs PR; once merged there is nothing volatile left. - ## Docs absorption after #217 — MERGED as #218 (2026-08-06 09:59Z) **PR #218** — https://github.com/levineuwirth/pmacs/pull/218. **This @@ -1907,40 +1170,45 @@ wants, not a continuation of a plan.** optional external tools; `/bin/sh`, `stty`, git and tar are documented and never checked. That is §18 onboarding work. -## Discovery lane (P4) — STAGE 1 MERGED (#207); STAGE 2 IS NEXT +## Discovery lane (P4) — STAGES 1 AND 2 MERGED (#207, #228); LATER WORK REMAINS -**Rewritten, not removed.** Rule 4 removes a lane when its ARC is done; -this arc is not — Stage 1 built the command surface and everything that -needs a Rust change is still ahead. Stage 1's durable facts are in -`docs/agent-handoff.md` §1. +**Rewritten, not removed, and the PR-specific Stage 2 block above it is +retired into this one.** Rule 4 removes a lane when its ARC is done; +this arc is not. **Deleting this lane on the strength of one merged +stage would have dropped four named pieces of open work** — predicate +evaluation, command metadata, help unification, and the prefix +decision — which is exactly why the coalesce is spelled out rather than +left to judgement. Durable facts from both stages are in +`docs/agent-handoff.md` (§1 for Stage 1; the v23 frozen-variant lesson +in §4 for Stage 2). -- **Landed:** eleven `help.*` commands over the existing registries, - indexed by `M-x help`, with `editor.describe-command` / +- **Landed, Stage 1:** eleven `help.*` commands over the existing + registries, indexed by `M-x help`, with `editor.describe-command` / `editor.describe-setting` kept as forwarders. No Rust, no protocol change. §5 moved substrate-without-surface → **Partial**. -- **Stage 2 candidates, in rough dependency order:** - 1. **Richer M-x rows** — a **protocol change**: - `MinibufferPrompt.candidates` is `Vec`, while - `CompletionPopupRow` already carries `kind`/`detail`, so the wire - pattern is solved and the bump is the work. - 2. **`Command` gains title / category / aliases / flags / +- **Landed, Stage 2 (#228, `0857bf4`):** M-x rows carry descriptions + over **protocol v23** — `MinibufferPromptRows` appended while + `MinibufferPrompt` stays frozen and still sent for `12..=22`, gated as + a range on both sides so exactly one variant reaches any peer. +- **Still open, and the reason this lane survives:** + 1. **`Command` gains title / category / aliases / flags / arg-schema** — a Rust type change across ~147 definition sites. MCP currently works around the missing schema by stuffing rendered JSON into the description string. - 3. **Predicate evaluation.** `Command.predicate` is read at + 2. **Predicate evaluation.** `Command.predicate` is read at `src/help.rs:76` and one test, and **evaluated nowhere**. Starting to evaluate it makes commands stop being invocable, so it needs its own decision about what "unavailable" means at each call site — M-x, dispatch, menu. `discovery_acceptance`'s `d9` pins today's behaviour with a *raising* predicate, so that stage must change the pin knowingly. - 4. **Help-layer unification.** `src/help.rs` is still orphaned. Stage 1 + 3. **Help-layer unification.** `src/help.rs` is still orphaned. Stage 1 funnels every command through `pmacs.editor._show_help` and renders via named per-subject functions, so the work is enumerated: replace the four subjects `src/help.rs` covers (key, mode, hook, buffer) and **write three new Rust renderers** for settings, lists and apropos. - 5. **The help prefix.** Deliberately untouched by Stage 1 — the + 4. **The help prefix.** Deliberately untouched by Stage 1 — the decision is one for the whole family, and `C-h` is **not** free (non-kitty terminals cannot disambiguate Ctrl+Backspace from Ctrl+H; both produce byte 0x08). `F1` / `C-c ?` / a rebind are the diff --git a/docs/agent-handoff.md b/docs/agent-handoff.md index 04747b8..3f46fe7 100644 --- a/docs/agent-handoff.md +++ b/docs/agent-handoff.md @@ -2747,9 +2747,30 @@ nothing; and the CLOSE message must use the same variant family as the OPEN, or a session closed by the other family's clear leaves its surface on screen forever. -**Fake LSP** (`src/bin/pmacs_fake_lsp.rs`) modes: `fullonly`, -`rangeonly`, `rangeonly16` (UTF-16 + fail-closed bounds validation), -`sighelp`. Use these for capability-matrix tests, not real servers. +**LaTeX is served by `texlab`, and its root is NOT the repository root** +(#230). `pmacs.lsp.config.latex` resolves the document root by an upward +marker walk — `.texlabroot`/`texlabroot` are texlab's own markers, +verified in its `crates/distro/src/language.rs` rather than assumed — +and **`.git` is deliberately excluded from that walk**: a repository +root is the wrong answer for a multi-file document, which is the whole +reason the resolver exists. `config.latex.root` already accepted a +resolver function, so this added no new mechanism. + +**Fake LSP** (`src/bin/pmacs_fake_lsp.rs`) modes — enumerated from the +binary, because this list had gone stale and a stale mode list is how a +test ends up covering the shape next to the defect. Capability matrix: +`fullonly`, `rangeonly`, `rangeonly16` (UTF-16 + fail-closed bounds +validation), `sighelp`, `prepare`, `preprefuse`, `rename`, +`inlaybounds`, `inlayrefresh`, `semantictokensrefresh`, +`applyeditplan`, `resourceops`, `posecho`, `defenv`, `wsconfig`, +`rooturi`, `leanprogress`. Failure shapes: `crash`, `error`, `garbage`, +`silent`. **File watchers (issue #233)**: `filewatch` (RelativePattern +`**/*.txt`), `filewatchabs` (plain-string ABSOLUTE glob), +`filewatchflat` (RelativePattern with no leading `**/`), +`filewatchbare` (bare relative string — the P1 regression guard), +`filewatchrereg` (same id twice, no unregister), `filewatchjoin`, +`filewatchretire`. Use these for capability-matrix tests, not real +servers. ## 5. Hard-won ops lessons