14 KiB
Agent handoff — cross-machine continuity
Last updated: 2026-07-14, on the laptop, by the compile-mode
session (post-merge snapshot). This file is the
bridge between development machines. If you are an agent reading
this on a fresh clone: this document plus the docs/*-framing.md
files ARE your memory. Read this fully before taking on work, seed
your persistent memory from it, and update this file (and commit
it) whenever project state changes materially — the next machine
reads it the way you just did.
1. Where the project stands (2026-07-14)
main@98323df(compile-mode #113 merged), protocol v15 (SUPPORTED=[6..15]; no bump in #113).- Compile-mode (Arc 5 stage 1, #113) LANDED — merged 2026-07-14
after seven PR review rounds (framing
docs/compile-mode-framing.mdat revision 13, revisions 7–13 = the rounds; final commit was the user's atomic-teardown hardening). Shape:compile.runstreams/bin/sh -c "exec 2>&1; <cmd>"(pipes,stdin="null",group=true, TERM=dumb) into an intercept-read-only*compilation*buffer via a Lua-side ANSI parser (column-counted newline-segmented CR/BS rewrites, tracked O(1) line start); once-per-newline error rules (raw-read validated per-run snapshots); unifiederror.next/error.previousdispatcher (M-g n/p taken over from diag with fallback;C-x `; M-! shell-command); buffer-revision external-edit guard with desync marker + anchor epochs; grep-mode upgrade ofproject.search. New substrate other code can use:ProcessSpec.stdin/group(group lifecycle: reap ledger, in-drain enforcement, cancellable poll readers),buf:revision(), jump_back firesbuffer.after-switch,pmacs.errors.claim,parser:finish()/AnsiParser::finish()(observable reset;emitted_stylealt-screen resync), and the style-overlay stack: buffer-attachedBufferStyleSpanTranslator(translation exactly once per edit, fragment-preserving, no-op-edit immune), render-only window overlays with identity-deduped attachment (Window::ensure_overlay), split copy (clone_for_split), validatedattach_style_overlay, idempotent atomichandle:dispose()(registry detach works headless). - Editing-conveniences pack (editops, #111) landed — the Lua
parallel lane. Framing
docs/editing-conveniences-framing.mdat revision 6 (three pre-branch rounds, one adopted post-approval hardening, one PR round: full UTF-8 scalar validation, per-word capitalize with the_-constituent deviation named, trim-on-save dual-channel error reporting). Ships goto-line, case ops, transpose, zap-to-char (kill-chain member with an origin guard and killring's pending-prompt marker), line move/duplicate/join, region sort/reverse/dedupe, delete-trailing-whitespace + opt-inpmacs.editops.trim_on_save. New substrate other code can use:pmacs.killring.kill_range/break_chain([fid])/arm_kill_prompt+commit_kill_prompt(the marker lifecycle), and the origin-guard pattern for chain-sensitive minibuffer commands. The development worktree at../pmacs-editopshas been folded back. - Auto-pairing (#110) landed — Arc 2 is COMPLETE. Framing
docs/auto-pairing-framing.mdat revision 6 (two pre-branch rounds- three PR rounds). Shape that shipped: the nine built-in pair
chars leave both frontends' optimistic classifiers
(
BUILTIN_PAIR_CHARSin pmacs-protocol; dispatch-routed → adjacent daemon-peer undo units); reaction hookbuiltin/runtime/pair.lualoads BEFORE lsp.lua (first-didChange ordering contract); exact one-shot typed-edit provenance viapmacs.editor.take_typed_edit()with a buffer-revision postcondition (Q#AP9). New substrate other code can use:buf:path(),pmacs.lsp.buffer_language(buf),PMACS_FAKE_LSP_CHANGE_SINK(fake-LSP doc-sync replay),TestDaemon::spawn_with_config(init.lua-carrying daemon fixture).
- three PR rounds). Shape that shipped: the nine built-in pair
chars leave both frontends' optimistic classifiers
(
- NEXT: themes (Arc 4) is the standing runner-up from the
decision discussion — scout fresh before framing (protocol bump
v15→16 for a ThemeFacts channel, the LineNumbers/Q#UX1
control-plane template, glyphon font reload is the hard part).
PR #114 (
cuda-lsp: clangd + bundled CUDA grammar) is the USER'S own lane — hands off; scout main freshly before branching. - Roadmap:
docs/roadmap-2026-07.md(ranked arcs). Position:- Arc 1 (LSP utility surface) COMPLETE — completion popup (#92/#93), panels/references/outline/hover (#94–#96), plus hardening follow-ups (#102, #105, #106).
- Arc 2 (editing table stakes) COMPLETE — query-replace (#97),
kill ring +
M-y(#103/#105/#106), comment-toggle (#107), auto-indent (#109), auto-pairing (#110). - Arc 3 (persistence) COMPLETE — saveplace/recentf (#98), desktop-save (#99), autosave/crash-recovery (#100), save-clobber fix (#101).
2. How we work (the part that must not drift)
The user is expert and reviews deeply — they falsify framings and find real bugs in round after round. The cadence that has worked for ~40 PRs:
- Scout ground truth in the code before proposing anything.
- Write a framing doc —
docs/<feature>-framing.md, numbered decisions (Q#XY1…), explicit "Ground truth", "Bets", "Deferred (named)", and an acceptance-test list. Present it and wait for explicit approval ("Go for it" / "Ready to roll"). Expect 1–3 rounds of findings first; revise the doc, don't argue. - Branch off main (one feature = one branch = one PR). Commit the framing as the first commit.
- Implement. Every reviewer finding gets a bite-verified fix — a test that fails without the fix. Watch for vacuously-passing tests.
- Run the full gate suite (§3). Open the PR with
gh. - The user replies with "Findings" lists on the PR rounds too. Same discipline. They say when to merge — never merge unprompted.
- After merge: update this handoff + your memory.
Commit/PR conventions: commit messages via git commit -F <file> (no
inline backticks through the shell); end with the Claude co-author
line. PR bodies end with the Claude Code attribution. Clippy runs as
its own step, never &&-chained.
3. Gate suite (all green before any PR)
cargo fmt --check
cargo clippy --workspace --all-targets -- -D warnings # own step
cargo test --lib # ~1500
cargo test --lib --features crdt # ~1672
cargo test --test <the new/touched acceptance suites>
cargo test --test m4_acceptance -- --skip basedpyright
PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu # 58
cargo test --workspace -- --skip basedpyright # full sweep
git diff --check
Machine-specific caveats — re-verify on a machine you haven't used before trusting them:
- basedpyright: the DESKTOP's local binary is broken and HANGS the
m4_5_basedpyrighttests — hence the--skipthere. The LAPTOP has a working basedpyright 1.39.9 (verified 2026-07-10: the m4_5 test passes in 0.18s), so the skip is droppable on the laptop. - GPU on the laptop: AMD Radeon 780M (RADV) — native Vulkan,
PMACS_REQUIRE_GPU=1works without lavapipe. - m8 daemon tests are FLAKY (timing). A lone m8 failure → rerun before investigating.
- GPU tests need a Vulkan device.
PMACS_REQUIRE_GPU=1makes absence a hard failure instead of a silent skip. Headless option: lavapipe (seedocs/repository-audit-2026-07-03.mdfor the CI harness how-to). On a laptop without discrete GPU, mesa/lavapipe works. - The desktop's shell is fish (no
$(...), use(...); no$UID, use(id -u)). Check$SHELLhere before assuming.
4. Substrate invariants (do not undo; tests enforce most of these)
Command boundaries (Arc 2 kill-ring substrate) —
EditorCore.command_history: HashMap<FrontendId, CommandBoundary{this, last}>,
per frontend. Rotate on: keybound command, self-insert, menu invoke,
invoke_interactive (the M-x path). Break on: unbound key, GPU
optimistic CRDT edits, pointer gestures (wheel scroll deliberately does
NOT break), unified paste. Plain pmacs.command.invoke stamps
nothing (programmatic API); invoke_interactive rotates-then-invokes
(Emacs M-x semantics). Single-codepoint optimistic CRDT inserts
classify as buffer.self-insert (exact decode; "a(" breaks instead).
Lua: ed.this_command() / ed.last_command().
Effective-edit returns — buf:insert/delete/replace return the
post-intercept (start, end, inserted_len). Callers that care
(killring, comment) compare EXACTLY against the request; length-delta
and text-at-position checks are documented defeated patterns. Always
pcall the mutator: a rejecting intercept must report, not throw
through, and failed ops must leave no state (kill chains, yank
sessions).
Kill ring — entries {id, text} with stable monotonic ids; chains
and yank sessions are per-frontend and id-checked (an index is not
stable under other frontends' pushes). OS clipboard mirrors to the
acting frontend only. Paste is a unified daemon arm keyed by the
dispatcher's AUTHENTICATED source — never a payload frontend_id.
LSP outbound positions — every Position/Range builder in
src/lsp.rs routes through outbound_position (byte → negotiated
encoding). Any new request builder must too; UTF-16 servers reject raw
byte columns on non-ASCII text. Semantic tokens: full, full.delta,
and range are three INDEPENDENT capabilities — gate each.
Persistence (Arc 3) — state-dir wiring lives in
install_state_dirs() on real entry points only, NOT EditorState::new()
(tests stay hermetic); PMACS_STATE_HOME overrides. Autosave: one
buffer owns a path's recovery slot; only recover/discard release
unclaimed crash data; adopt clears the old owner's skip cache.
Protocol — encoding-breaking bumps are deliberate and versioned
(SUPPORTED=[6..15]). v15 = CompletionPopup + StatusFacts.message.
New wire surface ⇒ bump + both-frontends support + acceptance.
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.
5. Hard-won ops lessons
- The checkout may be shared with the user. Check
git statusfor foreign uncommitted work before any stash/checkout/branch surgery; never assume dirty files are yours. (Their uncommitted fix was nearly orphaned once.) A clean status goes stale within minutes when two lanes are active — for parallel work,git worktree adda sibling directory off main instead of switching the shared checkout. - Never
git stashin this repo. The stash namespace is REPO-GLOBAL — one list shared across every worktree and with the user; a scripted push/pop can pop a human's years-old stash into your tree (happened during #111: a failedstash pushchained intostash pop, which grabbed the user's PR-#17-era entry). For run-tests-against-an-old-version swaps, usescripts/bite— a trap-guarded one-file swap over read-onlygit show, with an inverted verdict (exit 0 iff the tests FAIL against the old version), making bite-verification machine-checkable. - Stacked PRs: retarget the child to main BEFORE merging the parent — GitHub auto-closes a PR whose base branch is deleted and cannot reopen it (#104 → re-opened as #105).
- Scripted edits (sed/python) in files with repeated similar blocks
(
src/lsp.rsJSON builders): anchor on a unique line or you will silently edit the wrong block. This produced a vacuously-passing test and cost an hour. - Acceptance fixtures that open
.rs/.pyfiles must emptypmacs.lsp.configfirst — the after-load hook spawns real servers (rust/python/c have default configs). Language detection (grammars +pmacs.lsp.filetypes) is unaffected. - Test scratch buffers have no path ⇒ no language; use tempdir files when language matters.
6. Named deferrals (the standing backlog, consolidated)
Editing: word kills (M-d/M-BS — need bytes-returning deleters +
prepend-on-backward append), C-SPC set-mark, C-u C-y / C-M-w,
kill-ring browser + persistence, clipboard watching, block comments +
mid-line comment spans, comment-dwim append-at-EOL, per-language
comment padding. Pairing (framing "Deferred"): wrap-region on opener,
pair-aware backspace, RET-inside-pair closer-on-own-line,
in-string/in-comment inhibit (needs node-at-byte pmacs.parse),
undo amalgamation (pair = one step), balance-aware quotes,
per-buffer toggle (config-registry-blocked). Editops deferrals (full
list in its framing): recenter (blocked on viewport facts — the GPU
never consumes daemon view_top), Unicode case/word classes,
region-spanning move/duplicate, locale collation for sort-lines,
ensure-final-newline on save.
Substrate: buffer-aware edit epoch (after-edit currently compares the
ACTIVE buffer only), wire provenance for CRDT self-insert
classification, Lua intercept probe, completion.lua still on the old
cursor-delta heuristic (migrate to this_command), the TUI's
nonempty-selection optimistic type-over gate, generated-buffer search
invalidation, cross-peer chronological undo arbitration (mixed
source/daemon history; pinned by auto-pairing acceptance),
origin-pinned buffer.after-edit fan-out (a context-switching
intercept changes what later callbacks — LSP, completion — observe).
LSP/persistence: hidden-buffer LSP attach, daemon desktop-restore, the
warning half of external-change detection (verify-visited-file-
modtime), config registry (no unified config surface yet).
GPU: auto-reconnect after daemon restart, splits/multi-buffer, gutter
riders (whitespace guides, folding, git markers).
Housekeeping: F-016 lua_bindings/mod.rs split paused mid-way
(tranches 0–2 landed, ~5–8 PRs left; see
docs/lua-bindings-split-framing.md).
7. Machine-local facts (desktop) that do NOT travel
Three untracked files live only on the desktop working tree and are
deliberately never committed: docs/pmacs-gpu-editing-perf-handoff.md,
docs/session-5-stale-styling-handover.md, python_experiment.md.
Don't expect them in a clone; on the desktop, never delete them.
8. Update protocol for this file
When a PR merges, an arc opens/closes, or a decision lands: edit the snapshot (§1), append lessons (§5) and deferrals (§6) as they arise, bump the date line at the top, and commit — usually riding the same PR as the work. Keep it under ~250 lines: this is a briefing, not a log; prune sections that stop being true.