# Active work — cross-machine resume ledger **Snapshot: 2026-07-25.** This file records volatile work that has not landed on `main`. Read it after `docs/agent-handoff.md`. Remove completed entries when their PR merges; do not let this become a second permanent backlog. ## Repository authority - Canonical development URL: `https://github.com/levineuwirth/pmacs.git`. This ledger uses the normalized local alias `githubsucks` so its refs and recovery commands are identical on every machine. Remote names are otherwise machine-local: `origin` may name this canonical URL, a release mirror, or something else, and therefore has no authority by name alone. - Canonical base at this snapshot: `githubsucks/main` @ `0dd16a5` (GPU initial-target #148 atop folding Stage 2 landed-doc refresh #150, folding Stage 2 #149, the ledger refresh #147, web grammars HTML+CSS #146, and the LaTeX Stage 1 #144 / inline-math framing #145 pair; protocol v20). - On the transfer source, `origin/main` named a release mirror at `d3fa632` and lagged badly. On the current destination, `origin` names the canonical URL. This difference is why all recovery begins by verifying URLs and normalizing `githubsucks` rather than trusting `origin/main`. - The shared desktop checkout contained unrelated uncommitted work. The branches below were prepared in isolated worktrees; never clean or overwrite the shared checkout to recover them. Start on another machine by inspecting its remotes: ```sh git remote -v git remote get-url githubsucks ``` If the second command says the alias is absent, add it; if it prints a different URL, stop and resolve that collision rather than overwriting an unknown remote: ```sh git remote add githubsucks https://github.com/levineuwirth/pmacs.git ``` Then recover current refs: ```sh git fetch githubsucks --prune git log -1 --oneline githubsucks/main git worktree list git status --short --branch ``` The `git log` command must expose `0dd16a5` or a newer intentional main. If it does not, stop and repair the remote/fetch configuration. ## Lean 4 lane (Arc 8) — Stages 1+2 MERGED; 3a IN REVIEW (#167); 3b STACKED - Stage 1 **merged as #160** (`main` @ `0827dd1`, 2026-07-25, one review round, all twelve checks green). Branch `githubsucks/lean4-stage1` retained; it was worked in the shared checkout (no sibling worktree). - Approved framing: `docs/lean4-mode-framing.md` revision 4, committed as the branch's first commit (`a382965`) after three review rounds. **Seven stages**, 19 decisions (Q#LN1–19), 64 acceptance criteria. North star: match or exceed VS Code's Lean support. - **Stage 1 implemented; no wire change (protocol stays v20), no LSP, no frontend change.** Four commits: framing, grammar, theme captures, editing surface + acceptance. - `Cargo.toml` + `src/syntax.rs`: `arborium-lean` 2.18 and one `BUILTIN_LANGUAGES` entry named **`lean4`** (Q#LN2 — the name becomes the `didOpen` language_id), claiming `.lean` only. - `src/highlight.rs`: four capture entries — `constructor`, `character`, `keyword.conditional`, `warning`. - `builtin/runtime/{comment,pair,syntax}.lua`: `--` comments, the `⟨⟩ ⦃⦄ ⟮⟯` pair set, the `lean` → `lean4` modeline alias. - `tests/lean4_stage1_acceptance.rs` plus unit tests in `syntax.rs` / `highlight.rs`: 12 criteria, 17 tests. - **Q#LN1's open obligation is discharged.** `tree-sitter-lean4` is unusable (depends on `tree-sitter ^0.25` directly against our 0.26, exports no `LANGUAGE` const despite its README, packages no queries); `arborium-lean` rides `tree-sitter-language 0.1` with a pre-generated ABI-15 parser. `cargo tree -d` shows no duplicate core. The parse smoke pins the failure mode that matters: `→`/`∀`/`≥` must produce `(arrow)`/`(forall)`/`(comparison)`, since a mismatched-core build degrades silently on exactly those characters rather than failing loudly. - **Q#LN4 is a deliberate retro-paint of seven language entries**, not four: `tree_sitter_javascript::HIGHLIGHT_QUERY` is concatenated base-first into javascriptreact/typescript/typescriptreact. Its shape is "every capitalized identifier" (`#match? "^[A-Z]"`) plus every Lua table brace — not "constructors". Pinned in both directions per #146. - Implementation findings not in the framing: - `warning` had to move from bold red to bold **bright** red: `number` is plain `fg(1)`, so `sorry` and an adjacent numeric literal were the same colour. Found by writing the test. - `Some(1)` is **not** `@constructor` — in call position a narrower `@function` pattern wins. Only bare or pattern-position capitalized identifiers reach it. Pinned so the blast-radius claim stays honest. - Lean node kinds nest: `module > declaration > def|theorem`. - `pmacs.parse.injection_aliases` is a documented **write-only** Lua proxy (canonical map is Rust-side), so fence tests must drive `_parse_now` and inspect layer languages, never read the table back. - **Review round 1 addressed.** The finding: acc12's server-list assertion could not fail for the regression it named — the shared `editor()` helper wipes `pmacs.lsp.config` before any buffer opens, so `#pmacs.lsp.list() == 0` holds for every language regardless of what Stage 1 ships. It now asserts against a **pristine** `EditorState` that `pmacs.lsp.config.lean4` is nil, with a non-vacuity check that the same lookup finds `rust`; bite-verified by adding a `lean4` config to `lsp.lua` and watching it fail. Also fixed a stale column in a `highlight.rs` comment. - Verification on this branch: `cargo fmt --check` clean; strict workspace Clippy clean; 1,826 default + 2,003 CRDT library tests; lean4 Stage 1 9/9; comment toggle 14; auto-pair 45; injection 4; M4 121; required GPU 152; **isolated-config workspace sweep 3,150 across 90 suites**; `git diff --check` clean. The sweep needs an isolated `XDG_CONFIG_HOME` for the reason recorded in the bottom-panel lane below. ### Stage 2 — multi-root LSP server affinity (Q#LN15) - Portable branch: `githubsucks/lsp-multi-root-affinity`, shared checkout, based on `githubsucks/main` @ `0827dd1`. Named for the substrate, not for Lean: **the diff contains no Lean content**, because `ensure_server` is the one server-affinity function every LSP language shares and a cross-cutting change to it must not be reviewable only as a Lean feature. - Three files, no protocol change: `src/lua_bindings/mod.rs` (the `lsp.list()` row builder gains `root_uri` + `cwd`), `builtin/runtime/lsp.lua` (`project_root_for` returns `root, source`; `ensure_server` hoists it above the reuse loop and matches on it), `tests/lsp_multi_root_acceptance.rs` (9 tests, acceptance 13–21). - **The rule that keeps this from regressing every other language: the affinity key is the root only when a root was actually FOUND.** `project_root_for` never returns nil for a file with a path — its last resort is the file's own directory — so a naive `(language_id, root)` key gives every directory of loose scratch files its own server, for every language. `source` is `"config" | "detected" | "fallback"` and only the first two become a key. - **Wire-identical for the fallback case, and that is provable rather than hoped.** Matching is on the spawned spec's `root_uri` (nil matching nil), so the fallback spawn passes `root_uri = nil`; `cwd` still carries the directory and `build_initialize` derives the identical `rootUri` from `cwd` when the field is None, using a percent-encoder with the same allowed set as Lua's `file_uri_for`. `build_initialize` (`src/lsp.rs`) is the **only** reader of `spec.root_uri` in the tree. - Deliberate behavior change, asserted not discovered: a server hand-spawned from `init.lua` with only `cwd` set also reads back nil, so a root-bearing attach will not adopt it. - `config[language].root` may now be a `function(path) -> string|nil`, memoized per directory — needed because the hoist puts root resolution on every attach rather than every spawn. The memo is keyed **weakly by the resolver function itself**, so replacing `config[lang].root` cannot serve a root the previous resolver computed. This is Q#LN8's generalization landing early; the Lean resolver that uses it is Stage 3. - Bite-verified three ways: 5/9 fail against the pre-change `lsp.lua`, 8/9 against the pre-change `mod.rs`, and — the one that matters most — installing the naive always-key-on-root variant fails acceptance 20 and 21 exactly as Q#LN15 part 2 predicts. The four that survive the first bite (13, 15, 16, 19) are the regression pins; passing on both sides is their job. - Every fixture sets `pmacs.project.set_search_boundary` at its own tempdir root. Without it the marker walk climbs to the filesystem root and a stray `.git` above the temp directory turns the markerless cases into detected ones — the assertions would still pass while testing nothing. - **Found but not fixed here (pre-existing, own lane):** `ensure_server` never forwards `cfg.restart` to `pmacs.lsp.spawn`, so a `restart = "never"` in `pmacs.lsp.config[lang]` is silently dropped on the auto-attach path. At least one existing test sets it believing it takes effect. Out of scope for a PR whose acceptance 16 pins existing attach behavior as unchanged. - **Review round 1 addressed.** The blocker was process, not design: the test file was committed *before* `cargo fmt` ran, so the fix sat uncommitted in the working tree and the branch as pushed failed the first gate. The reported "fmt clean" described the worktree, not the branch — gate results are only meaningful when run against the pushed tree. Also added the two pins review asked for (a **string** `config .root` as an affinity key — acc17 only covered the function form; and `root = false` reading as unset), each bite-verified against exactly the mutation it targets and neither against the other. And documented the canonicalization obligation: the `"detected"` arm is canonicalized for free, a **configured** root is not, so on macOS a resolver returning `/var/…` and a detected `/private/var/…` are different keys for one directory. Stage 3's Lean resolver is the first real consumer, so the obligation is written at the point of use. - Verification on this branch: `cargo fmt --check` clean; strict workspace Clippy clean; 1,826 default + 2,003 CRDT library tests; multi-root 11/11; M4 121; statusline 7; completion popup 9; auto-pair 45; required GPU 155; **isolated-config workspace sweep 3,164 across 91 suites**; `git diff --check` clean. The sweep needs an isolated `XDG_CONFIG_HOME` and `-- --skip basedpyright`. ### Stage 3a — dispatch seams + `pmacs.fs.canonicalize` (branch `lean4-stage3a-seams`) - Worktree `../pmacs-lean-stage3`, branched off `githubsucks/main` @ `46a1b8f`. Carries framing **rev 5** (the Stage 3 split) as its first two commits, then the implementation, then a bite-driven correction. - **Stage 2 merged as #161** (`main` @ `46a1b8f`, 2026-07-25, two review rounds). COHERENCE.md §7 records the slice; §1.2 records the dead `pmacs.error` channel found landing it. - **Framing rev 5 splits Stage 3 into 3a and 3b** because rev 4 broke its own §4 rule — the row read "two `lsp.lua` generalizations" under prose claiming Stage 3 was Lean-only. One generalization shipped as Stage 2; the other (Q#LN9's seams) is the shared event drain, so it is now its own substrate stage. 3a and 3b are **strictly sequential** — 3b's subscriber is written against 3a's seam and both touch `lsp.lua`. - Ships: `pmacs.lsp.on_notification` / `on_response`, two arms in `handle_server_requests`, a pending-response purge, and `pmacs.fs.canonicalize` (Q#LN20). No protocol change, no Lean content. - **Two framing claims were corrected during implementation**, both recorded in §0.1 finding 6 and in the round-2 commit: 1. The reachable leak is **not** a killed buffer. The Rust core fires exactly five hooks (`buffer.after-edit`, `buffer.after-load`, `buffer.after-switch`, `frontend.detached`, `process.after-tick`) — **there is no buffer-kill hook**, so nothing tears an attachment down and the drain keeps reaching that server. The real path is `attach_buffer` dropping a dead sid from `attachments` and rebuilding against a fresh server, which makes `crashed`/`stopped` the event *least* likely to be drained. Hence the purge polls `pmacs.lsp.list()` rather than riding the drain. 2. Acceptance 32 does **not** pin "removed before invocation" — `pcall` catches the raise either way, so before/after is unobservable without a re-entrant drain. It pins removal being **unconditional**; renamed accordingly. - **`pmacs._fs` is installed from `install_async`, not `install_project`**, purely for load order: `make_workspace` runs *after* `fs.lua` is evaluated, so a canonicalizer placed there reads nil. This cost one failing run to discover and is the kind of thing to check first. - Bites recorded (all against the committed tree): removal gated on a clean return → acc32 fails 2 != 1; an event-driven purge → the no-attachment case fails "never called" while the attached case still passes; a resolver without `canonicalize` → two servers (34b's own falsification, which ships as a test). - **Known unpinned:** the purge's generation (`attempt`) check. Reaching it needs a crash *and* its restart to fall in a gap with no `_async.tick`; the backoff is 500ms, so any tick sees `crashed` first and the absent-or-terminal arm fires. Labelled as defensive in the code rather than left looking covered. - Verification on this branch: `cargo fmt --check` clean; strict workspace Clippy clean; 1,826 default + 2,003 CRDT library tests; dispatch seams 15/15 on Linux (14 on macOS — see below); multi-root 13/13; M4 121; required GPU 155; **isolated-config workspace sweep 3,189 across 93 suites, zero failures**; `git diff --check` clean. - **Two flakes/portability facts from CI round 1, both worth keeping:** 1. `composition_overhead_under_ten_percent` tripped once in a local sweep at 18.8% against a 10% budget, then passed 3/3 in isolation here, passed in isolation on main, and passed a full sweep rerun. The tell is in its own output: the same run reported realistic-frame overhead as **-4.6%**, and a negative figure is measurement noise, not added work. Load-sensitive under a parallel `--workspace` run. 2. **A non-UTF-8 filename fixture cannot be built on macOS.** APFS enforces valid UTF-8, so `std::fs::write` fails with EILSEQ ("Illegal byte sequence") before the code under test is reached. `#[cfg(unix)]` is NOT sufficient for such a fixture — `#[cfg(target_os = "linux")]` is. Cost one red CI round to learn. ### Stage 3b — the Lean language server (branch `lean4-stage3b-server`) - Same worktree `../pmacs-lean-stage3`, **branched off `lean4-stage3a-seams`, not off `main`** — 3b consumes 3a's response seam and `pmacs.fs.canonicalize`, so it is strictly sequential. **Retarget PR #170 to `main` BEFORE merging #167, not after** — the kill-ring lesson exactly. (Round 1 of this ledger entry stated the reverse in its first sentence and the correct rule in the next; the review caught it. A safety rule written twice with opposite senses is worse than not written.) - Ships `builtin/runtime/lean.lua` (new), one `include_str!` line in `src/editor.rs`, `pmacs.lsp._attach_buffer` exported from `lsp.lua`, a `leanprogress` mode plus `waitForDiagnostics` validation on `pmacs_fake_lsp`, and `tests/lean4_server_acceptance.rs` (40 tests). No protocol change. - **Stage 1's acceptance 12 is half superseded and was rewritten, not deleted.** It asserted `pmacs.lsp.config.lean4 == nil` to catch a Stage-3 front-run; 3b is that stage. What survives is the restraint half — constructing an editor spawns nothing though the config now names `lake`, and opening a Lean buffer with no server configured spawns nothing — which is what holds Q#LN7's "not at init" promise. - **The marker test is wrong in two opposite directions if done naively** and both are pinned: `io.open` SUCCEEDS on a directory (so truthiness accepts a `lean-toolchain` dir), but requiring a non-nil read rejects an EMPTY `lean-toolchain` (a legitimate marker — existence semantics, not content). Discriminator is `read`'s SECOND return; decline only on a non-nil err. Probed on LuaJIT 2.1. - **Fifteen bites recorded, each against the committed tree.** R1: bare `io.open` → 24a fails / 24b passes; require-non-nil → 24b fails / 24a passes; no canonicalization → symlinked open spawns two servers; no re-attach after the swap → three latch tests fail; hook keyed on the attachment → the missing-`lake` case fails; `waitForDiagnostics` without `version` → acc37 fails with InvalidParams. R2: skip retiring a terminal server → `attempt` reaches 3; no originating-buffer gate → the Lean buffer is left on the `lake` stub; retry-forever → the failing-fallback test fails; version-probe any command → the working-wrapper test fails; no disabled guard → the unconfigured test sees "`nil` could not be started". R3: verdict keyed on `watching` → the late-verdict test finds the buffer still on `lake`; `buf_key` rewritten per load → the second-buffer test fails; hardcoded `lake serve` → the wrapper-naming test fails. - **Round-2 review: three more P1 lifecycle defects, suite 20/20 with all of them live.** (1) The crashed primary respawned forever — skipping the retire call avoided corrupting terminal servers but left `next_restart_at` armed. **`forget` is the call for a TERMINAL server** (it requires terminal state and removes the client, dropping the restart timer); `stop` is for a live one and corrupts a terminal one. (2) Re-attachment targeted whatever buffer was active when the async verdict landed; an unrelated Rust attachment satisfied "a different server id". (3) A failing fallback retried every tick forever, silent. Plus two P2s: the Lake version parser was applied to arbitrary wrapper output, and an UNCONFIGURED `config.lean4` was reported as failure and latched, poisoning the session. - **Round-3 review: two more P1s, both asynchronous correlation, suite 25/25.** (a) `probe.watching` is cleared when the server initializes, so a SLOW version verdict arrived with nil and retired nothing — `_attach_buffer` returned the still-live primary and the retry called it success, so status and config said "fell back" while the buffer stayed put. **That is the round-1 silent no-op reached through a third event ordering.** `probe.primary` is now separate from `probe.watching` and survives initialization. (b) `buf_key` was rewritten on every Lean `after-load`, so a second Lean buffer opened before the verdict became the rebuild target while the latch still watched the first buffer's server. Target buffer and primary server are one fact and are now armed together, once. Plus a P2: the failure message hardcoded `lake serve` after the latch became command-agnostic, sending wrapper users to debug the wrong binary. - **Round-4 review: one P1, and it is the same defect a FOURTH time.** `pmacs.lsp.config.lean4` is a single global entry, so swapping its command invalidates **every** Lean buffer and **every** Lean server — Q#LN15 gives one per project root. Rounds 1–3 each fixed the repair for one buffer and one server; round 4 is "repair the armed target, strand the rest". The shape that finally holds: retire ALL `lean4` servers on latch, and repair each buffer **lazily and at most once** when it becomes active (`buffer.after-switch` + the tick), because `_attach_buffer` is active-buffer-only and cannot reach the others. The per-buffer once-only bound is what stops a failing fallback retrying forever — the round-2 defect a naive global repair loop would have reintroduced for every buffer instead of one. Plus a P2: the argument-inclusive attribution was implemented but pinned only by "contains the command name", so a mutation dropping every argument still passed. - **Round-5 review: one P1 plus a frontend scope hole, and four more.** (1) A fallback that SPAWNS and then dies retried forever: the once-per-buffer guard bounds `_attach_buffer`, not the server it produced, and `ensure_server` never forwards `cfg.restart` so the fallback inherits `OnCrash` — respawned by the manager with no ceiling, silently, because `latched` had disabled the primary's poll. The fallback now gets its own one-shot die-before-initialize watch. (2) **Simultaneous frontends**: both repair triggers read the ambient `pmacs.window.buffer()`, and the daemon restores `active_frontend` to the last-dispatched one before `tick_processes`, so a Lean buffer active in ANOTHER frontend gets no `after-switch` and stays stale. Fixed at the right seam — **make CONSUMPTION safe**: both `attached_for_active` and `attachment_for_request` now refuse a record whose server is dead (the former rebuilds, the latter reports none, since it must not perturb LSP state). Healing at the point of use is frontend-agnostic, because whichever frontend runs a command is active while it runs. (3) The retirement sweep selected on `language_id`, so it stopped USER-spawned Lean servers too; it now keys on the `default-lean4` label `ensure_server` stamps, which is the derivation discriminator. (4) `probe.latched` gated repair even when NO swap occurred, so an already-fallback config was retried and misreported. Split out `probe.fallback_installed`. (5) The once-per-buffer assertion counted TABLE KEYS, which cannot distinguish "once per buffer" from "every tick for one buffer" — cardinality stays 1 either way. Now a numeric attempt counter; the bite shows **174 vs 1**. - **Round-6 review: four P1s and one P2, suite 40/40.** (1) General point-of-use healing treated a crashed OnCrash server as absent and spawned beside it while its old id still had `next_restart_at` armed; `attach_buffer` now forgets a terminal record before replacement. `attachment_for_request` remains non-attaching and preserves the record, so a same-id restart can recover instead of being orphaned. (2) The fallback watch was scalar, while Q#LN15 permits simultaneous per-root servers and lsp.lua can create them without passing through Lean's repair function. Watches are now per-SID and discover every config-driven Lean server from a private origin table. (3) The shipped `lean.wait-for-diagnostics` command bypassed both safe resolvers and still consumed a stopped record; it now uses a command-safe resolver, waits asynchronously for a healed replacement to initialize, and the test requires the real request to finish. (4) When no config swap occurred, one failed root still swept a healthy root; that arm now retires only the SID whose verdict fired. (5) `label` is public and unreserved, therefore not ownership. lsp.lua records successful config-driven spawns privately, and every Lean lifecycle decision keys on that origin fact; the user-server pin deliberately collides on `default-lean4`. All five bites against `19f48d4` discriminate: the old files produce 2 same-root servers, a fallback attempt of 4, a shipped command still targeting `stopped`, retirement of the healthy root, and retirement of the colliding user server, respectively. - **DURABLE LESSON — "the test that passes" vs "the test that discriminates."** Green tests across six rounds repeatedly pinned only a nearby helper or an absence, and only biting exposed it. **Carry this to `docs/agent-handoff.md` when the lane lands.** The concrete shapes, all from this branch: 1. R1 acceptance 36 asserted "every server is terminal" — pinning the ABSENCE of the fallback it claimed to test. 2. "No live non-fallback server" misses a respawn loop: a respawning server sits in `crashed` most of the time. `attempt` counts respawns; liveness does not. 3. Returning to a buffer via `find_or_open` re-fires `buffer.after-load`, which repairs the attachment regardless of the code under test. Use `switch_buffer`. 4. A MISSING executable fails synchronously inside `after-load`, where the rebuild happens inline — no async race can occur. Only the probe path exercises asynchronous ordering. 5. A mutation that RAISES (indexing a nil config) is swallowed by the hook's pcall, so the bite "passes" for the wrong reason. A bite must reproduce the original shape, not merely break the code. 6. A fixture whose `serve` sleeps can never let the primary initialize first, so it cannot reach the ordering where a late verdict must retire a LIVE server. 7. Asserting on a field that no longer exists (`_probe.reattach_from` after a refactor) reads as nil and passes for nothing. Assert positive facts — a count, a command string — not absences. 8. Counting DISTINCT KEYS cannot bound REPEATED WORK: a per-tick retry on one buffer keeps `#repaired == 1` forever. Count the attempts, not the things attempted against (bite: 174 vs 1). 9. A NONEXISTENT executable only exercises synchronous ENOENT. To reach "spawned, then died", the fixture must actually spawn. 10. Calling the two SAFE HELPERS directly does not pin a shipped command that bypasses both. Drive the command registry entry and require its terminal result — replacing a dead record with a `starting` server is still not success if the request is issued before initialize. Rule: **a test is not evidence until the mutation it targets has been shown to fail it.** - **SECOND DURABLE LESSON — a scope error repeats until the scope is named.** The "fallback silently does not happen" defect came back four times: no re-attach; re-attach cleared by an unrelated buffer; re-attach satisfied by the server being replaced; re-attach of one buffer while the others stay stale. Every fix was locally correct and none asked *what does this config swap invalidate?* — the answer being every Lean buffer and every Lean server, because the config entry is global and servers are per-root. **When a change edits shared state, enumerate everything derived from it before repairing anything.** - **SUBSTRATE BUG FOUND, not fixed here (framing §6).** `LspManager::stop` on an ALREADY-terminal server takes its not-initialized branch, terminates the dead process and sets `ShuttingDown { .. None }` on the premise that "the next exit observation cleans up" — but the exit already happened, which is what made it `Crashed`. No further event arrives, so the client is stuck in `ShuttingDown` **forever**: `server_is_live` reads it as LIVE, so `attach_buffer` never rebuilds, and `forget` refuses it for not being terminal. **Stopping a dead server is what makes it un-replaceable.** Lean works around it by dispatching on state: `forget` when terminal, `stop` when live. Merely SKIPPING the call is not enough — that leaves `next_restart_at` armed. - Round-1 review found four P1s, all real: the latch swapped the config but never spawned or re-attached (and acc36 *asserted every server was terminal*, pinning the absence of the fallback); a missing `lake` bypassed probe and latch entirely because the hook keyed on an attachment that ENOENT prevents; `waitForDiagnostics` omitted the `version` Lean requires; and the ledger stated the dangerous stacking order. - The probe's non-zero exit is deliberately NOT a fallback trigger — §2.9's elan shim makes `lake --version` fail where `lake serve` still works. Only a parseable version below 3.1.0 triggers it; the server-failure latch covers the rest. - Verification on this branch: `cargo fmt --check` clean; strict workspace Clippy clean; 1,829 default + 2,003 CRDT library tests; lean4 server 40/40; lean4 stage 1 9/9; dispatch seams 15/15; multi-root 13/13; M4 121; required GPU 155; **isolated-config serial workspace sweep 3,229 across 94 suites, zero failures**; `git diff --check` clean. (Round 1 of this entry recorded 17/17 and 3,206 — the PRE-fix counts — after the fixes were pushed. The ledger's protocol is that verification describes the pushed tree; recording it late is the #161 fmt-blocker error in a slower form.) ## Dired lane — framing APPROVED; Stage 0 MERGED, Stage 1 next - Approved framing: `docs/dired-framing.md` (revision 5), landing as its own docs PR off `githubsucks/main` @ `2af1ab3`, branch `githubsucks/dired-framing`, worktree `../pmacs-dired-framing`. The repo's `-framing`-branch convention (`vterm-framing`, `gpu-initial-target-framing`, `tab-width-parity-framing`). - **Stage 0 (`C-x C-f` find-file) MERGED as #162** (`main` @ `2af1ab3`, 2026-07-25, one review round, 12/12 CI green). Durable facts moved to `docs/agent-handoff.md` §1 per rule 3 below. - **Stage 1 (the dired view) is next and unstarted.** Branch `dired` (worktree `../pmacs-dired-arc`) carries the framing commits only and is based on the now-superseded `0827dd1`; **rebase it onto the `main` resulting from the framing PR before implementing**, or cut a fresh branch — its framing commits become redundant once the docs PR lands. - Stage 1's scope, from the framing §10: `builtin/runtime/dired.lua`; the `dired` major mode + mode keymap; buffer-per-directory with lexical canonicalization and the ownership check; read-only intercept + `set_round_trip_input`; visit routing through `window.display_file`; parent/sort/revert/quit; `C-x d` (with the `display` opt) / `C-x C-j`; cursor preservation by basename; the `dired.kill-when-opening` config key; **and the tolerant `read_dir` opt** — the only Rust in the stage. - The one Rust change is load-bearing and is why Stage 1 is not pure-Lua: `read_dir_blocking` (`src/fs.rs:201`) fails the **entire listing** on any of five per-entry conditions, and the tolerant wrapper its own module doc delegates to package authors **cannot be written in Lua** — the primitive returns one error and no partial vec. - Coherence (framing §0.5, required since #163): serves `COHERENCE.md` §20 Priority 1, which names this work explicitly; journey steps 7 and (partially) 3; **adds no interaction island** — keys are a mode-scoped keymap, and wdired is a mode swap; adopts `pmacs.config` for `dired.kill-when-opening`; inherits §9's worker-attribution gap for its `read_dir` jobs without worsening it. - **Boundary with the Journey Stage 1 arc** (`COHERENCE.md` §20 arc-cut 1): CLI directory-argument handling (`pmacs .` exits 1) belongs there, not here. The two meet at `resolve_target_buffer`; dired supplies the buffer a directory should resolve *to*, and `pmacs .` should route into it rather than growing a second directory surface. ## Bottom-panel lane (window placement + side windows) — Stage 1 IN REVIEW - Portable branch: `githubsucks/bottom-panel`, worktree `../pmacs-bottom-panel`, based on `githubsucks/main` @ `ddaa80d`. - Approved framing: `docs/bottom-panel-framing.md` revision 4, committed as the branch's first commit (`c27f75a`). - **Stage 1 implemented; no wire change (protocol stays v20).** What landed on the branch: - `src/window.rs`: `WindowParams` (`side` / `fixed_rows` / `dedicated` + implementation-owned `quit_action` and `origin_document`), `Side`, a depth-bounded `QuitAction`, `MIN_WINDOW_OUTER_ROWS = 2`, `Layout::compute(area, fixed)`, the `subtree_min_rows` / `interactive_min_rows` recursions, `boundary_below`, and the three new `FrontendView` fields (`panel_capable`, `frame_geometry`, `panel_hidden`). - `src/editor_core.rs`: `primary_document_window`, the non-side target rule, `display_buffer` + the Q#BP3 placement policy, `quit_window`, `reconcile_panel_layout_core`, `resize_boundary`, per-frontend `JumpEntry`s, and the shared `resolve_target_buffer` seam that the #148 initial-target bootstrap now routes through as well. - `src/editor.rs`: the reconciliation transaction, geometry declaration, the side-window `dispatch_idle_for` gate, the divider paint, and the divider drag. - `src/lua_bindings/window_panel.rs`: the whole `pmacs.window` panel surface plus the shared adopter-placement helpers; `builtin/runtime/window.lua` owns `window.panel-height` / `window.min-height` and the resize commands. - Adopters: `listview.open`, `compile.run`, `pmacs.terminal.open` all take `display = "current" | "panel"` (Stage 1 default `"current"`); LSP/compile visits route through `display_file`. - **Review round 1 addressed.** The load-bearing finding: the Q#BP6 side-window split guard (`try_split_active`) had **no production caller** — `pmacs.window.split_horizontal` / `split_vertical`, and so `C-x 2` / `C-x 3`, still went through plain `split_active`. Splitting a focused panel made the root wrapper's final child a split rather than `Leaf(side)`, which both `Layout::compute`'s fixed pass and `document_subtree` key on. It survived the first round because the acceptance test called the core method **directly**; it now goes through the real Lua binding. This is the folding-arc round-2 lesson repeating exactly: *after wiring a guard into a production hook, pin it through the real path — a direct-call test misses the wiring.* Also fixed: the armed divider drag was not scoped to its arming frontend (it could cancel and swallow a peer's mouse events); a recompile carries no `display` and duplicated a panel-placed `*compilation*` into the document window; and `paint_mode_line_graphemes` had lost its doc block to an insertion. Five bite-verified fixes (three via `scripts/bite`, two by manual revert since their tests share `src/daemon.rs` with the production code). - Two Stage-2 hazard pins now exist in `src/daemon.rs`, closing the gap the review named: a fresh attach while `LOCAL` is focused in a panel inherits `LOCAL`'s **document** buffer, and an initial-target bootstrap whose `after-load` hook creates and selects a panel still reasserts into a document window. - **Review round 2 addressed.** The load-bearing finding: **Q#BP7 item 1 — "growth reaching the live tail re-arms follow" — was never implemented.** `at_bottom` is the instantaneous geometric readout `scroll_offset == 0`, which a still-anchored view satisfies whenever it is momentarily tall enough to reach the tail, so the round-1 assertion could not see the gap: the next rows the child printed pushed the anchored view back into history. `src/terminal/view.rs` now has `rearm_follow_on_growth`, reached by one shared `declare_view_size` helper from every size-declaring path (`snapshot_for_view`, `record_view_size`, `view_status_for_size`) so grid and semantic declarations cannot disagree. Also fixed: the PTY fixtures emitted LF-only output, which staircases until every row clips to blanks — so the anchor assertions compared `""` with `""` and could not fail (now CRLF, each guarded by `assert!(!top_before.is_empty())`); acc33's contrast case asserted nothing; `start_run` let `already_in_panel` override an **explicit** `display = "current"`, which is the documented opt-out from the Stage 3 flip (now gated on omission); and `window_drag` was a daemon-global slot that a peer's mode-line press could clear. - Durable test lessons from this round, both the same class: 1. **A geometric readout is not a state predicate.** `at_bottom` says "the viewport currently reaches the tail", not "this view follows the tail". Pinning follow requires feeding MORE output and asserting the view moved (acc32b uses a filesystem gate between two bursts). 2. **A PTY in the default mode does not translate LF to CRLF.** An `echo`-driven fixture staircases rightward and clips to blanks past the viewport width, so any text equality over it is vacuously true. Emit `\r\n`, and guard text comparisons with a non-empty assertion the way the daemon pin guards on `!panel_hidden`. - **Round-2 self-review caught a regression the round-2 commit introduced**, in the change it labelled "minor": routing `pmacs.window.buffer()`'s **no-argument** arm through the fid-scoped `selected_window` validator made it **fallible**, and `acting_frontend` can name a frontend with **no registered view** (a bare `dispatch_key` from an unattached peer does exactly that). The runtime calls that function on ordinary edits from `killring`, `syntax`, `autosave`, `pair`, `indent` and `comment` **without `pcall`**, so the raise never surfaced as an error — it silently dropped the operation. `kill_ring_acceptance` went 30/30 → 25/5 (`frontend_detached_drops_per_frontend_state`: "B has kill state"). The no-arg arm is back on ambient `active_buffer_id()` and documented as deliberately infallible; the explicit-window arm keeps its Q#BP11 validation. New **acc19c** pins it through the real path (a `buffer.after-edit` subscriber during a viewless peer's `dispatch_key`) and bites against the regressing commit. Generalizes: **a "uniformity" cleanup that changes a function's fallibility is not minor** — check every caller's error discipline first, and remember that an ambient resolver's fallback IS its contract. - Verification on this branch: `cargo fmt --check` clean; strict workspace Clippy clean; 1,817 default + 1,994 CRDT library tests; `bottom_panel_stage1_acceptance` 46/46; kill ring 30 default + 30 CRDT; vterm Stage 1 9 default + 10 CRDT; M4 121; required GPU 152; compile 67; vterm Stage 2 4 / Stage 3 5 (7 CRDT); folding Stage 2 48; statusline 7; listview 6; **isolated-config workspace sweep 3,130 passed across 89 suites, zero failures**; `git diff --check` clean. - **Run the sweep with an isolated `XDG_CONFIG_HOME`.** The real `~/.config/pmacs/init.lua` on this desktop calls `pmacs.packages.install_local(...)`, so every editor the sweep builds races on one shared install root; a losing race sets a status message that leaks into the mode line and breaks `folding_stage2_acceptance::unfolded_frame_is_identical_to_the_pre_folding_baseline`, which compares whole painted frames. Standalone it is 48/48. This generalizes the known `compile_mode_acceptance` real-config trap: any suite that paints the status area inherits it. - **A latent pre-existing `main` bug surfaced while gating and is NOT this branch's**: `buffer::tests::proptests::rope_matches_crdt_projection_after_arbitrary_edits` fails on `main` @ `352bf0b` with `ops = [Insert(0,"a"), Insert(0,"aaa"), Replace(0,1,"a"), Undo]` — undo of a textually-null `Replace` returns a no-op edit result still carrying `crdt_op = Some`, violating the suite's own shape invariant. `src/buffer.rs` is byte-identical here, and the seed was deliberately **not** committed (it would make an unrelated failure deterministically red on this PR). Needs its own lane. - Durable test lesson from this round: `TerminalViewStatus.scroll_offset` is documented as the retained rows between **this viewport** and the live tail, so it necessarily tracks the viewport height. Asserting it constant across a panel height change is either vacuous or wrong — the invariant Q#BP7 actually states is that the **anchor** is frozen, which the acceptance now pins by comparing the first visible row's text, plus `at_bottom` for the follow re-arm. - `compile_mode_acceptance` needs `--test-threads=1` locally; it is 67/67 there. Under default parallelism it fails roughly 1 run in 3, with a *different* test each time (acc14/acc25a, then acc24) — **verified pre-existing** by swapping in `githubsucks/main`'s `builtin/runtime/compile.lua` and reproducing the same rate. The `pmacs-gpu` bin tests have historically gone red under a loaded sweep (wgpu device contention). Rerun isolated before treating either as a regression. - Stage 2 (the GPU panel band, next available protocol version) has its own re-framing obligation before implementation; Stage 3 is the default placement flip. ## Folding lane (Arc 6) — Stages 1 and 2 MERGED; Stage 3 (GPU) is next Both shipped stages are on `main`; nothing in this arc is in flight. Stage 3 has **no branch and no framing yet**. - Stage 1 (headless fold engine) merged as **#142**, Stage 2 (grid/daemon collapse) as **#149** — both under "Closed since the last snapshot". - Retained, carrying nothing unmerged: branches `folding` / `folding-tui` and worktrees `../pmacs-folding` / `../pmacs-folding-tui`. The framings `docs/folding-framing.md` (rev 5) and `docs/folding-stage2-framing.md` (rev 4) are the approved artifacts Stage 3 re-scouts against. - **Stage 3 (GPU) obligations, already named by the framings** — the starting point for its own framing doc: GPU collapse at TUI parity; caret/hit-test fold-awareness; the `BufferSnapshot` **fold-mirror clear** (parent R2-4 — without it, empty-after-revert diff suppression leaves stale folds on the GPU, the same trap class as #120); CRDT-origin and GPU-optimistic interactive unfold (parent R2-3); and flipping `FrontendView.fold_projection` to `true` for semantic frontends, which Stage 2 deliberately left `false` (Q#FD21). ## Parked lane: kill-ring browser + persistence - Portable branch: `githubsucks/kill-ring-browser` - Parked framing head: `503c489` - State: framing only, revision 2; no implementation and no PR. - Status: explicitly parked by the user on 2026-07-20. - Its original scout was based on `0efb5cd`. The preserved framing marks this ground truth stale and requires a complete re-scout against the then-current `githubsucks/main` before implementation. - Compile-mode has merged since the original scout, so old “compile-mode in flight” keybinding/touch-set assumptions are not authoritative. Recovery worktree, only when the user un-parks it: ```sh git worktree add --track \ -b kill-ring-browser \ ../pmacs-kill-ring-browser \ githubsucks/kill-ring-browser ``` ## Documentation lane - Portable branch: `githubsucks/handoff-2026-07-20` - Carries synchronized `AGENTS.md` / `CLAUDE.md`, this ledger, the durable handoff refresh, and the keybinding reference correction. - It changes no runtime code. - Review and merge this documentation branch separately; it must not be folded into a feature framing branch. - Now also absorbs both landed arcs: Vterm Stage 1 (#126) and the config registry (#127). Canonical `main` is merged into it up to `2e37c04`, so its diff against `main` is documentation only. ## Closed since the last snapshot - **GPU initial target — MERGED as #148** (`main` @ `0dd16a5`, 2026-07-24, after two review rounds). `pmacs --gpu [--socket …] FILE` opens a target before the GPU window appears. Protocol bumped 19 → 20: a semantic-session `SessionBootstrapRequest` after `AttachRequest`, plus an appended `InstanceMessage::InitialTargetResult` pre-window readiness barrier; v6–v19 wire encodings are unchanged. Root owns launcher tilde/cwd resolution and exact raw-byte path transport; the daemon resolves/dedups/loads the target and runs load/switch hooks inside one dispatcher transaction, then publishes CRDT-upgraded targets to existing grid replicas (gated on `upgraded_to_crdt`, independent of the load/create outcome, so a dedup onto a hidden not-yet-backed buffer still reaches pre-attached replicas — round 2 finding). Semantic replicas receive a publication only when displaying that buffer, so a second target launch cannot switch an existing GPU window. Round 2 also closed a failure-containment gap: every dispatcher-side bootstrap failure now shuts down the socket (a dropped write-half clone does not close a shared FD), and the dispatcher drops any event from a session that was never installed, rather than reaching absent render/size state. Integrated cleanly with Folding Stage 2 (#149): fold projection at attach is selected from the same negotiated `semantic_render` bit the target bootstrap uses. Its lane, worktree (`../pmacs-gpu-initial-target`), and branch (`gpu-initial-target`) are done; the `-framing` branch is kept. Durable substrate facts and both review-round lessons live in `docs/agent-handoff.md` §§1/5 and `docs/gpu-initial-target-framing.md` rev 3. - **Folding Stage 2 (grid/daemon collapse) — MERGED as #149** (`main` @ `6ed4fe9`, 2026-07-24, after **five** review rounds). The grid TUI now renders collapses. Spine (Q#FD12): `src/fold_view.rs`'s `VisibleLineMap`, derived from the fold store plus a window's line offsets and **never stored**, threaded as `Option<&'a VisibleLineMap>` on a lifetime-bearing `Viewport<'a>` that stays `Copy`. No wire schema or protocol change; the GPU path is Stage 3. 48 acceptance tests on the real `paint_frame` grid, every behavioral claim bite-verified. Durable design points, each a trap Stage 3 inherits: - the map's unit is a **merged hidden component** (overlapping *or adjacent* intervals unioned, keeping the earliest visible head), not a fold — folds may cross, and a later fold's own head can be hidden; - instances are **per rendered window** and **per command/event operation**, never per frame; a command's map follows the operation's **target** window, since a wheel event names a pane without activating it; - fold projection is **per-frontend** (`FrontendView.fold_projection`) — shared `EditorCore` motion would otherwise make a simultaneous unfolded GPU session's cursor skip lines it still displays; - a hidden cursor normalizes by **position**, not row, and `set_view_top` clamps in the setter rather than being repaired at render time; - the interactive-Lua unfold keys on the **post-intercept** edit site — a managed buffer intercept may legally relocate the op. Process notes worth keeping: `main` moved under the arc, and the merge was textually clean but **not semantically clean** (#146 added `Viewport` literals the new `folds` field invalidated) — a clean `git merge-tree` does not mean the merged tree compiles. CI was red at review on the macOS/luajit `outline_5_level_100_entry_renders_within_100ms` budget flake and went green on rerun. - **Documentation ledger refresh — MERGED as #147** (`main` @ `0a479ae`, 2026-07-24). The #142 housekeeping, expanded after review found the ledger stale through four merges rather than one. Its own macOS/luajit red was the vterm `VTERM_ALT_READY` PTY timeout; green on rerun. - **Web grammars HTML + CSS — MERGED as #146** (`main` @ `47581f4`, 2026-07-23). `.html/.htm/.xhtml` and `.css` highlight off the official `tree-sitter-html` 0.23 / `tree-sitter-css` 0.25 crate query constants (no in-repo overlay), and HTML's `INJECTIONS_QUERY` lights up `