pmacs/COHERENCE.md

78 KiB
Raw Blame History

Product Coherence for Pmacs

Status of this document

This document has two jobs. It states the product-coherence thesis for pmacs, and it records the audited ground truth of how the codebase measures against that thesis, so that no future agent or contributor has to re-excavate it.

  • The vision prose is durable. The Ground truth subsections were established 2026-07-25 by a four-lane code audit (discoverability, interaction islands, packages/workers, first-run journey) plus a distribution check, on branch lsp-multi-root-affinity (= main @ 0827dd1 plus the multi-root LSP work).
  • Citations name symbols first, file:line second. Line numbers drift with the tree — docs/keybindings.md drifted by 2501000 lines within days of its "last verified" stamp (§24) — so treat the symbol name and the structural claim as authoritative and the line number as a hint. Re-grep before relying on a number.
  • Grades used below: Strong / Partial / Weak / Missing (and Broken where something actively fails).
  • Update protocol is §25. When a PR changes any audited claim here, updating this file rides that PR, the same way docs/agent-handoff.md does.

Relationship to the other required documents: docs/agent-handoff.md carries durable project state and working method; docs/active-work.md carries volatile branches and recovery; docs/side-quest-backlog.md carries item-level deferrals. This document carries product direction and the measured distance to it. It is not a second backlog; it is the standard the backlog gets ranked against.


Purpose

Pmacs already has an unusually strong technical foundation for an editor at its stage of development. Its daemon/frontend split, semantic rendering protocol, CRDT-based editing, structured worker runtime, Lua programmability, language tooling, package resolver, terminal support, and remote-capable architecture all point toward a system with genuine long-term differentiation.

The next challenge is not primarily adding more isolated capabilities. It is making the existing and planned capabilities converge into a coherent product.

Visual Studio Code is used throughout this document as a reference point because it is an exceptionally successful modern editor. Pmacs is obviously not trying to become VS Code. Its goals are substantially different: live programmability, inspectability, stronger concurrency semantics, frontend plurality, and deeper user control are central to Pmacs in ways they are not central to VS Code. The useful lesson is therefore not to copy VS Code's interface or architecture wholesale, but to understand how a technically complex system can become immediately useful, progressively discoverable, and easy to adopt.

The deeper reference point is Emacs. Emacs's beauty comes from its ontological unity: the editor is text, Lisp, commands, buffers, and a running system that the user can interrogate and change. Its enduring achievement is not any single feature, but that it created the kind of environment in which generations of users could build almost anything.

Pmacs should preserve that unity while correcting the accidental historical constraints beneath it: cooperative rather than general parallelism, unclear ownership, global mutation, difficult unloading, rendering coupled too closely to the core, opaque latency, implicit remote context, and inconsistent package lifecycle.

Pmacs does not need to contain everything Emacs contains before it can be considered a successor. It must instead remain the kind of system in which everything Emacs contains could eventually be built — with clearer ownership, stronger concurrency, richer frontends, explicit execution locations, and fewer historical traps.

That places the VS Code comparison in its proper role. VS Code demonstrates how a complex development environment can be coherent, approachable, and immediately useful. Emacs demonstrates how an editor can become a live, fertile, user-transformable world. Pmacs should combine the adoption discipline of the former with the programmability and unity of the latter.

The core product objective should be:

Pmacs should be immediately excellent, progressively understandable, completely inspectable, and ultimately replaceable.

A user should receive a polished workstation before they become an editor engineer. If they choose to become one, the entire system should remain open to them.


0. Scorecard (audited 2026-07-25)

§ Concern Grade One-line state
2 Golden product journey Broken at entry pmacs . exits 1; only "launch" and "edit" pass cleanly zero-config
3 Zero-configuration state Partial Defaults genuinely strong; missing-tool failure is silent, not graceful
4 Progressive disclosure Inverted The advanced level is real; the beginner level is the missing one
5 Unified discoverability Substrate without surface Best-in-class registration metadata; almost no way for a user to reach it
6 Interaction islands Weak, and growing Six hardcoded key-interception shadows; no transient-keymap mechanism exists
7 First-class workspaces Missing (conventions only) Marker walk + four independent consumers; no workspace object
8 Execution locations Missing (architecture ready) SSH attach works; "location" is not a value anywhere
9 Worker ownership Mechanism without identity Cancellation solid; no owner/purpose/hierarchy; four disjoint activity views
10 Extension trust classes Missing (one class) Shared Lua state, __index = _G; MCP is the one out-of-process seam
11 Config layering + provenance Partial (foundation only) Typed registry is right; 5 settings live in it; no value provenance
12 Profiles Missing One hardcoded default keymap; not a named concept
13 Package lifecycle UX Resolution without lifecycle Mature resolver/lockfile; init-only install; no uninstall/disable/search
14 Workbench primitives Partial (best trajectory) Listview is a real shared primitive; bottom panel landed (#155)
15 Contextual affordances Weak Right-click menu only; code actions apply first-blindly; no git integration at all
16 Semantic frontend Strong v6..=v20 negotiated protocol; degradation practiced; TUI/GPU share the model
17 Distribution Missing CI is test-only; no binaries, channels, checksums, or update path
18 Onboarding Missing No welcome, no tutorial; C-h deletes a word; M-x is the only door in
19 Coherence acceptance tests Missing (culture ready) Superb per-arc acceptance discipline; zero cross-subsystem journey tests

Three cross-cutting patterns explain most of the table; they are detailed in §1.1§1.3: substrate without surface, the silence asymmetry, and per-arc coherence debt.

Coherence-shaped work already in flight at audit time: find-file / dired Stage 0 (C-x C-f, PR #162, docs/dired-framing.md), bottom panel Stage 1 (merged #155), multi-root LSP affinity (PR #161), the config registry foundation (merged #127).


1. The Product Problem

Pmacs is building many difficult things correctly and in parallel. That is appropriate for an early systems project. The risk is that the project succeeds architecturally while remaining fragmented experientially.

A technically sophisticated editor can still feel incoherent when:

  • installation requires repository knowledge;
  • capabilities exist but are difficult to discover;
  • subsystems expose unrelated interaction conventions;
  • project, process, terminal, language-server, and remote state are modeled separately;
  • configuration is powerful but provenance is unclear;
  • packages can extend the editor but cannot be understood, controlled, or attributed;
  • background work is concurrent but not meaningfully owned;
  • new users must configure the system before they can experience its strengths.

The relevant distinction is between capability completeness and product coherence. Capability completeness asks "can pmacs do X?". Product coherence asks whether a user naturally encounters X at the right time, whether X behaves by shared conventions, whether the user can understand why X is active, and whether X feels like part of one editor rather than an adjacent demonstration.

Pmacs is well on its way toward capability completeness in several major areas. Product coherence must now become an explicit development track rather than an emergent consequence of subsystem work. The 2026-07-25 audit found that every one of the eight bullet points above is true of pmacs today, and that they share three structural causes.

1.1 Ground truth: substrate without surface

The single most consistent audit finding, appearing independently in all four lanes: the mechanism layer is disciplined, often best-in-class; the product surface that would make it perceptible is missing. The July 2026 roadmap named an instance of this "dark matter — built but unwired" and treated it as a one-time backlog. It is not one-time; it is the project's default failure mode. The audited inventory of complete, working, unreachable capability:

  • The entire rich help system. src/help.rs implements a self-navigable *help* buffer with [command:] / [key:] / [mode:] / [hook:] / [buffer:] / [view:] cross-reference links and follow_link_at, installed as pmacs.help.show_command / show_key / show_buffer / show_mode / show_hook / show_view (install_help_module, src/lua_bindings/mod.rs:5597). grep -rn "pmacs.help" builtin/ returns zero hits — no command, no keybinding, no caller.
  • File-name completion. CompletionSource::Files { root } (src/minibuffer.rs:589) is reachable from Lua as source = "files"
    • source_root — zero builtin callers.
  • Command availability. Command.predicate is stored on every command and never evaluated by invoke, invoke_interactive, keymap dispatch, M-x filtering, or the menu (§5).
  • Package ownership. CurrentlyLoadingPackage is a stack correctly pushed/popped around every package chunk (src/lua_bindings/mod.rs:3953-3966) and consulted by exactly one binding (on_unload's fallback). Every registrar ignores it (§13).
  • LSP health. LspManager::status_buffer_text() (src/lsp.rs:1204) is Lua-bound; no builtin command opens *lsp* (§2, §9).
  • Interactive file opening. pmacs.buffer.find_or_open (src/lua_bindings/mod.rs:3103) had no interactive caller at audit time; a complete 1,384-line dired exists as a frozen test fixture (tests/fixtures/pmacs-dired/init.lua). Being fixed now: dired Stage 0 (PR #162).

The strategic consequence: most coherence gaps in pmacs are doors, not engines — deliberately deferred surface, not design error. That is the cheap kind of gap, and it should change how the remaining work is costed.

1.2 Ground truth: the silence asymmetry

Synchronous, user-initiated failures report well: M-x errors surface as "M-x error: <first line>" (builtin/commands/default.lua:633-641), compile spawn failures print in-buffer and on the status line (builtin/runtime/compile.lua:850-855), and pmacs --gpu with no pmacs-gpu binary produces the best missing-tool message in the codebase — it names both the sibling path it tried and the PATH fallback (src/main.rs:367-379).

Automatic, background failures are swallowed. The canonical case, hit on every file open when a language server is preconfigured but not installed: Command::spawn ENOENT propagates up through LspManager::spawn and raises in Lua — where ensure_server pcalls it and returns nil (builtin/runtime/lsp.lua:614-626), and the buffer.after-load hook pcalls the whole attach (builtin/runtime/lsp.lua:895-897). Net user-visible result: nothing. No status message, no *errors* entry, no modeline marker (the LSP segment is gated on an attachment record existing, so absence is indistinguishable from "unsupported file type"). Working tree-sitter highlighting actively masks the failure — the user sees colored text and assumes language intelligence is on. Post-crash is the same shape: LspEventKind::Crashed is pushed (src/lsp.rs:2394) and no builtin subscriber surfaces it.

This directly contradicts the product thesis (§23): the "without freezing" half is delivered; the "without becoming opaque" half is currently false for exactly the failures a new user will hit first.

The reporting channel the runtime believes it has does not exist. Fifteen call sites — async.lua (5), syntax.lua (4), and one each in lsp.lua, mcp.lua, fs.lua, editops.lua, autosave.lua, and commands/default.lua — report background failures through pmacs.error, each guarded as if pmacs.error then pmacs.error(...). pmacs.error is never defined in production — the only assignment in the tree is a test stub (src/editor.rs:9881), and type(pmacs.error) is nil in a fresh EditorState. So every one of those fifteen reports is dead: the guard makes the silence look deliberate and keeps it from ever being noticed. pmacs.errors (plural, builtin/runtime/compile.lua:45) is an unrelated namespace and is not it. This is the silence asymmetry one level deeper than §1.2 first recorded — not "the failure isn't surfaced" but "the surface was written, guarded, and never built." Found while landing PR #161, which nearly added a sixteenth; that one reports via pmacs.editor.set_status (which exists) with the pmacs.error arm riding along for when the channel is built.

Rule to adopt: anything that fails automatically must leave a user-visible trace with a named owner. A pcall around background wiring must log attributed failure, never discard it. Corollary from the above: report through a channel with a test that observes it, or the guard is indistinguishable from the silence it was meant to fix.

Frequency note (PR #161): per-root server affinity means the preconfigured-but-missing-server failure now fires once per project root rather than once per language per session. The silence is unchanged in kind; it is strictly more frequent. Surfacing it stays Priority 1 work with its own framing — it is a user-visible product behavior (what message, where, with what guidance), not a substrate fix to smuggle into an affinity PR.

1.3 Ground truth: coherence debt compounds per-arc

Three audited growth patterns show subsystem work accruing coherence debt with no counter-pressure:

  • Each new modal UI extended the shadow family instead of building the keymap-layer mechanism (menu → completion → query-replace, §6) — and each addition must hand-sync three guard lists (dispatch_key, dispatch_idle_for, dispatch_paste).
  • Each new subsystem added its own activity view (*workers*, pmacs.process.list, *lsp*, the terminal-private id set, §9), because no common identity key exists to join them.
  • Each new option individually decides whether to adopt the config registry; five have, everything else has not (§11).

The framing-doc workflow (scout → framing → approval → acceptance criteria → bite-verified review) is exactly the right tool to reverse this — no framing has ever carried a product-coherence acceptance criterion. Adding them is a process change, not an engineering arc, and it is what makes this document required rather than advisory.


2. The Golden Product Journey

Pmacs should maintain one protected end-to-end experience against which all major work is tested:

  1. Install Pmacs.
  2. Launch it without prior configuration.
  3. Open a real project.
  4. Understand the visible interface.
  5. Edit immediately.
  6. Receive language intelligence.
  7. Find a symbol or file.
  8. Open a terminal.
  9. Build or test the project.
  10. Inspect and act on an error.
  11. Understand what background work is running.
  12. Close and later restore the workspace.

This does not need to exercise every advanced feature. It exists to prove that the editor's components form a usable whole. A strong initial target is a Rust project, because Rust stresses many of pmacs's intended strengths: project detection, toolchain discovery, language-server lifecycle, async diagnostics, build/test integration, terminal use, large compilation workloads, symbol search, background indexing, structured error presentation.

Install Pmacs
    ↓
Run `pmacs .`
    ↓
Project root detected
    ↓
Rust mode activated
    ↓
rust-analyzer found or installation guidance shown
    ↓
Files, diagnostics, terminal, and project actions available
    ↓
Build or test command discoverable
    ↓
Errors become navigable structured results

This journey should become a release gate. New architectural work should be evaluated partly by whether it improves, preserves, or complicates the journey.

Ground truth: the journey today

Grade: broken at step 3. Verified empirically at audit time:

$ ./target/release/pmacs .
pmacs: Is a directory (os error 21)
EXIT=1

The literal first arrow of the diagram above fails. load_file (src/file_io.rs:81-87) does File::open (succeeds on a directory) then read_to_end → EISDIR, which is not NotFound, so EditorState::open returns Err and main prints and exits (src/main.rs:411-414). Multiple file arguments are also rejected ("multiple files not yet supported", src/main.rs:227). Everything from step 6 onward is gated on a file being open, and the only zero-config way to open one is naming it on the command line — which requires already knowing the path.

Full verdict table:

# Step Verdict Evidence
1 Install Partial Source build only: cargo build --release --workspace --features pmacs/crdt (README.md). No binaries, no packaging. Runtime deps (/bin/sh, git, tar, coreutils) documented, never checked at runtime
2 Launch unconfigured Works EditorState::new() → empty *scratch*; missing config is not an error (src/config.rs:7-9); recentf/saveplace/autosave default-on
3 Open real project Missing pmacs . exits 1 (above). No directory handling anywhere
4 Understand interface Partial Mode line gives name/modified/L:C/scroll + mode/LSP/terminal segments; but no welcome text (EditorCore::new sets status: String::new()), no cheat sheet, and C-h deletes a word (§18)
5 Edit Works Full CUA + Emacs keymap in 161 lines (builtin/keymaps/default.lua); isearch, query-replace, kill ring, undo/redo, auto-indent/pair/comment, atomic save. Genuinely excellent zero-config
6 Language intelligence Partial Rust grammar bundled and auto-attaches; rust-analyzer preconfigured (builtin/runtime/lsp.lua:44-52) — but a missing binary fails silently (§1.2) and highlighting masks it. No LSP status command exists to diagnose
7 Find symbol / file File: missing → in flight (PR #162). Symbol: works but undiscoverable No find-file/dired/picker existed at audit; M-./M-?/C-c o bound but advertised nowhere and server-gated; no workspace-symbol command; pmacs.index.* has no UI
8 Open terminal Works but undiscoverable Full PTY with scrollback + modeline segment — reachable only as M-x terminal, no keybinding
9 Build / test Partial M-x compile.run works, defaults cwd to detected project root, parses Rust --> errors — but no keybinding, an empty first prompt (initial = last and last.cmdline or "", builtin/runtime/compile.lua:1134-1138), and no cargo build/cargo test suggestion despite ProjectKind::Cargo existing (src/project.rs:77)
10 Inspect error Partial (good once reached) E:n W:n modeline counts, underlines, M-g n/p + C-x ` walking a unified compile/grep/diag source, message echo, RET visits. Gated entirely on step 6 or 9 succeeding first
11 See background work Works but undiscoverable *workers* view via M-x editor.list-workers; C-c C-k cancel-at-point. No keybinding, no statusline spinner/progress indicator anywhere (§9)
12 Close + restore Partial Per-file cursor+scroll (saveplace), recent files, minibuffer history, autosave recovery all restore zero-config. Open-buffer set and window layout do not: desktop-save is opt-in (pmacs.session.desktop_mode(true)) and a documented no-op under a daemon (src/desktop.rs:323-326, :353-356, Q#DS9)

A journey observation worth keeping verbatim from the audit: keybinding coverage is inverted relative to frequencyC-c @ C-M-s opens all folds, while opening a file, opening a terminal, and running a build have no bindings at all.


3. A Strong Zero-Configuration State

Pmacs should not require configuration before it becomes pleasant. The default experience should demonstrate the editor's thesis: responsive editing, visible asynchronous work, coherent project awareness, language intelligence, integrated terminal and task execution, helpful diagnostics, discoverable commands, graceful failure when external tools are absent.

Configuration should be an escalation path:

  1. The editor works.
  2. The user notices a preference.
  3. The relevant setting or command is easy to find.
  4. The user changes it.
  5. The editor explains where the effective value came from.
  6. Advanced users can replace the behavior entirely.

The graphical frontend should have a deliberate default workspace with a restrained number of visible regions: main editor area; compact statusline; optional project/files surface; bottom panel for terminal, build output, diagnostics, and other transient tools; command palette; contextual actions; unobtrusive background activity indicator. The TUI should express the same conceptual model within terminal constraints. The goal is not identical geometry across frontends — it is shared nouns, commands, lifecycle, and state.

Ground truth

Grade: partial — the defaults half is strong, the graceful-failure half fails.

What already works with zero configuration, and is a real asset:

  • Missing config is not an error by contract (src/config.rs:7-9); no config directory is created or required; a broken init.lua does not block startup — the error lands in *errors* and the status line (src/config.rs:10-13).
  • Default-on persistence: recentf (builtin/runtime/recentf.lua, cap 50, C-x C-r), saveplace (builtin/runtime/saveplace.lua, restores cursor + view on after-load), autosave every 30 s with next-session recovery (builtin/runtime/autosave.lua:24), per-bucket minibuffer history. State root: PMACS_STATE_HOME$XDG_STATE_HOME/pmacs~/.local/state/pmacs (user_state_dir, src/state.rs:54-70), wired only in real entry points (install_state_dirs) so tests stay hermetic.
  • Atomic saves, full editing surface, bundled grammars for every preconfigured LSP language.

What fails the escalation path:

  • Step 3 ("easy to find") fails for both settings and commands (§5).
  • Step 5 ("explains where the value came from") is unanswerable today: config overrides are stored as bare values with no source (§11).
  • "Graceful failure when external tools are absent" is the silence asymmetry (§1.2). The --gpu message (src/main.rs:367-379) is the pattern to replicate; LSP auto-attach is the anti-pattern.

4. Progressive Disclosure

Pmacs should support several levels of use without requiring users to inhabit the most advanced one. These levels should be different presentations of the same underlying objects — a command selected from a context menu, invoked through M-x, bound to a key, called from Lua, or triggered by an agent should be the same command object.

Ground truth

Grade: inverted. The advanced level is largely real; the beginner level is the one missing. Audited level-by-level:

Beginner (should see: files, buffers, search, diagnostics, terminal, build actions, menus, missing-tool guidance):

  • files ✗ (no find-file at audit; PR #162 in flight) · buffers ✓ (C-x b, *buffer-list*) · search ✓ (C-s/C-r/C-M-s; project.search is M-x-only) · diagnostics ✓ once a server runs · terminal ✓ but M-x-only · build ✓ but M-x-only with empty prompt · menus △ (right-click only, 11 items) · missing-tool guidance ✗ (§1.2).

Intermediate (should discover: palette, keybinding search, workspace settings, profiles, package management, task definitions, frontend/language settings):

  • palette △ (M-x fuzzy over bare names, §5) · keybinding search ✗ (no list-keybindings/where-is commands) · workspace settings ✗ (no workspace scope, §11) · profiles ✗ (§12) · package management ✗ in-session (§13) · task definitions ✗ · frontend customization △ (themes, pmacs.gpu.set_font, statusline providers — all Lua-only) · language settings △ (raw Lua tables, outside the registry).

Advanced (should be able to: inspect implementations, redefine live, create packages, new views, providers, keymap layers, workspace policy, orchestrate workers, replace interaction models):

  • inspect ✓ (SourceLocation on everything; no jump-to-source command though) · redefine live ✓ (unregister + define) · packages ✓ (authoring is real, §13) · new views ✓ (listview is Lua-usable) · providers ✓ (statusline; completion/minibuffer sources are a fixed Rust vocabulary) · keymap layers ✗ (§6 — the mechanism does not exist) · workspace policy ✗ · orchestrate workers △ (pmacs.workers.register funnels into builtin dispatchers, §9) · replace interaction models ✗ (the shadows, §6).

The "same command object" principle largely holds where surfaces exist — menu items, keybindings, and M-x all resolve command names into the one registry — with one caveat: menu items are a parallel registry of labels whose command references are unvalidated (§5).


5. Unify Discoverability

Pmacs already has the beginnings of a strong command registry. This should become the center of a broader discovery model. Every meaningful action should eventually expose: stable symbolic identity, title, description, category, aliases, current keybindings, provenance, applicability predicate (with an explanation when unavailable), argument schema, destructive/asynchronous/reversible flags, locality, related commands and settings, and source location. Settings should expose name, type, description, default, effective value, provenance, scope, validation rules, listeners, related commands. Packages and workers should expose the analogous sets (§13, §9).

This suggests a general pmacs principle:

Anything that can affect the user should be discoverable as a structured object with identity, provenance, ownership, and lifecycle.

Ground truth

Grade: substrate without surface — the sharpest instance of §1.1.

What the substrate already has (genuinely strong):

  • Command (src/command.rs:66-79) = { name, description, source, body, predicate }. Description is mandatory and validated (R42); duplicate names are a hard error, not an overwrite; SourceLocation { file, line } is auto-captured from Lua debug info on every command, hook, menu item, config definition, config listener, and keybinding — the user cannot forge it. ~147 pmacs.command.define sites across builtin/.
  • ConfigDefinition (src/config_registry.rs:396-410) is richer than Command: name, mandatory description, ConfigKind (Boolean/Integer/Number/String/Enum with bounds, choices, allow_empty), default, Live/StartupOnly mutability, source. pmacs.config.list() returns full descriptor tables.
  • Reverse keybinding lookup exists as data: KeymapStack::iter_all() (src/keymap_stack.rs:295-311) enumerates every binding; pmacs.describe.command(name).key_bindings computes where-is on demand.
  • pmacs.describe.* (src/lua_bindings/mod.rs:6042-6162) returns structured tables for command/key/buffer/view/mode/hook, and describe.key resolves against the active buffer + major mode.
  • M-x matching is fuzzy (case-insensitive subsequence with boundary/consecutive bonuses, fuzzy_score, src/minibuffer.rs:637-666).

What is missing, itemized:

  • Command has no title, no category, no aliases, no argument schema, no destructive/async/reversible flags. The dotted-name prefix (buffer., lsp.) is convention, not data. MCP tooling works around the missing schema by stuffing rendered JSON schema text into the description string.
  • Command.predicate is dead metadata. It is read in exactly two places (a literal line in the unreachable help renderer, and a test) and never evaluated by invoke, invoke_interactive, dispatch, M-x filtering, or the menu. The doc comment's claim that "the command palette (T M2.7) uses it to gray out unavailable entries" describes something that never shipped.
  • M-x shows bare name strings. CompletionSource::Commands returns Vec<String> of names; the wire type MinibufferPrompt.candidates is Vec<String> (pmacs-protocol/src/message.rs:994-1006). No description, no keybinding, no category alongside candidates — while CompletionPopupRow (:1231) already carries kind and detail, proving richer rows are a solved wire problem in this codebase.
  • The entire Rust help layer is orphaned (§1.1). Consequence: two parallel *help* implementations exist — help.rs's cross-referenced renderer and the Lua show_help_text in builtin/commands/default.lua:1103-1136 — and the one users can actually reach (M-x editor.describe-command) renders less than the unreachable one (no source, no scope, no predicate note).
  • Missing as commands entirely: describe-key, describe-mode, describe-hook, describe-buffer, where-is, list-commands, list-settings, list-keybindings, apropos. What exists: editor.describe-command, editor.describe-setting, editor.describe-instance[-buffer], editor.list-buffers, editor.list-workers. M-x describe-setting prompts free-text with no completion source (deliberately skipped — builtin/commands/default.lua:1180-1185); a typo yields a status line error.
  • No help prefix key. C-h is buffer.delete-word-backward (builtin/keymaps/default.lua:86, with a comment noting the key "was free"). No F1, no C-h k/f/b.
  • Settings value provenance is absent. Overrides are stored as bare values (global: HashMap<String, ConfigValue>, src/config_registry.rs:693-708); describe-setting's "Source:" is the definition site. "Why is this setting 4 and who set it?" is unanswerable (§11).
  • Menu items are a parallel registry. MenuItem (src/menu.rs:56-78) carries its own hand-written label duplicating the command's description, with a lazily-resolved command name string that is never validated to exist — a typo'd item silently does nothing when clicked. The wire row is label + separator only (MenuPromptRow): no key hints, no grayed state. Note the asymmetry: pmacs.menu.list reports has_predicate; pmacs.describe.command does not.
  • The two key-lookup APIs disagree. pmacs.keymap.lookup is global-only (it resolves with no buffer and no modes, src/lua_bindings/mod.rs:6294-6307) while pmacs.describe.key is context-aware. pmacs.keymap.list erases source and renders Scope::Buffer(id) as bare "buffer" (id erased), so full-fidelity enumeration requires per-command describe.command calls. There is no which-key-style prefix surface.

Shape of the fix: roughly (a) three metadata additions on Command (title, category, predicate actually evaluated + reported), (b) value provenance in the config registry, (c) a dozen interactive commands and richer M-x candidate rows over introspection that already exists. This is the highest payoff-per-effort concern in the document.


6. Eliminate Hardcoded Interaction Islands

Pmacs's public programmability story will be strongest when all major interaction layers pass through ordinary registries and extension points. Temporary or modal interfaces — incremental search, query replace, minibuffer prompts, completion menus, context menus, transient selectors — should eventually use inspectable keymap layers rather than special Rust-level interception. A general transient keymap model includes priority, activation condition, owner, lifetime, fallback behavior, discoverability, help labels, and cancellation behavior.

Ground truth

Grade: weak, and growing by one island per modal feature.

Everything funnels through one function: EditorInstance::dispatch_key (src/editor.rs:901), a single input-precedence state machine (its own #[allow(too_many_lines)] says as much). The audited precedence order:

# Surface Guard site Decoder Kind
0 popup-vs-modal auto-close editor.rs:917-925 pre-step
1 Context menu editor.rs:933 MenuKey::from_chord (editor.rs:3005) full shadow
2 isearch editor.rs:939 SearchKey::from_chord (editor.rs:2902) full shadow
3 query-replace editor.rs:945 QueryReplaceKey::from_chord (editor.rs:2967) full shadow
4 Minibuffer editor.rs:951 MinibufferAction::from_chord (src/minibuffer.rs:468) full shadow
5 Completion popup editor.rs:958-971 CompletionPopupKey::from_chord (editor.rs:3056) partial shadow (control chords only; skipped while a multi-key prefix is pending)
6 Terminal transport + C-c escape editor.rs:973-1010 is_terminal_escape_chord (editor.rs:4355) partial, transport-level
7 Ordinary dispatch editor.rs:1018-1032 KeymapStack::resolve the only inspectable layer

Facts that define the gap:

  • Full shadows eat every key, including unrecognized ones (each decoder has an Ignore/Dismiss fallback arm). While a terminal buffer is focused and unescaped, all keys encode to the child — C-c-leading user bindings are structurally unreachable in a terminal buffer.
  • No transient-keymap mechanism exists to migrate to. KeymapStack has exactly three fixed scopes — Buffer(BufferId), Mode(String), Global (src/keymap_stack.rs:37-44); resolution order buffer → mode → global with cooperative prefix-pending across scopes (resolve, keymap_stack.rs:235-291). No layer stack, no push/pop, no priority, no lifetime. The Lua scope accept-list hard-rejects anything else. So this is not "migrate the shadows to the layer system" — the layer system must be built first. (active_modes is also at most one mode today; minor modes are unbuilt.)
  • describe-key lies while a shadow is active. With the completion popup open, describe-key C-n reports cursor.down @global; the literal arm 'n' => Some(Self::Next) fires instead. Introspection has zero awareness of the shadows; Lua can observe only a boolean per surface (popup_visible, search_active, query_replace_active, minibuffer-active).
  • This is deliberate and documented — rationale R51 (docs/keybindings.md, src/minibuffer.rs:470): the shadows are intentionally not user-configurable. The completion framing considered and rejected buffer-local binds on teardown-lifecycle grounds (docs/in-buffer-completion-framing.md:93-105) — the objection was a leaked binding outliving its session, which is an argument for a lifetime-owning layer handle, not against layers.
  • Three hand-synced guard lists must be updated per shadow: dispatch_key, dispatch_idle_for (editor.rs:791 — deliberately omits the partial popup shadow; load-bearing for CRDT frontends' optimistic-apply correctness), and dispatch_paste (editor.rs:1129-1140).
  • Off-path hardcodes: client-side F12 detach (is_detach_key, src/attach.rs:997-1006) and the GPU optimistic key classifier (crate::optimistic::classify_key) — the latter is classification, not routing, and is kept honest by dispatch_idle_for.

The counter-example that proves the idiom: the entire picker/panel family — listview (references, outline), project-search, buffer-list, compile-mode, REPL, terminal scroll commands — uses ordinary buffer-local keymaps via pmacs.keymap.bind { scope = "buffer" } (builtin/runtime/listview.lua:76-88 and siblings). These are inspectable, correctly reported by describe-key, and rebindable from init.lua. Roughly half the transient UI already lives on the right side of the line.

The concrete missing primitive is small and well-scoped: a transient overlay consulted before buffer scope (a Scope::Transient or an overlay Vec<Keymap>), with (a) push/pop tied to session lifetime via a lifetime-owning handle (RAII on the Rust side), (b) a full-shadow vs partial-shadow flag (isearch eats everything and falls back to search-self-insert; the popup intercepts eight chords and falls through), and (c) dispatch_idle_for derived from the stack ("any active layer is full-shadow") instead of hand-maintained. With that, the six ladder rungs collapse into "session pushes a layer on open, pops on close," and describe-key becomes truthful for free.


7. First-Class Workspaces

Project-root detection is useful, but pmacs needs a richer workspace object. A project answers "which root contains this file?"; a workspace answers "which persistent development environment owns this set of activity?" A workspace should eventually own:

Workspace
├── identity
├── one or more roots
├── execution location
├── environment and toolchain
├── configuration layers
├── trust policy
├── enabled packages
├── language-server instances
├── indexes
├── terminals and processes
├── tasks
├── debugger sessions
├── open buffers and views
├── frontend layout state
└── persistence and restoration policy

This matters for multi-root language servers, monorepos, generated files, remote projects, containers, HPC environments, per-project packages, task ownership, session restoration, and project-specific trust. The workspace should be a core runtime entity, not an informal convention shared across unrelated subsystems.

Ground truth

Grade: missing — what exists is a marker walk plus four independent per-subsystem conventions.

  • Detection: src/project.rsdefault_markers() is Cargo.toml, go.mod, package.json, .git (directory), with the deliberate rule that language markers outrank .git at the same ancestor level; upward walk, innermost wins; set_search_boundary honored; ProjectKind (e.g. Cargo) exists and is consumed by nothing user-facing.
  • Four independent consumers, each resolving on its own: LSP root (project_root_for, builtin/runtime/lsp.lua:554-570: config override → marker walk → file's own directory, returning root, source where source ∈ config/detected/fallback); compile cwd (project_root_of_active, builtin/runtime/compile.lua:600-608); project-search root (resolve_search_root, builtin/commands/default.lua:843-857, falls back to "."); the project symbol index (.pmacs/index.json, src/project_index.rs).
  • There is no "current project" independent of the active buffer's path. With only *scratch* open, every consumer above returns nil/".". Nothing owns the set {roots, servers, terminals, tasks, layout} — which is why desktop-save under a daemon had nothing principled to attach to (Q#DS9, §2 step 12).
  • First slice landed (PR #161): the multi-root LSP server-affinity work makes (language, found-root) the server identity — the first time a root functions as an identity key rather than a spawn parameter. It also establishes the rule that a fallback root (the file's own directory, when no marker was found) is deliberately not an identity, so markerless files keep sharing one server per language. Note it is again per-subsystem: LSP learns roots; compile, search, index, and trust do not share the object.

A workspace entity is a model gap (real arc), not wiring. It is also the prerequisite that keeps §8 (locations), §9 (task ownership), §11 (workspace config scope), and step 12 of the journey from each inventing their own ownership story.


8. First-Class Execution Locations

Pmacs's daemon/frontend architecture gives it an excellent basis for remote development. The next step is to model execution location explicitly — a value that can be inspected and assigned, not an implementation detail hidden inside file access or process spawning:

Location
├── local
├── ssh://host
├── container://name
├── slurm://allocation
├── daemon://session
└── custom provider

Filesystem roots, processes, terminals, language servers, workers, debuggers, indexers, package services, and build/test tasks should all carry a location. That makes answerable: where is this server running? where will this build execute? is this terminal local? can this worker migrate? what happens if the remote daemon disconnects?

Ground truth

Grade: missing as a model; the architecture half already works.

What exists: pmacs --attach user@host (remote TUI over SSH), ssh:user@host/instance / local:/path.sock addressing, mosh-modeled reconnect-on-drop, and the daemon/frontend split itself — i.e. daemon://session exists implicitly and robustly. What does not exist: any Location value. Every ProcessSpec spawn, LSP server, terminal PTY, and worker is implicitly daemon-local; no resource carries a location field; nothing can be asked "where is this running?". No container/slurm/provider concept anywhere.

This concern is deliberately after §7 in dependency order: a location without a workspace to scope it has nothing to attach to. For the research/HPC ambition (§12's Research Workstation profile), this pair is the long-lead differentiator — nothing else in the editor market models it well.


9. Extend the Worker Model into Structured Concurrency

Pmacs's worker system is one of its most distinctive strengths. Cancellation, supersession, streaming, frame-aware draining, and the *workers* view provide a strong basis. The next step is ownership and hierarchy: every substantial task should have an owner, a workspace, an optional buffer/view, a parent, children, a latency class, a cancellation scope, a resource budget, an execution location, progress, and failure attribution.

Workspace: pmacs
└── Command: project-build
    ├── Task: save-dirty-buffers
    ├── Task: cargo-check
    │   ├── Process: cargo
    │   └── Stream: compiler-diagnostics
    └── Task: refresh-diagnostics

Cancelling project-build should cancel its children. Closing a workspace should terminate or detach workspace-owned work. Reloading a package should stop package-owned tasks. The activity view should answer what is running, why, who owns it, where, what depends on it, and what cancellation will affect. That turns parallelism into a product feature rather than an implementation claim.

Ground truth

Grade: mechanism without identity.

The mechanism layer is solid: cooperative per-job cancellation tokens with panic isolation (src/worker.rs:13-28); supersession with correct settle-time pruning (src/async_runtime.rs:688-701) — a genuinely good primitive; a completions ring (cap 64); register_external so non-pool work (LSP requests, MCP) appears uniformly; one shared ProcessSupervisor under everything (src/editor.rs:341); the *workers* view (src/workers_buffer.rs, opened by M-x editor.list-workers, auto-refreshing, C-c C-k cancel-at-point).

The identity layer is absent:

  • PendingJob (src/async_runtime.rs:365-392) carries {cancel, state, supersede_key, stream_buffer, max_batch, kind, dispatched_at}. No owner. No purpose string. No workspace/buffer association. No parent. The one buffer link that exists (parse job → buffer) lives in a SyntaxCoordinator side map, invisible to the workers view.
  • JobKind is a closed 12-variant enum (Sleep, ComputeSum, EmitN, Grep, Parse, FsReadDir, FsStat, FsRename, FsChmod, FsRemove, McpRequest, LspRequest). pmacs.workers.register funnels Lua jobs into existing Rust dispatchers, so every third-party job renders under a builtin's label.
  • Supersession is opt-in per dispatch site and underused: "search" (grep) and lsp:{method}:{sid}:{uri} use it; parse jobs and all MCP requests pass None — a fast typist stacks parse jobs.
  • Cancellation scopes: per-id and per-key only. No cancel-all, by-kind, by-buffer, by-owner, or by-subtree — there is no scope to range over.
  • Four disjoint activity planes with no join key:
Plane Surface What it misses
Async jobs *workers* processes, servers, terminals
OS processes pmacs.process.list (no buffer view exists) filters to LineOriented only — terminal PTYs are invisible; spawn_terminal bypasses the public path entirely
LSP servers *lsp* status text no builtin command opens it; LSP sets RestartPolicy::Never on the supervisor and runs its own restart logic
Terminals private id set drained after the supervisor tick user-visible in none of the above

A terminal PTY appears in no user-visible activity view. An LSP server appears in *lsp* (unreachable) and list(); its requests appear in *workers*; nothing joins them.

  • No progress indicator exists anywhere — no statusline spinner, no busy count (grep for progress/spinner/busy in src/statusline.rs is empty). "Visible asynchronous work" (§3) is currently false unless the user knows to run M-x editor.list-workers.
  • ProcessSpec.label is the nearest thing to attribution: caller- supplied, unvalidated convention (lsp:{name}, terminal buffer name).

The audit's conclusion, worth preserving verbatim: because identity is missing, scoped cancellation has nothing to scope over and a unified activity view has nothing to group by — the four views exist precisely because there is no common key to merge them on. Owner/purpose/parent fields on the job and process specs are the prerequisite; the unified view and the ownership tree fall out of them.


10. Define Extension Trust and Isolation Classes

Pmacs should preserve live, low-friction programmability — it should not force all extensions into rigid out-of-process APIs. At the same time, namespace isolation inside a shared Lua state is not enough for fault containment, security, latency containment, memory accounting, native-code isolation, reliable unloading, or project-local trust. Pmacs should define extension classes before the ecosystem becomes large:

  • 10.1 Trusted core packages — in-process, deep API access, distributed with pmacs or explicitly trusted.
  • 10.2 Normal Lua packages — shared/managed runtime, declared capabilities, owned registrations and workers, execution budgets, measurable latency, reloadable lifecycle, package-level error attribution.
  • 10.3 Isolated service extensions — separate process, typed RPC, crash recovery, resource accounting, explicit fs/process/network permissions.
  • 10.4 Project-local / untrusted — explicit approval, restricted capabilities, strong isolation, workspace-scoped trust, easy revocation.

Ground truth

Grade: missing — one class exists.

Every package today is a 10.1/10.2 hybrid with none of 10.2's machinery: in-process, per-package _ENV with __index = _G (namespace hygiene, not containment), full API access, no capability declarations, no budgets, no latency measurement, no owned-registration lifecycle (§13). The only containment primitive in the tree is the instruction-count hook that can cancel a hot-looping main-thread chunk (src/lua_isolation.rs:1-39) — a runaway guard, not an isolation class.

Two real assets to build on: the loader's exports gating (the package searcher is deliberately inserted at position 1 of package.searchers so exports are enforceable, src/lua_bindings/mod.rs:3891-3900), and MCP as the existing 10.3 seam — packages can already spawn MCP servers and consume their tools over a typed transport (docs/mcp-for-package-authors.md), which is exactly the separate-process/typed-RPC shape 10.3 asks for. Project-local trust (10.4) has a natural anchor once §7's workspace exists.

Sequencing note: 10.2's "owned registrations, reloadable lifecycle, error attribution" is the same work as §13's ownership gap — do it once, under one arc.


11. Configuration as Typed, Layered Data

Pmacs's typed configuration registry is the correct foundation. It should develop into a layered system with explicit provenance. Likely layers: built-in defaults; profile defaults; user settings; machine-local; remote-location; workspace; root/folder; language/mode; buffer-local; session overrides. A setting inspection view should show the full chain and the active source:

setting: editor.tab-width
effective value: 4
type: integer
scope: workspace

defined by:
  built-in default: 8
  Rust profile: 4
  user setting: 2
  workspace override: 4

active source:
  ~/src/pmacs/.pmacs/settings.lua

Pmacs should also preserve three distinct levels — settings (typed declarative data), behavioral customization (commands, hooks, keymaps, Lua), package construction (new capabilities) — so that users do not need executable Lua for ordinary preferences, while advanced users can still replace the mechanism.

Ground truth

Grade: partial — the foundation shipped (#127) and is correct; the layering, provenance, and adoption have not followed.

  • The registry is typed, described, duplicate-rejected, freeze-aware (StartupOnly), listener-bearing, and introspectable — see §5. Its design decisions (always-store overrides, explicit buffer, no ambient scope) are recorded in docs/config-registry-framing.md.
  • Two scopes exist of the ten layers listed above: global and buffer-local. Per-language and per-project are patterns (a hook calling set_local), not scopes. No profile, workspace, machine, or remote layer.
  • Value provenance is absent (§5): overrides are bare ConfigValues; describe-setting's "Source:" names where define() ran. The inspection view sketched above is currently impossible to render.
  • Adoption is five settings: editing.auto-pair (pair.lua), editing.trim-on-save (editops.lua), autosave.interval-ms (autosave.lua), window.panel-height + window.min-height (window.lua). Everything else a user might set — theme, fonts, LSP server config, killring size, recentf/saveplace/desktop enables, pair sets, comment strings, pmacs.parse.* — lives in raw Lua outside the registry and is therefore invisible to describe-setting and any future settings UI. The migration list is already written: docs/config-registry-framing.md "named deferrals" (table-valued settings are the hard prerequisite for LSP/pair/comment tables).
  • No persistence: settings changed at runtime do not survive restart (the custom-file split-brain question is a named deferral).
  • The three-level separation holds in principle today (registry / hooks+keymaps / packages), but with five settings registered, level 1 is effectively empty — users need executable Lua for nearly every ordinary preference, which is the exact failure the section warns about.

12. Profiles as Product-Level Bundles

Pmacs should offer a small number of official profiles bundling default keymaps, visible interface regions, package recommendations, settings, task conventions, discovery hints, and onboarding: Pmacs Standard (approachable graphical workstation), Emacs (familiar bindings, minibuffer-centered), Minimal, and later Research Workstation (terminals, remote machines, Slurm, proof assistants, long-running builds). Profiles must not create separate products — they exercise the same registries and primitives.

Ground truth

Grade: missing. Not a named concept anywhere in the tree. There is one hardcoded default: a single 161-line keymap (builtin/keymaps/default.lua) that is already a de-facto hybrid of the "Standard" and "Emacs" profiles (CUA selection + Emacs kill/yank/isearch chords). No profile object, no bundle format, no selection mechanism, no per-profile defaults layer (§11's missing profile scope is the same gap). Prerequisites: the config profile layer, and enough registry adoption that a profile has something declarative to set.


13. Package Experience, Not Merely Package Resolution

Pmacs already has serious package-resolution machinery. Product coherence requires a package lifecycle experience: search, installation, updates, disable, reload, uninstall, version inspection, dependency graph, compatibility warnings, capability declarations, ownership inspection, error history, active-worker inspection, resource use, trust state. Installation should work during a running session. Users should be able to install coherent capability bundles ("Rust Development") rather than individual packages. Marketplace sequencing: stable format → ownership/reload lifecycle → in-editor manager → curated registry → bundles → publisher identity → public marketplace.

Ground truth

Grade: resolution without lifecycle — the artifact layer is mature, the lifecycle layer assumes a single author iterating on their own package.

Mature (keep): pmacs.toml manifest (validated name, semver, pmacs_required, dependencies/conflicts, entry, exports); git-address installs (github:/gitlab:/URL; auth delegated to git config; no registry service); iterate-to-fixed-point resolver with deterministic ordering and honest unsatisfiability errors (documented no-backtracking tradeoff); merged SHA-256 lockfile; per-package _ENV; exports enforced by a position-1 searcher; bundled packages through the identical path.

The lifecycle facts:

Operation State
install / install_project / install_local / update exist, init.lua-onlyrequire_init_phase raises InitOnlyApi mid-session; the error text admits there is no CLI equivalent ("restart with an updated init.lua")
reload(name) works in-session and is well-built: unload hooks → loaded-table invalidation (name + name. prefixes) → env clear → re-require
installed() / describe(name) / load(name) / on_unload(fn) work in-session; describe returns manifest metadata only
uninstall / remove absent — the documented procedure is rm in a shell (src/packages/installer.rs:1178-1180)
disable / enable absent — no concept
search / list-available absent — no registry, no index; you must already know a git URL
inspect contributions absentdescribe cannot say which commands/hooks/keys/settings a package contributed; no *packages* view exists

Structural findings that any lifecycle arc must address:

  • The roster is in-memory per session, rebuilt from init.lua calls. A package on disk that init.lua doesn't install is invisible to require/installed(). And because do_install unconditionally runs the resolver, every startup runs git fetch --prune --tags per package before the idempotent fast-path can trigger — a first-launch latency and offline-use problem. (The Rust-side UpdatePolicy::Frozen that would fix offline installs is unreachable from Lua — dead code.)
  • Ownership is not tracked. SourceLocation is path attribution, not package attribution (for install_local the path may be the dev tree, not the install root); nothing indexes registrations by source; there is no "what did package X register" query and no bulk-unregister. The correct signal (CurrentlyLoadingPackage) already exists and is ignored by every registrar (§1.1).
  • The teardown surface is incomplete in a way that makes the documented convention unsatisfiable: pmacs.hook.remove does not exist (install_hook_module exposes define/add/list/run; HookRegistry has no removal method at all). A package that calls pmacs.hook.add leaks a callback on every reload, permanently. The package-author guide's hand-rolled OWNED = {} cleanup pattern (docs/package-author-guide.md:379-415) cannot be followed for hooks. (Independently rediscovered by the Lean 4 arc scout.)
  • Error attribution exists on exactly one code pathpackages.load wraps require and logs [package <name>] load failed to *errors*and nothing in builtin/ uses it. Plain require from init.lua attributes only by traceback; a failing install aborts the whole init.lua with no per-package isolation.
  • Install-root directory names are the manifest name's last segment, so same-basename packages collide on disk (knowingly accepted, installer.rs:44-50).

Sequencing: ownership + hook.remove + attribution is the same work as §10's class 10.2 and is the prerequisite for disable/uninstall/inspect; in-session install requires reworking the init-phase gate; search/ bundles/marketplace remain correctly last.


14. Coherent Workbench Primitives

Pmacs should resist implementing each subsystem with a custom UI vocabulary. It should provide a small set of reusable view primitives — editable text view, virtual list, tree, structured table, inspector, output channel, diagnostics collection, task/progress view, diff view, transient selector, contextual popup, side panel, bottom panel, help/documentation view — and packages should provide structured models to them. Git status, project files, symbol outlines, package dependencies, and worker trees should share one tree model with consistent selection, expansion, filtering, action discovery, mouse and keyboard behavior, persistence, and accessibility.

Ground truth

Grade: partial, with the best trajectory of any concern.

Primitive-by-primitive against the list above:

  • Editable text view ✓ — the buffer itself, everywhere.
  • List ✓ — listview is a real shared primitive, the strongest coherence asset in the UI layer: references, outline, buffer-list, and project-search all use it, with a shared buffer-local keymap idiom (RET/SPC visit, n/p, g refresh, q quit) that is inspectable and rebindable (§6's counter-example).
  • Output channel ✓ — the compile-mode *compilation* model (streamed, intercept-read-only, error-rule parsing), reused by grep and shell-command.
  • Diagnostics collection ✓ — DiagnosticStore + signs + unified error.next source.
  • Transient selector ✓ — the minibuffer (though its source vocabulary is fixed Rust-side).
  • Contextual popup ✓ — completion popup, context menu (each a shadow, §6).
  • Bottom/side panel ✓ — landed as bottom-panel Stage 1 (#155): WindowParams side/fixed_rows/dedicated, display = "current" | "panel" adopted by listview/compile/terminal, quit-action, divider drag. Stage 2 (GPU band) pending its own framing.
  • Task/progress view △ — *workers* exists but joins nothing (§9).
  • Help view △ — exists twice (§5); needs unification, not invention.
  • Tree ✗ — none. The named future consumers (project files, symbol hierarchy, package dependency graph, worker trees, git status) will each need it; building it once before dired's directory view and the workers tree harden their own conventions is exactly this section's point.
  • Structured table / inspector / diff view ✗ — none. (describe.* tables are the inspector's data model without a view; the wire-declared ResourceOffer family was reserved for diff/blame sources and remains unproduced.)

15. Contextual Affordances

Pmacs should remain excellent for keyboard-driven users while making capabilities visible to users who do not know their names: a diagnostic should offer code actions; a test definition run/debug; a Git change stage/revert/diff; a missing formatter configuration guidance; a symbol references/rename/definition/documentation; a remote workspace its location; a long-running task progress and cancellation. Affordances should invoke ordinary commands, never separate logic paths.

Ground truth

Grade: weak.

What exists: the right-click context menu — 11 items in 4 groups (edit/symbol/diagnostic/history, builtin/menus/default.lua:117-142), with a closed context vocabulary (always/selection/symbol/ diagnostic, src/menu.rs:44) and per-item predicates that are evaluated (unlike command predicates). It correctly invokes ordinary commands by name. Its limits: right-click only (no keyboard path in), no key hints on rows, invisible items filtered rather than grayed, and the unvalidated command references of §5.

What does not:

  • Code actions apply the first action blindly — no picker (a roadmap "dark matter" item still true at audit).
  • There is no Git integration at all — no status, stage, diff, blame, or gutter markers anywhere in the tree (gutter git riders and the ResourceOffer diff/blame family are named deferrals). The Git affordance list above has nothing to attach to yet.
  • No test run/debug affordances (DAP is a future arc, docs/dap-debugging-framing.md).
  • No missing-tool guidance affordances (§1.2 — the diagnostic that should say "rust-analyzer not found — install with rustup" says nothing).
  • No remote-location display (§8 — nothing carries a location).
  • Task progress/cancellation affordances exist only inside *workers* (§9); a long-running task shows nothing at the point of origin.

16. Productize the Semantic Frontend Architecture

The semantic protocol should be visible as a product advantage: native frontend rendering, frontend-specific typography, high-quality decorations, efficient incremental updates, accessible semantic information, multiple simultaneous frontends, stable remote attachment, frontend experimentation without reimplementing editor semantics. To preserve coherence: core commands frontend-neutral; stable semantic identities; explicit capability negotiation; graceful degradation; layout state separated from semantic state; no frontend becoming the de facto privileged implementation.

Ground truth

Grade: strong — the healthiest concern in this document, and most of its asks are already practiced.

  • Versioned, negotiated protocol SUPPORTED=[6..=20] with deliberate encoding-breaking bumps, both-frontends support required per bump, and byte-pin discipline for appended variants (handoff §4).
  • Two genuine frontends share the conceptual model; CRDT concurrent editing with presence across them; remote attach + reconnect.
  • Graceful per-frontend degradation is practiced, not aspirational: fold projection is per-frontend (FrontendView.fold_projection, selected from the negotiated semantic_render bit) so a grid frontend collapses folds while a simultaneous GPU session does not skip lines (#149/#148).
  • The GPU frontend exceeds the TUI (minimap, squiggles, typography) without the TUI losing the model — the "no privileged frontend" rule is holding under real divergence pressure.

Remaining, honestly small relative to the section's ambition: capability negotiation is per-bit rather than a first-class declared capability set; layout state vs semantic state separation is partial (window layout is daemon-side; desktop restore under a daemon is unresolved, §2 step 12); and the advantage is invisible as product because §17 means nobody outside the repo can try it.


17. Distribution Is Part of the Product

Pmacs should eventually be installable without repository familiarity: reproducible release builds, Linux and macOS binaries, checksums and signatures, stable and nightly channels, one-command update, rollback, protocol- and package-API compatibility reporting. First launch should create/locate config directories, explain the default profile, identify optional external tools, and let the user open a project immediately.

Ground truth

Grade: missing — zero release machinery exists.

.github/workflows/ contains exactly one workflow, ci.yml, and it is test-only (fmt/clippy/test matrix; the only release strings in it are cargo test --release flags). No release job, no artifact upload, no tags-to-binaries path, no checksums, no channels, no update or rollback mechanism. Installation is git clone + cargo build --release --workspace --features pmacs/crdt (README), which additionally requires knowing the feature-flag matrix (luajit vs lua54 × crdt). Runtime dependencies (/bin/sh, stty, git, tar) are documented in the README and never checked at runtime. First launch creates nothing and explains nothing (§18) — though by design it also requires nothing (§3), which is the right half to have.

This concern is independent of every other arc and can start anytime; until it does, every other coherence improvement is invisible outside the repository.


18. Onboarding

Pmacs needs onboarding that teaches concepts through use: open a project → command palette → find a file → terminal → inspect a diagnostic → view workers → change a setting → inspect where it came from → Lua REPL → redefine a command. That sequence communicates the whole thesis: already useful, discoverable, visible computation, explainable settings, programmable internals. It should be an ordinary, restartable help workspace, not a one-time modal wizard.

Ground truth

Grade: missing entirely.

No welcome buffer, no tutorial, no first-run detection, no cheat sheet reachable from inside the editor (docs/keybindings.md exists on disk only). C-h is buffer.delete-word-backward; there is no help prefix key and no F1. The sole discovery affordance is knowing to press M-x (builtin/keymaps/default.lua:141 — whose own header comment calls it the "command palette"). The empty *scratch* buffer that greets a new user says nothing (EditorCore::new sets an empty status).

Note the dependency: five of the ten onboarding steps above currently lead somewhere broken or invisible (find a file — in flight; inspect a diagnostic — silent-failure risk; view workers — undiscoverable; setting provenance — unanswerable). Onboarding is correctly sequenced after the P1/P4 fixes, but the cheap floor — a welcome buffer in *scratch* naming M-x, the keybinding cheat sheet as a help buffer, and a help prefix decision — has no prerequisites at all.


19. Product Coherence Acceptance Tests

Pmacs should add acceptance tests that exercise product behavior across subsystems, complementing (not replacing) subsystem tests:

  • Installation/first launch — no config, open a directory, usable workspace, actionable guidance for missing tools.
  • Command discovery — search by title and synonym; display keybinding, provenance, availability; invoke from palette and menu through the same object.
  • Workspace lifecycle — multi-root open, servers, terminal, build, close, restore, ownership cleanup.
  • Worker ownership — start completion/search/build, inspect, cancel a parent, confirm child cancellation and UI recovery.
  • Package lifecycle — install in-session, inspect contributions, disable, confirm disappearance, reload, uninstall cleanly.
  • Remote execution — attach to remote daemon, edit optimistically, remote terminal and server, disconnect/reconnect, coherent state.

Ground truth

Grade: missing — but the culture that would make them excellent is the project's strongest process asset.

Zero cross-subsystem journey tests exist. Every acceptance suite in the tree pins one subsystem's contract (superbly — bite-verified, falsified-by-revert, vacuity-checked). Several of the scenarios above are currently untestable because the behavior doesn't exist (install in-session, disable, open a directory); the ones that are testable (first launch, command discovery, worker cancellation, remote attach/reconnect) could be written today and would immediately pin the journey against regression. The first coherence acceptance suite should be the §2 journey itself, growing a step at a time as steps become real — that is how "the journey is a release gate" stops being aspirational.

(Related lesson already in the handoff: compile_mode_acceptance accidentally reads the real user config — an unintentional whole-product test that keeps catching real coherence bugs. That is evidence this class of test has teeth.)


Each priority is annotated with its audited state and whether the gap is wiring (surface over existing machinery — cheap) or model (a missing runtime entity — a real arc).

Priority 1: Protect the golden product journey

Establish the end-to-end workflow; treat regressions as release blockers. State: broken at step 3 (§2). Mostly wiring, and unusually cheap: directory-argument handling; a find-file surface (in flight, PR #162); surfacing the LSP spawn failure with guidance (§1.2); a compile keybinding + cargo build/test default from the existing ProjectKind::Cargo; a terminal keybinding; a welcome buffer. The journey acceptance suite (§19) is the ratchet that keeps it fixed.

Priority 2: Make workspace and location explicit

Otherwise project, LSP, remote, task, and persistence accumulate incompatible ownership models — the audit confirms four have already diverged (§7). State: missing; first slice in flight (multi-root LSP affinity). Model gap: the Workspace entity (§7), then Location values (§8). This is the long-lead arc; start it before the fifth and sixth subsystems grow their own root conventions.

Priority 3: Strengthen extension ownership and isolation

State: missing; prerequisite-shaped. Model gap, with one bug-sized prerequisite: pmacs.hook.remove does not exist (§13). The work unit: registrations carry their owning package (the CurrentlyLoadingPackage signal already exists), removal APIs complete the set, error attribution becomes default rather than opt-in. This single arc unblocks §13's disable/uninstall/inspect, §10's class 10.2, and package-scoped task cancellation in §9.

Priority 4: Unify discovery

State: substrate without surface. Almost pure wiring — the best payoff-per-effort in this document (§5): a dozen interactive commands over existing introspection, richer M-x rows (the wire pattern already exists), title/category on Command, predicate evaluation, help-layer unification, a help prefix key. Most of P1's "understand the interface" and §18's floor ride on this.

Priority 5: Finish the workbench convergence

State: partial and moving (§14) — bottom panel Stage 1 landed, GPU band pending; listview proven. Remaining: the tree primitive (build it before dired and the worker tree invent two), table/inspector/diff, help unification. Wiring plus one modest model piece (the tree model).

Priority 6: Productize configuration

State: foundation only (§11). Model-lite: value provenance in the registry, then layering (profile/workspace scopes — depends on P2 for workspace, §12 for profiles), then adoption migration (table-valued settings are the hard prerequisite), then persistence.

Priority 7: Build package lifecycle UX

State: not started; correctly sequenced after P3. In-session install (init-gate rework), disable/uninstall over P3's ownership, *packages* view over P5's primitives, then bundles and registry sequencing per §13.

Priority 8: Ship binaries and release channels

State: zero (§17). Independent of everything — can start anytime. The editor becomes testable by users who are not repository contributors; every other priority's value is invisible until this one exists.

How this maps to arcs

Candidate arc cuts, honoring one-feature-one-branch-one-PR and the framing workflow (each needs its own scout + framing before any implementation — this list is direction, not commitment):

  1. Journey Stage 1 (P1): directory open + compile defaults + LSP-failure surfacing + bindings + welcome buffer + the first journey acceptance suite. Rides alongside the in-flight dired arc.
  2. Discovery surface (P4): the describe/list/where-is command family, M-x rich rows, help unification, help prefix.
  3. Transient keymap layer (§6): the overlay scope + lifetime handle + derived dispatch_idle, then migrate shadows one per PR.
  4. Extension ownership (P3): hook.remove, owner-carrying registrations, attribution-by-default.
  5. Worker identity (§9): owner/purpose/parent on jobs and processes, join the four planes, statusline activity indicator.
  6. Workspace entity (P2): the object, then location values.
  7. Config provenance + adoption (P6).
  8. Package lifecycle (P7, after 4).
  9. Distribution (P8, anytime).

A standing process change accompanies all of them (§1.3): every new framing doc must state its coherence impact — which journey steps it touches, whether it adds an interaction island, whether its options enter the config registry, whether its background work is attributed — so the debt stops compounding silently.


21. What Pmacs Should Borrow

Proven adoption-cost reducers from successful modern editors, with audited status: immediate usefulness (△ — editing yes, journey no); strong defaults (✓ where they exist, §3); progressive disclosure (✗ inverted, §4); searchable commands (△ names-only, §5); integrated language tooling (✓ data layer / △ surface); project awareness (△ conventions, §7); visible contextual actions (△ §15); coherent task/terminal integration (✓ mechanics / ✗ visibility, §9); package discoverability (✗, §13); configuration layering (△ foundation, §11); remote development as core workflow (△ works, unmodeled, §8); smooth distribution and updates (✗, §17); consistent interface primitives (△ best trajectory, §14); explicit missing-tool guidance (✗ except --gpu, §1.2).


22. What Pmacs Should Preserve and Deepen

Pmacs should not trade away the qualities that justify its existence — and the audit confirms these are today's genuine strengths: live programmability (redefine/unregister at runtime, per-package envs); implementation inspectability (SourceLocation on every registration, mandatory descriptions); replaceable interaction models (aspirational — §6 is the gap); multiple genuine frontends and semantic rendering (✓, §16 — the strongest concern); explicit parallel work with cancellability and observability (mechanics ✓, product visibility ✗, §9); remote daemon architecture (✓); user control over the editor as a running system (✓).

The goal is not to make pmacs less powerful so that it becomes approachable. The goal is to make power progressively available.


23. Product Thesis

Emacs offers: the editor is a programmable environment, and the user may transform it completely. VS Code offers: the editor is already a coherent development workstation, and extensions fill in the remaining gaps. Pmacs should offer:

The editor is already an excellent workstation, and every part of that workstation remains inspectable, programmable, concurrent, and replaceable.

Its strongest distinctive proposition is not "Emacs in Rust" or "Emacs with threads":

Pmacs is a live-programmable editor in which computation, interfaces, ownership, and execution locations are explicit — allowing local, remote, interactive, and background work to coexist without freezing or becoming opaque.

The audit's one-line verdict on the thesis: "without freezing" is delivered; "without becoming opaque" is not yet true — for the failures a new user meets first (§1.2), for background work (§9), for settings (§11), and for what a key will do while a modal surface is active (§6). Product coherence is what will make the architecture perceptible. Without it, pmacs risks becoming an impressive collection of subsystems. With it, pmacs becomes a workstation whose complexity is available without being imposed.


24. Known documentation drift (as of 2026-07-25)

Found during the audit; fix opportunistically, ideally before this document is wired into CLAUDE.md/AGENTS.md as required reading:

  • docs/keybindings.md — every src/editor.rs line citation in §3 is stale by ~2501000 lines despite a "last verified @ f8096ff (2026-07-20)" stamp; its shadow list also omits the terminal C-c escape (reports 5 shadows, actual 6).
  • builtin/api/packages.lua (EmmyLua annotations) — missing install_local, reload, load, describe, on_unload; claims update is unimplemented (it is implemented).
  • CHANGELOG.md (~line 300) — claims a describe-key command for self-introspection; no such command ever shipped (the Lua API pmacs.describe.key exists; the interactive command does not).
  • docs/config-registry-framing.md (~658) — claims describe-setting renders through src/help.rs; it hand-builds its own text in builtin/commands/default.lua.
  • src/workers_buffer.rs module doc — says the completions ring caps at 32; COMPLETED_RING_CAP is 64.
  • src/command.rs doc comment on predicate — describes palette gray-out behavior (T M2.7) that never shipped.

25. Update protocol for this document

  • When a PR changes any audited claim here, updating this file rides that PR — flip the grade, rewrite the fact, note the PR number. Same discipline as docs/agent-handoff.md.
  • Line numbers are hints; symbols are authoritative. When touching a section anyway, re-verify its citations; do not let this document accumulate the drift §24 catalogs in others.
  • Grades change only with evidence (a landed PR, a re-audit), never aspirationally.
  • The Ground truth subsections are a snapshot dated 2026-07-25. If a future comprehensive re-audit is performed, update the date in the header and prune superseded facts rather than appending — this is a briefing, not a log.
  • Framing docs for coherence-affecting work should cite the section they serve (e.g. "COHERENCE §6") and state their coherence impact per §20's standing process change.