Merge pull request #236 from levineuwirth/gui-arc-stage0

docs(gui-arc): Stage 0 — the standard sees the GUI
This commit is contained in:
Levi Neuwirth 2026-08-11 20:08:24 +00:00 committed by GitHub
commit f8ad3e758a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 1501 additions and 802 deletions

View File

@ -98,18 +98,18 @@ remain open to them.
| 2 | Golden product journey | **Runs end to end, thin at 11–12** | **Step 1 now works — v1.1.0 ships binaries (#211)**, so the journey no longer begins with a source build. `pmacs .` opens the directory (1a); the interface introduces itself (1b-3); a missing language server says so (1b-2, #204); a build is bound and prefilled (1b-1, #203). **Steps 11 (background-work ownership, §9) and 12 (session restore) are the remaining thin end** | | 2 | Golden product journey | **Runs end to end, thin at 11–12** | **Step 1 now works — v1.1.0 ships binaries (#211)**, so the journey no longer begins with a source build. `pmacs .` opens the directory (1a); the interface introduces itself (1b-3); a missing language server says so (1b-2, #204); a build is bound and prefilled (1b-1, #203). **Steps 11 (background-work ownership, §9) and 12 (session restore) are the remaining thin end** |
| 3 | Zero-configuration state | **Partial** | Defaults genuinely strong; missing-tool failure is silent, not graceful | | 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 | | 4 | Progressive disclosure | **Inverted** | The advanced level is real; the beginner level is the missing one |
| 5 | Unified discoverability | **Partial** | Discovery Stage 1: eleven `help.*` commands (describe key/mode/hook/buffer/command/setting, where-is, list commands/keybindings/settings, apropos) over the existing registries, indexed by `M-x help`. Commands, keys, modes, hooks and settings are now reachable, and `*workers*` already was (`M-x editor.list-workers`); **packages have no comparable surface** (§13), and workers still lack owner/purpose/hierarchy and any indicator (§9). `Command` still has no title/category/flags, M-x rows are still bare names, and the Rust help layer is still orphaned | | 5 | Unified discoverability | **Partial** | Discovery Stage 1: eleven `help.*` commands (describe key/mode/hook/buffer/command/setting, where-is, list commands/keybindings/settings, apropos) over the existing registries, indexed by `M-x help`. Commands, keys, modes, hooks and settings are now reachable, and `*workers*` already was (`M-x editor.list-workers`); **packages have no comparable surface** (§13), and workers carry a required **purpose** and a statusline **activity indicator** since #232, but still lack an accountable **owner** and any **hierarchy** (§9). `Command` still has no title/category/flags, and the Rust help layer is still orphaned. M-x rows carry descriptions since #228 (protocol v23) |
| 6 | Interaction islands | **Weak, and growing** | Six hardcoded key-interception shadows; no transient-keymap mechanism exists | | 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 | | 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 | | 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 | | 9 | Worker ownership | **Mechanism with purpose, without ownership** | Cancellation solid; **purpose required on every job and process, and a statusline activity indicator, since #232**; still **no accountable owner and no 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 | | 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 | | 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 | | 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 | | 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 primitive but only **4** call sites, all LSP panels (`*lsp*` added post-audit by #204); buffer-list and search re-implement it; **the bottom panel is COMPLETE — both frontends, and Stage 3 flipped the adopter default so omission means the panel**. **Tree is implemented (◐) with the LSP outline as its one adopter; the remaining consumers, including dired's `i`, have not adopted** | | 14 | Workbench primitives | **Partial (best trajectory)** | Listview is a real primitive with executable call sites in **two** modules — `lsp.lua` (**4**) and **`git.lua` (1, `*git-status*`, #227)** — so it is **no longer LSP-only**; *(counted excluding comment mentions, which an earlier `grep -c` included)*; buffer-list and search re-implement it; **the bottom panel is COMPLETE — both frontends, and Stage 3 flipped the adopter default so omission means the panel**. **Tree is implemented (◐) with the LSP outline as its one adopter; the remaining consumers, including dired's `i`, have not adopted** |
| 15 | Contextual affordances | **Weak** | Right-click menu only; code actions apply first-blindly; no git integration at all | | 15 | Contextual affordances | **Weak** | Right-click menu only; code actions apply first-blindly; **Git integration reaches status and diff (#227, Stage 1) and no further** — §15's ground truth is authoritative, and this row previously said "no git integration at all" |
| 16 | Semantic frontend | **Strong** | v6..=v21 schema support; production attach remains v20 during the dark panel slice; degradation practiced | | 16 | Semantic frontend | **Architectural: Strong · Product: Weak** | v6..=v23 schema support; production attach remains v20 during the dark panel slice; degradation practiced. **The two subgrades and the product criteria live in §16** — the row points there rather than carrying a grade of its own, so the GUI-as-a-product half cannot hide inside an architectural `Strong` |
| 17 | Distribution | **Partial** | **v1.1.0 ships prebuilt Linux/macOS binaries on tag** (#211) with checksums and a stated glibc floor. No channels, in-place update, rollback, signing, or package-manager distribution | | 17 | Distribution | **Partial** | **v1.1.0 ships prebuilt Linux/macOS binaries on tag** (#211) with checksums and a stated glibc floor. No channels, in-place update, rollback, signing, or package-manager distribution |
| 18 | Onboarding | **Partial** | Journey Stage 1b-3: an unconfigured launch greets in `*scratch*` naming `M-x` and four real bindings, and `M-x help` renders a cheat sheet. Still no tutorial and `C-h` still deletes a word — deliberately, see §18 | | 18 | Onboarding | **Partial** | Journey Stage 1b-3: an unconfigured launch greets in `*scratch*` naming `M-x` and four real bindings, and `M-x help` renders a cheat sheet. Still no tutorial and `C-h` still deletes a word — deliberately, see §18 |
| 19 | Coherence acceptance tests | **Started** | `tests/journey_acceptance.rs` carries 45 pins over steps 2, 3, 4, 5, 6 and 9 — the ratchet is real and stages add rows to it. The other five §19 scenarios (workspace lifecycle, worker ownership, config provenance, package lifecycle, extension isolation) are still unwritten | | 19 | Coherence acceptance tests | **Started** | `tests/journey_acceptance.rs` carries 45 pins over steps 2, 3, 4, 5, 6 and 9 — the ratchet is real and stages add rows to it. The other five §19 scenarios (workspace lifecycle, worker ownership, config provenance, package lifecycle, extension isolation) are still unwritten |
@ -356,7 +356,200 @@ This journey should become a release gate. New architectural work should
be evaluated partly by whether it improves, preserves, or complicates be evaluated partly by whether it improves, preserves, or complicates
the journey. the journey.
### Ground truth: the journey today ### 2a. Required subclaims per step (the GUI arc, Stage 0)
**A grade whose subclaims are unstated is not falsifiable.** The
per-frontend table below grades each (step, frontend) cell as the
**minimum over that step's required subclaims** — never the first word
of a prose verdict. This section is that list, published so a reader can
disagree with a grade by pointing at a subclaim rather than at taste.
Two rules decide what belongs here. **Discoverability is a subclaim
wherever it is the step's substance**, not an annotation on it — steps
4, 7, 8, 9 and 11 are graded on whether a capability is *findable*, because
"present but nobody can find it" is the failure those steps exist to
catch. And **any defect cited as the reason a cell is below `Works` must be a
subclaim**: an annotation cannot absorb a failing one. *Narrowed from
"anything named as a defect", which swept in limitations this section
explicitly excludes — no package manager, no tutorial — and so
contradicted its own exclusions.*
| # | Step | Required subclaims |
|---|---|---|
| 1 | Install | (a) a documented install path completes on a clean machine; (b) the installed binary launches |
| 2 | Launch unconfigured | (a) starts with no user config; (b) presents a usable editing surface; (c) raises no error the user must dismiss |
| 3 | Open real project | (a) a file opens from the CLI; (b) a file opens from inside the editor; (c) a directory opens and is browsable; (d) the project root is established |
| 4 | Understand interface | (a) the mode line reports buffer state; (b) a help surface exists; (c) **discoverable** — the route to it is advertised in-product, not only known |
| 5 | Edit | (a) text edits apply and render; (b) undo/redo; (c) selection, kill and yank; (d) **the full contents of an overlong line are reachable**, by wrapping or horizontal navigation |
| 6 | Language intelligence | (a) a server attaches for a supported language; (b) diagnostics surface; (c) completion, hover and go-to-definition are available; (d) styling reflects the server; (e) **failure is visible and actionable** — both a startup failure and a *later* crash say so, rather than intelligence silently ceasing |
| 7 | Find symbol / file | (a) open a file by path; (b) browse to a file without knowing its path; (c) find a symbol; (d) **discoverable** — the file, browse and symbol routes are **advertised in-product**, not merely bound |
| 8 | Open terminal | (a) a terminal opens; (b) input and output round-trip; (c) it can be closed or killed; (d) **discoverable** — reachable by an advertised binding |
| 9 | Build / test | (a) a build or test command runs **in the opened project's context**; (b) its output is captured; (c) the output is navigable; (d) **discoverable** — §2's own criteria say "Build or test command discoverable" (line above), which is what `C-c c` was bound to satisfy |
| 10 | Inspect error | (a) errors are enumerated; (b) navigation reaches the site; (c) the site is marked in the buffer |
| 11 | Understand what background work is running | (a) work in flight is **visible without asking**; (b) a detailed view is reachable that names each substantial background work item's **purpose and accountable owner — not merely its kind or dispatcher** — **across jobs, processes, servers and terminals**; (c) **discoverable** — reachable by an advertised binding; (d) cancellable |
| 12 | Close + restore | (a) closing is clean and loses no data; (b) per-file state (cursor, scroll) restores; (c) the open-buffer set and window layout restore |
**Aggregation inside a subclaim, stated because it decided two cells.**
A subclaim that names a conjunction — "completion, hover and
go-to-definition"; "a startup failure *and* a later crash" — is graded
by the same minimum rule as the step: **an absent member makes the
subclaim `Missing`, not `Partial`.** Degradation is for a member that is
present and qualified; absence is absence at every level. Steps 6 and 11
are `Missing` on this rule, and an earlier draft graded both `Partial`
by treating a conjunction as one degradable atom — which would have made
the ordinal mean something different at the subclaim level than at the
step level, and is the reading the framing's own Step 7 worked example
already rejects.
**3(c) requires a browsable directory SURFACE, not dired.** Dired is
today's evidence for it, not its definition — so a frontend inherits
whatever browsing it actually has, which is precisely what the
per-frontend columns exist to expose. A subclaim naming the
implementation would have graded the mechanism instead of the journey.
**Deliberately NOT subclaims.** Performance, aesthetics, and parity with
other editors: each is real, and each is graded elsewhere or nowhere.
**5(d) is the boundary case and is deliberately a correctness claim**:
unreachable text is the long-lines arc's defect, while its 100 MB timing
witness stays a separate performance gate. "Large files stay usable" was
in an earlier draft of this row and was performance wearing a
correctness coat.
Folding them in here would make every cell a judgement about taste and
the ordinal would stop meaning anything. **Frontend-specific
limitations are also not subclaims** — they are what the three columns
*measure*, so encoding them here would beg the question.
**Step 12(c) is the worked example of why subclaims are per step and
not per frontend.** It is a required subclaim everywhere, and Q#DS9
makes it structurally impossible on any daemon-backed frontend — so the
GPU cell fails it by construction rather than by omission. That is a
finding the old single-verdict table could not express, because "Partial"
was doing the work of both "some of this is missing" and "some of this
cannot exist yet".
### 2b. The per-frontend journey table
Each cell is the **minimum over §2a's required subclaims**, with the
binding subclaim named. Columns: **local TUI** (`pmacs .`), **attached
TUI** (`pmacs --attach`), **GPU**. **The bar is GPU ≥ local TUI**; the
attached column is evidence, separating a daemon-boundary gap from a
frontend-local one.
Anchors: `COHERENCE.md` §2's ground-truth rows, the GUI arc audit at
`4bc55e8` (`docs/gui-arc-framing.md` §2), Q#DS9
(`docs/desktop-save-framing.md`), and `builtin/runtime/welcome.lua`.
**What counts as "advertised in-product", stated because an earlier
draft of this paragraph got it wrong and mis-graded two steps.** The
welcome advertises **both** a key table (`C-x C-f`, `C-c t`, `C-c c`,
`C-x b`) **and prose** — its first line names `M-x` and `M-x help`
(`welcome.lua:61`). Advertisement is therefore **transitive through the
help graph**: a command reachable from an advertised route counts as
advertised, which is how `help.list-keybindings` carries browse and
symbol (step 7) and how `help.list-commands` carries
`editor.list-workers` (step 11). Reading only the four-entry key table
is what produced the earlier `Partial` at steps 4 and 7.
**A binding is a stronger claim than a route, and 11(c) asks for the
binding.** That is why step 11 still fails while step 7 passes: both are
reachable through the help graph, but **no binding reaches
`editor.list-workers`**. `*workers*` is not binding-free — it carries a
buffer-local `C-c C-k` for `workers.cancel-at-point`
(`async.lua`; `docs/keybindings.md:282`) — and that binding lives
*inside* the view, so it cannot help anyone find it. The absent thing is
specifically **a binding that opens it**.
| # | Step | local TUI | attached TUI | GPU | binding subclaim |
|---|---|---|---|---|---|
| 1 | Install | Works | Works | **Works** | — release builds, ships and *verifies* `pmacs-gpu` (`release.yml:149,168,195`) |
| 2 | Launch unconfigured | Works | Works | Works | — |
| 3 | Open real project | Works | Works | Works | 3(c) satisfied by a browsable surface, not by dired specifically |
| 4 | Understand interface | Works | Works | Works | 4(c) satisfied — the welcome's **first line** says "M-x runs any command; **M-x help lists the keys**" (`welcome.lua:61`). An earlier draft graded this Partial by reading only the four-entry key table and ignoring the prose above it |
| 5 | Edit | Works | Works | **Partial** | 5(a) on GPU — **no IME, no `set_ime_allowed`**, so composed/CJK input is impossible; Latin editing is fine. Local/attached satisfy 5(d) by wrap and by QoL Stage 5 horizontal scroll |
| 6 | Language intelligence | **Missing** | **Missing** | **Missing** | 6(e) — startup failure surfaces; a **later crash does not**, and an absent member of a conjunction makes the subclaim **Missing**, not degraded (§2a's aggregation rule). `server_is_live` is consulted at five sites and every one *skips* work rather than reporting. GPU additionally degrades 6(d) — single-authority semantic styling vs the grid's `merge_styles` — but the grade is already floored by 6(e) |
| 7 | Find symbol / file | Works | Works | Works | 7(d) satisfied **under the published "advertised in-product" wording**: the advertised `M-x help` route reaches `help.list-keybindings`, which lists **every registered binding** (`help.lua`; asserted over every sequence at `discovery_acceptance.rs:208`), so browse and symbol are advertised transitively. *If the intent is direct advertisement only, 7(d) must say so — and the framing's own worked example then forces **Missing**, never Partial* |
| 8 | Open terminal | Works | Works | Works | 8(c) satisfied by the **global** `M-x buffer.kill-this` (`default.lua:1209`); killing a terminal buffer prunes the session and reaps the process (`vterm_stage1_acceptance.rs:336`). 8(c) never required a terminal-*specific* command. 8(d) passes on the welcome's `C-c t` |
| 9 | Build / test | Works | Works | Works | 9(d) passes on the welcome's `C-c c` entry, not on the bare binding; 9(a) runs in the detected project's context |
| 10 | Inspect error | Works | Works | Works | all of 10(a–c) hold. *Being gated on step 6 or 9 is a **dependency, not a subclaim**, and §2a's membership rule forbids an annotation from lowering a grade — an earlier draft let it do exactly that. Grading the dependency would require adding it as a subclaim first* |
| 11 | Understand background work | **Missing** | **Missing** | **Missing** | 11(c) — **no binding opens `editor.list-workers`**, so a keybinding listing cannot lead anyone to it: the subclaim is **absent**, not degraded, and it floors the cell. *The view is not binding-free — `C-c C-k` runs `workers.cancel-at-point` buffer-locally inside it (`docs/keybindings.md:282`) — but a binding that only works once you are already there cannot satisfy a discoverability subclaim.* 11(b) fails too — #232 gives **purpose**, never an accountable **owner**, and the planes stay disjoint (jobs in `*workers*`, processes in `pmacs.process.list`, servers in `*lsp*`, terminals **nowhere**). 11(a) passes on the activity indicator |
| 12 | Close + restore | **Partial** | **Missing** | **Missing** | 12(c) — local restores nothing beyond per-file state (desktop-save is opt-in); on **both daemon-backed frontends it is a structural no-op** (Q#DS9), so they fail it **by construction** |
**Closure condition 2 currently FAILS, with KNOWN failures at two
steps and the exact set pending verification.** GPU is below local TUI
at **step 5** (Partial vs Works) and **step 12** (Missing vs Partial),
and every *graded* cell elsewhere is equal — but GPU 3(c) and 9 rest on
inference (below), so a third failure cannot be ruled out until they are
verified. "Exactly two" would be a stronger claim than this table's own
evidence supports, and an earlier draft made it. GPU 6(c) is inferred
too, but cannot change step 6, which is already floored at `Missing` by
6(e) on all three frontends. That is the whole of
the GPU's journey deficit as this table measures it — a narrower result
than "the GUI feels behind", and a falsifiable one. It survived a review
round that corrected six of the twelve rows, which is some evidence it
is a property of the tree rather than of the grader.
**The journey's two worst steps are frontend-INDEPENDENT**, and that is
the table's other finding. Steps 6 and 11 grade `Missing` in all three
columns: language intelligence dies silently after a server crash, and
**no binding opens** the background-work view — a route exists
(`M-x help` → `help.list-commands` → `editor.list-workers`), and a
buffer-local `C-c C-k` exists *within* the view, but nothing bound leads
to it. That is why 11(c) asks for a binding rather than a route, and why
the binding it asks for is one that **reaches** the view. Neither is GUI work,
neither is closed by this arc, and both were previously carried as
`Partial` — which is how they stayed off the critical path.
**The attached column earns its place at those two rows, and they point
opposite ways.** Step 12 fails on attached TUI *and* GPU, so it is a
**daemon-boundary** gap that no GPU work can close — Q#DS9 and the
workspace object own it, which is why Stage 4b is P2-gated. Step 5
fails on GPU *alone*, so it is **frontend-local** — Stage 1d's IME work
closes it. A single merged TUI column would have shown two identical
red cells and no way to tell those apart.
**Inference flags — and when they must be closed (user ruling,
2026-08-11).** These do **not** block the Stage 0 docs PR. They **must**
be verified **before this table is first enforced as a release gate**.
**3(c) and 9 can change the deficit set**; **6(c) cannot change step 6's
grade today** — 6(e) already floors it at `Missing` on all three
frontends — but still needs evidentiary closure rather than standing
indefinitely as an assumption.
GPU 3(c) is inferred **in both halves** —
rendering and browsing interaction — as are GPU 9 (compile output
renders like any other daemon buffer) and GPU 6(c) (hover reaches the
echo area as on the grid). All must be verified before this table is
used as a release gate.
*An earlier draft called 3(c)'s rendering half "verified" on
`gpu_invocation_acceptance.rs:706`. That test runs
`--headless-managed-probe`, which connects, receives and DECODES a
`BufferSnapshot` into text; it never constructs GPU render state and
never calls `render_offscreen` (`pmacs-gpu/src/main.rs:1065`). It
witnesses delivery and decoding — which is real, and is not rendering.
Retracted rather than softened, because "verified" was the word doing
the damage.*
### Ground truth: the journey today — EVIDENCE ONLY; §2b OWNS THE GRADES
**The verdict column below is the SUPERSEDED single-verdict model.**
§2b's per-frontend table is authoritative for every grade, and where the
two disagree — steps 4, 6, 7, 10 and 11 — **§2b is correct and this
table's verdict is historical.** It disagrees because it grades by a
prose string rather than by the minimum over declared subclaims, which
is the reading §2a and §2b exist to replace.
**Kept rather than deleted, and kept rather than re-synced.** Its value
is the per-step *evidence* — what exists, what was measured, which PR
changed it — which §2b's cells cite and do not restate. Re-syncing its
verdicts would restore a second source of truth for grades and
guarantee this drift recurs; marking it removes the second source
instead.
**A historical grade may stand here. A false statement about the tree
may not** — the two are different, and only the first is what "kept for
its evidence" licenses. Step 8's "no close/kill command" was the latter
and is corrected in place below.
**Grade: reaches step 5; thin from step 6 on.** Was **broken at step 3** **Grade: reaches step 5; thin from step 6 on.** Was **broken at step 3**
at audit time: at audit time:
@ -395,11 +588,11 @@ Full verdict table:
| 4 | Understand interface | **Partial** | Mode line gives name/modified/L:C/scroll + mode/LSP/terminal segments. Journey Stage 1b-3 adds a welcome in `*scratch*` and `M-x help`; **still Partial** because `C-h` deletes a word (deliberately — §18) and there is no tutorial | | 4 | Understand interface | **Partial** | Mode line gives name/modified/L:C/scroll + mode/LSP/terminal segments. Journey Stage 1b-3 adds a welcome in `*scratch*` and `M-x help`; **still Partial** because `C-h` deletes a word (deliberately — §18) and there is no tutorial |
| 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 | | 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`). **Journey Stage 1b-2 (#204) ended the silence** for a server that fails to *start*: the status line names the command, language and errno once per `(language, root, command)`; the modeline reads `LSP:!` instead of nothing; and `M-x lsp.status` renders `*lsp*` over the `status_buffer_text()` renderer that had existed since M4.8 with no caller. **Still Partial**, for a reason unaffected by that landing: a server that starts and then *crashes* is still unsurfaced — `LspEventKind::Crashed` is pushed and no builtin subscriber handles it | | 6 | Language intelligence | **Partial** | Rust grammar bundled and auto-attaches; rust-analyzer preconfigured (`builtin/runtime/lsp.lua`). **Journey Stage 1b-2 (#204) ended the silence** for a server that fails to *start*: the status line names the command, language and errno once per `(language, root, command)`; the modeline reads `LSP:!` instead of nothing; and `M-x lsp.status` renders `*lsp*` over the `status_buffer_text()` renderer that had existed since M4.8 with no caller. **Still Partial**, for a reason unaffected by that landing: a server that starts and then *crashes* is still unsurfaced — `LspEventKind::Crashed` is pushed and no builtin subscriber handles it |
| 7 | Find symbol / file | **File: fixed (open by path merged #162; browsing #165). Symbol: works but undiscoverable** | No find-file/dired/picker existed at audit. Now `C-x C-f` opens a known path and `C-x d` / `C-x C-j` browse (flat listing, `dired` mode keymap); `M-.`/`M-?`/`C-c o` still bound but advertised nowhere and server-gated; no workspace-symbol command; `pmacs.index.*` has no UI | | 7 | Find symbol / file | **File: fixed (open by path merged #162; browsing #165). Symbol: works but undiscoverable** | No find-file/dired/picker existed at audit. Now `C-x C-f` opens a known path and `C-x d` / `C-x C-j` browse (flat listing, `dired` mode keymap); `M-.`/`M-?`/`C-c o` are bound and server-gated. **The "advertised nowhere" claim previously recorded here was false** under the transitive-help contract (§2b): being bound, they are named by `help.list-keybindings`, which the welcome's advertised `M-x help` reaches — which is why §2b grades this step `Works`. Corrected in place as a statement about the tree, while the historical verdict at the left stands. Still true: no workspace-symbol command, and `pmacs.index.*` has no UI |
| 8 | Open terminal | **Works** | Full PTY with scrollback + modeline segment, bound to `C-c t` and configurable through three registered settings (`terminal.default-profile`, `terminal.scrollback-rows`, `terminal.escape-key`) plus named `pmacs.terminal.profiles` (PR #173), and searchable through `M-x terminal.copy-mode` / `C-c C-t`, which materializes the retained scrollback into an ordinary read-only buffer (Stage 2). Named limitations: `C-c t` is unreachable from *inside* a terminal window, where `C-c` is consumed as the escape — `M-x terminal` still works there; and there is still **no close/kill command**, which is the remaining half of this step's discoverability gap. *Was broken outright on the GPU frontend until the double terminal-layout sync was fixed: the child took a `SIGWINCH` storm at tick cadence, so typing into it was impossible while output still flowed.* | | 8 | Open terminal | **Works** | Full PTY with scrollback + modeline segment, bound to `C-c t` and configurable through three registered settings (`terminal.default-profile`, `terminal.scrollback-rows`, `terminal.escape-key`) plus named `pmacs.terminal.profiles` (PR #173), and searchable through `M-x terminal.copy-mode` / `C-c C-t`, which materializes the retained scrollback into an ordinary read-only buffer (Stage 2). Named limitations: `C-c t` is unreachable from *inside* a terminal window, where `C-c` is consumed as the escape — `M-x terminal` still works there; and `C-c t` is not re-advertised there. **The "no close/kill command" claim previously recorded here was false**: `M-x buffer.kill-this` is global (`builtin/commands/default.lua:1209`) and killing a terminal buffer prunes the session and reaps the owned process (`tests/vterm_stage1_acceptance.rs:336`). Corrected in place because it is a statement about the tree, not a historical grade. *Was broken outright on the GPU frontend until the double terminal-layout sync was fixed: the child took a `SIGWINCH` storm at tick cadence, so typing into it was impossible while output still flowed.* |
| 9 | Build / test | **Works** | Journey Stage 1b-1 (#203): `C-c c` runs `compile.run`, and the first prompt is prefilled from the detected project kind (`pmacs.compile.defaults`, seeded `rust = "cargo build"`, extensible from `init.lua`) via `ProjectKind::Rust` — **not** `Cargo`, see §24. The prompt **captures** its directory rather than re-resolving at accept time, so the command it offers and the directory it runs in cannot drift while the minibuffer waits. Still defaults cwd to the detected project root and parses Rust `-->` errors. Named limitation: after `pmacs <dir>` the active buffer is dired's and pathless, so the cwd falls back to the process cwd — §8's execution-location model owns that, and the degradation stays coherent (no suggestion is offered for a directory with no detected Cargo project) | | 9 | Build / test | **Works** | Journey Stage 1b-1 (#203): `C-c c` runs `compile.run`, and the first prompt is prefilled from the detected project kind (`pmacs.compile.defaults`, seeded `rust = "cargo build"`, extensible from `init.lua`) via `ProjectKind::Rust` — **not** `Cargo`, see §24. The prompt **captures** its directory rather than re-resolving at accept time, so the command it offers and the directory it runs in cannot drift while the minibuffer waits. Still defaults cwd to the detected project root and parses Rust `-->` errors. Named limitation: after `pmacs <dir>` the active buffer is dired's and pathless, so the cwd falls back to the process cwd — §8's execution-location model owns that, and the degradation stays coherent (no suggestion is offered for a directory with no detected Cargo project) |
| 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 | | 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) | | 11 | See background work | **Partial** | **Statusline activity indicator since #232** — in-flight count plus the oldest job's purpose, absent entirely when idle, through `ui.activity-indicator`. `*workers*` view via `M-x editor.list-workers`, **with no binding that OPENS it**; `C-c C-k` cancel-at-point works buffer-locally once inside. *Was "Works but undiscoverable — no statusline spinner/progress indicator anywhere"; #232 shipped exactly that indicator on 2026-08-09 and the row went stale the same day* |
| 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) | | 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: A journey observation worth keeping verbatim from the audit:
@ -499,19 +692,32 @@ level is the one missing. Audited level-by-level:
build actions, menus, missing-tool guidance): build actions, menus, missing-tool guidance):
- files ✓ since #162 / #165 (`C-x C-f` opens a path, `C-x d` browses; - files ✓ since #162 / #165 (`C-x C-f` opens a path, `C-x d` browses;
neither is advertised anywhere but the keymap) · buffers ✓ (`C-x **both are advertised** — `C-x C-f` directly in the welcome's key
b`, `*buffer-list*`) · search ✓ (`C-s`/`C-r`/`C-M-s`; project.search table, `C-x d` transitively through `M-x help` →
is M-x-only) · diagnostics ✓ once a server runs · terminal ✓ but `help.list-keybindings`) · buffers ✓ (`C-x b`, `*buffer-list*`) ·
M-x-only · build ✓ but M-x-only with empty prompt · menus △ search ✓ (`C-s`/`C-r`/`C-M-s`; project.search is M-x-only) ·
diagnostics ✓ once a server runs · terminal ✓ **and advertised**
(`C-c t`, in the welcome) · build ✓ **and advertised** (`C-c c`, in
the welcome; the prompt is **prefilled from the detected project
kind**, not empty) · menus △
(right-click only, 11 items) · missing-tool guidance ✗ (§1.2). (right-click only, 11 items) · missing-tool guidance ✗ (§1.2).
*Three claims in this bullet were false and are corrected together:
"neither is advertised anywhere but the keymap", "terminal ✓ but
M-x-only", and "build ✓ but M-x-only with empty prompt". All three
were answered by the welcome buffer and by `C-c t`/`C-c c` — the very
fixes the paragraph after §2's audit quote already records as
landed. They survived because a level-by-level inventory was never
re-run against them.*
**Intermediate** (should discover: palette, keybinding search, workspace **Intermediate** (should discover: palette, keybinding search, workspace
settings, profiles, package management, task definitions, settings, profiles, package management, task definitions,
frontend/language settings): frontend/language settings):
- palette △ (`M-x` fuzzy over bare names — the rows are still bare, §5) · - palette △ (`M-x` fuzzy over bare names — the rows are still bare, §5) ·
keybinding search ✓ (`help.list-keybindings`, `help.where-is`, keybinding search ✓ (`help.list-keybindings`, `help.where-is`,
`help.describe-key`; still no key to reach them) · workspace settings `help.describe-key`; **advertised through the welcome's `M-x help`
route, but no DIRECT binding reaches them**) · workspace settings
✗ (no workspace scope, §11) · profiles ✗ (§12) · package management ✗ ✗ (no workspace scope, §11) · profiles ✗ (§12) · package management ✗
in-session (§13) · task definitions ✗ · frontend customization △ in-session (§13) · task definitions ✗ · frontend customization △
(themes, `pmacs.gpu.set_font`, statusline providers — all Lua-only) · (themes, `pmacs.gpu.set_font`, statusline providers — all Lua-only) ·
@ -652,10 +858,11 @@ the sharpest instance of §1.1.**
function passed as `CompletionSource::Custom`. function passed as `CompletionSource::Custom`.
**What remains missing here:** a discovery surface for **packages** **What remains missing here:** a discovery surface for **packages**
(§13) — `*workers*` already has one, reachable by (§13) — `*workers*` already has one, reachable by
`M-x editor.list-workers`, though §9's ownership model and activity `M-x editor.list-workers`, and §9's activity indicator now exists
indicator are still absent — and **no key reaches any of this**: the (#232) while its **ownership model remains absent** — and **no DIRECT binding reaches any of
family is `M-x`-only by design until the help-prefix decision is taken this**, though the family is advertised: the welcome names `M-x help`,
(see the prefix bullet below). which indexes it. The family stays unbound by design until the
help-prefix decision is taken (see the prefix bullet below).
*Completion is assistance, not validation:* `resolve_accepted_value` *Completion is assistance, not validation:* `resolve_accepted_value`
returns the literal typed text when no candidate is selected, so a returns the literal typed text when no candidate is selected, so a
typo still reaches the handler; refusing a non-candidate is unbuilt typo still reaches the handler; refusing a non-candidate is unbuilt
@ -977,14 +1184,23 @@ so non-pool work (LSP requests, MCP) appears uniformly; one shared
`*workers*` view (`src/workers_buffer.rs`, opened by `M-x `*workers*` view (`src/workers_buffer.rs`, opened by `M-x
editor.list-workers`, auto-refreshing, `C-c C-k` cancel-at-point). editor.list-workers`, auto-refreshing, `C-c C-k` cancel-at-point).
**The identity layer is absent:** **The identity layer is PARTLY built — purpose landed, ownership did not:**
- `PendingJob` (`src/async_runtime.rs:365-392`) carries `{cancel, - `PendingJob` (`src/async_runtime.rs`) carries `{cancel, state,
state, supersede_key, stream_buffer, max_batch, kind, supersede_key, stream_buffer, max_batch, kind, dispatched_at,
dispatched_at}`. **No owner. No purpose string. No **purpose**}`. Since #232 the **purpose is required** — `JobSpec` has
workspace/buffer association. No parent.** The one buffer link that no `Default`, so a dispatcher that supplies none **does not compile**
exists (parse job → buffer) lives in a `SyntaxCoordinator` side map, (`src/async_runtime.rs:429`), which makes presence a type obligation
invisible to the workers view. rather than a convention. **Still absent: an accountable owner, any
workspace/buffer association, and any parent.** The one buffer link
that exists (parse job → buffer) lives in a `SyntaxCoordinator` side
map, invisible to the workers view.
The distinction that survives #232: a purpose says what a job is
**doing**; ownership says **who asked**. Populated from static
per-subsystem constants, an `owner` field would have been an *origin*
and would have misattributed third-party work to a builtin — which is
why #232 shipped none rather than a plausible one.
- `JobKind` is a **closed 12-variant enum** (Sleep, ComputeSum, EmitN, - `JobKind` is a **closed 12-variant enum** (Sleep, ComputeSum, EmitN,
Grep, Parse, FsReadDir, FsStat, FsRename, FsChmod, FsRemove, Grep, Parse, FsReadDir, FsStat, FsRename, FsChmod, FsRemove,
McpRequest, LspRequest). `pmacs.workers.register` funnels Lua jobs McpRequest, LspRequest). `pmacs.workers.register` funnels Lua jobs
@ -1002,26 +1218,43 @@ editor.list-workers`, auto-refreshing, `C-c C-k` cancel-at-point).
|---|---|---| |---|---|---|
| Async jobs | `*workers*` | processes, servers, terminals | | 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 | | 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 | | LSP servers | `*lsp*` status text | opened by **`M-x lsp.status`** (`builtin/runtime/lsp.lua:3533`), but by no binding; 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 | | 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 A terminal PTY appears in **no** user-visible activity view. An LSP
server appears in `*lsp*` (unreachable) and `list()`; its requests server appears in `*lsp*` (unreachable) and `list()`; its requests
appear in `*workers*`; nothing joins them. appear in `*workers*`; nothing joins them.
- **No progress indicator exists anywhere** — no statusline spinner, - **A progress indicator now exists (#232, 2026-08-09)** — a statusline
no busy count (grep for progress/spinner/busy in `src/statusline.rs` provider rendering the in-flight count and the **oldest** job's
is empty). "Visible asynchronous work" (§3) is currently false unless purpose, absent when idle, gated by `ui.activity-indicator`. So
the user knows to run `M-x editor.list-workers`. "visible asynchronous work" (§3) is **no longer false**: activity is
- `ProcessSpec.label` is the nearest thing to attribution: caller- visible without knowing to run `M-x editor.list-workers`, though **no
supplied, unvalidated convention (`lsp:{name}`, terminal buffer binding opens that view** — `C-c C-k` is bound inside it, which cannot
name). help anyone find it.
**The indicator's first act was to expose three months of invisible
LSP file-watcher polling (issue #233)** — §9's own argument,
demonstrated.
- **`purpose` is now required** on every job and process (#232), through
a single allocation funnel with no `Default`, so the compiler proves
every caller supplies one. That is a real per-job description where
there was none.
- **Attribution is still missing, and that is what §9 grades.** A
purpose says what a job is *doing*; it does not say **who asked** —
no owner, no parent, no package. `ProcessSpec.label` remains a
caller-supplied, unvalidated convention (`lsp:{name}`, terminal
buffer name). **This section's grade is left untouched pending a
re-audit**: the mechanism-without-identity finding is *partly*
answered, and moving a grade is an audit act, not a documentation
correction.
The audit's conclusion, worth preserving verbatim: *because identity is The audit's conclusion, worth preserving verbatim: *because identity is
missing, scoped cancellation has nothing to scope over and a unified missing, scoped cancellation has nothing to scope over and a unified
activity view has nothing to group by — the four views exist precisely 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 because there is no common key to merge them on.* **Purpose** landed with #232;
fields on the job and process specs are the prerequisite; the unified **owner and parent** fields on the job and process specs remain the
view and the ownership tree fall out of them. prerequisite, and the unified view and the ownership tree fall out of
them.
--- ---
@ -1490,10 +1723,73 @@ facto privileged implementation.
### Ground truth ### Ground truth
**Grade: strong — the healthiest concern in this document, and most of **This concern carries TWO subgrades, because it asks two different
its asks are already practiced.** questions and one answer was hiding the other.**
- Versioned protocol schema `SUPPORTED=[6..=21]` with deliberate | subgrade | grade | what it measures |
|---|---|---|
| **Architectural** | **Strong** | the protocol, its versioning discipline, capability negotiation, degradation, and the absence of a privileged frontend |
| **Product** | **Weak** | whether the graphical frontend is a workbench a user would choose — graded against §2b's per-frontend journey table and §3.1's blocker list |
**Why the split exists.** "Productize the semantic frontend" was graded
`Strong` on architectural evidence alone, and that grade was true and
was answering the wrong question: the protocol is excellent *and* the
GUI is not yet a daily driver. A single cell could not hold both, so the
product half was invisible — which is how a frontend the reporter would
not choose sat inside the healthiest concern in this document.
**Product subgrade criteria** (each falsifiable, none aspirational):
1. **GPU ≥ local TUI at every journey step** under §2b. **Currently
FAILS at two known steps** — 5 (no IME) and 12 (restore is a
structural no-op) — with the exact set pending verification of §2b's
inferred cells.
2. **The daily-driver blocker list is empty.** **Currently nine open**
(`docs/gui-arc-framing.md` §3.1): Escape quits, IME absent,
`translate_key` holes, sub-line/horizontal scroll, no DPI, folding
dead on the GPU, no session restore, no reconnect, the one-window
ceiling.
3. **Every surviving divergence is declared**, not accidental.
**Not yet assessed** — §3.2's register is seeded empty, so this
criterion has no finding either way and cannot lift or lower the
grade.
**How the grade is derived, so it is a grading rule and not an
adjective.** The three criteria above are *closure* conditions; on their
own they distinguish only "closed" from "not closed", which is not a
grade. The product subgrade reads:
| grade | condition |
|---|---|
| **Strong** | all three criteria pass |
| **Partial** | usable, criterion 1 **passes**, and not all three criteria pass |
| **Weak** | usable and criterion 1 **fails** at one or more steps |
| **Missing** | no usable graphical frontend |
**The rule is TOTAL by construction** — every usable state is covered by
exactly one of `Partial`/`Weak`/`Strong`, since criterion 1 either
passes or fails and `Strong` is the all-pass case. An earlier draft
required a non-empty blocker list for `Weak` and named blockers in
`Partial`, which left a real state ungraded: criterion 1 passing with an
empty blocker list but criterion 3 failing or unassessed. Criterion 1 is
the discriminator because it is the only one that measures the GPU
*against the TUI*; the others measure it against its own goals.
**Current: Weak** — criterion 1 fails at two known steps. (Nine
blockers are open including the one-window ceiling; under the total rule
they no longer *derive* the grade, but they are why it is not close to
`Strong`.) The distinction that
matters is between `Weak` and `Partial`: the GPU is not merely
*unfinished*, it is **behind a frontend that ships in the same binary**,
and that is what the ceiling and the two failing steps say.
**What moves it.** The GUI arc, and nothing else — it exists to satisfy
exactly these three. The product subgrade is what the arc closes
against, and it should be re-graded when the arc closes, not before.
**Architectural ground truth follows; it is unchanged and still strong.**
- Versioned protocol schema `SUPPORTED=[6..=23]` with deliberate
encoding-breaking bumps, both-frontends support required per bump, encoding-breaking bumps, both-frontends support required per bump,
and byte-pin discipline for appended variants (handoff §4). The v21 and byte-pin discipline for appended variants (handoff §4). The v21
bottom-panel family landed with Stage 2B-1 (#184) and is **live in bottom-panel family landed with Stage 2B-1 (#184) and is **live in
@ -1615,7 +1911,10 @@ across the whole command family, not an oversight.
Note the dependency: five of the ten onboarding steps above currently Note the dependency: five of the ten onboarding steps above currently
lead somewhere broken or invisible (find a file — the mechanism is fixed lead somewhere broken or invisible (find a file — the mechanism is fixed
since #162/#165 but is advertised nowhere except the keymap; inspect a since #162/#165 and IS advertised — `C-x C-f` sits in the welcome's key
table directly, and every bound command is reachable through `M-x help`;
this clause read "advertised nowhere except the keymap" and was false at
both levels, found while correcting step 7's row above; inspect a
diagnostic — silent-failure risk; view workers — undiscoverable; diagnostic — silent-failure risk; view workers — undiscoverable;
setting provenance — unanswerable). Onboarding is correctly sequenced setting provenance — unanswerable). Onboarding is correctly sequenced
*after* the P1/P4 fixes, but the cheap floor — a welcome buffer in *after* the P1/P4 fixes, but the cheap floor — a welcome buffer in
@ -1701,6 +2000,47 @@ documented no-op under a daemon. Journey Stage 1 closing is what moves
this priority's own work to done — what is left is other priorities' this priority's own work to done — what is left is other priorities'
work showing up in the journey. work showing up in the journey.
### The GUI arc — placement (Q#GA5)
**Half A slots after Priority 1**, whose own work is complete (§20 P1),
which is why this block sits here rather than lower down — in a
*Recommended Priority Order*, position IS the schedule, and an earlier
draft placed it after Priority 5 while the prose said P1.
**How it interleaves with P2–P5.** Half A is independent of them and
runs alongside: it adds no workspace object, no extension-ownership
model, no discovery surface. The interaction is at **P2 only**, through
the gate below. P3, P4 and P5 neither block it nor are blocked by it,
and P5's workbench-primitive adoption is *helped* by it — Stage 9's
sidebar is a tree-primitive consumer.
It is the product half of §16, and it is what the §16 product subgrade
is graded against.
**Reaching Stage 4b is a P2 START GATE.** Stages 4b (session save and
restore) and 9 (project/files sidebar) are **workspace-owned**: a
session and a sidebar root are both P2's objects, and inventing local
conventions for them is precisely the "fifth independent root
convention" §7 warns against. So when the arc reaches Stage 4b, **P2
starts**, and **no later GUI stage begins** until P2 has an approved
framing and an opened lane. Non-gated GUI work may interleave freely
after that while the object lands.
**The gate is on STARTING P2, not on finishing it** — so the arc is
never blocked on work nobody has begun, and it cannot outrun the model
it depends on. Without the gate, every non-gated stage could finish
first and leave P2 as a terminal closure blocker.
**Arc naming (Q#GA4).** This arc is **"the GUI arc"**, deliberately a
name and not a number. The roadmap's **"Arc 8 — GPU structural parity"
label is RETIRED** here; its scope is this arc's Half B. **"Arc 8" now
unambiguously means the Lean 4 arc** (`docs/agent-handoff.md` §1a,
stages 1–4b landed), which is the numbering that stays. Landed framing
documents that say "Arc 8 adjacent" of viewport facts or splits
(`editing-conveniences-framing.md`, `bottom-panel-framing.md`) keep
their historical text — they are dated records, and this is the
authoritative disambiguation for anything written from here on.
### Priority 2: Make workspace and location explicit ### Priority 2: Make workspace and location explicit
Otherwise project, LSP, remote, task, and persistence accumulate Otherwise project, LSP, remote, task, and persistence accumulate
@ -1803,8 +2143,10 @@ implementation — this list is direction, not commitment):
handle + derived `dispatch_idle`, then migrate shadows one per PR. handle + derived `dispatch_idle`, then migrate shadows one per PR.
4. **Extension ownership** (P3): `hook.remove`, owner-carrying 4. **Extension ownership** (P3): `hook.remove`, owner-carrying
registrations, attribution-by-default. registrations, attribution-by-default.
5. **Worker identity** (§9): owner/purpose/parent on jobs and 5. **Worker identity** (§9): **owner and parent** on jobs and
processes, join the four planes, statusline activity indicator. processes, and joining the four planes. *Purpose and the statusline
activity indicator shipped in #232 (Stage 1); what remains is the
ownership half.*
6. **Workspace entity** (P2): the object, then location values. 6. **Workspace entity** (P2): the object, then location values.
7. **Config provenance + adoption** (P6). 7. **Config provenance + adoption** (P6).
8. **Package lifecycle** (P7, after 4). 8. **Package lifecycle** (P7, after 4).

View File

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

View File

@ -1178,6 +1178,10 @@ someone forgot.
it — the reverted seam failed only the consumed-plan check, not the it — the reverted seam failed only the consumed-plan check, not the
content assertion. That is what the consumed-plan check is for. content assertion. That is what the consumed-plan check is for.
- **Lean 4 arc (Arc 8) — stages 1, 2, 3a, 3b, 4a, 4b ALL LANDED** - **Lean 4 arc (Arc 8) — stages 1, 2, 3a, 3b, 4a, 4b ALL LANDED**
*(**"Arc 8" means THIS arc and only this one.** The roadmap's
colliding "Arc 8 — GPU structural parity" label is retired: that scope
is Half B of **the GUI arc**, which is a name and not a number —
`COHERENCE.md` §20, Q#GA4.)*
(`docs/lean4-mode-framing.md`; #160, #161, #167, #170, #179, #181). pmacs edits Lean 4: `arborium-lean` highlighting, a (`docs/lean4-mode-framing.md`; #160, #161, #167, #170, #179, #181). pmacs edits Lean 4: `arborium-lean` highlighting, a
`lean4` major mode, `⟨⟩ ⦃⦄ ⟮⟯` pairs, and a `lake serve` language `lean4` major mode, `⟨⟩ ⦃⦄ ⟮⟯` pairs, and a `lake serve` language
server with a Lake-aware outermost root, a lazy toolchain probe, a server with a Lake-aware outermost root, a lazy toolchain probe, a
@ -2747,9 +2751,30 @@ nothing; and the CLOSE message must use the same variant family as the
OPEN, or a session closed by the other family's clear leaves its surface OPEN, or a session closed by the other family's clear leaves its surface
on screen forever. on screen forever.
**Fake LSP** (`src/bin/pmacs_fake_lsp.rs`) modes: `fullonly`, **LaTeX is served by `texlab`, and its root is NOT the repository root**
`rangeonly`, `rangeonly16` (UTF-16 + fail-closed bounds validation), (#230). `pmacs.lsp.config.latex` resolves the document root by an upward
`sighelp`. Use these for capability-matrix tests, not real servers. marker walk — `.texlabroot`/`texlabroot` are texlab's own markers,
verified in its `crates/distro/src/language.rs` rather than assumed —
and **`.git` is deliberately excluded from that walk**: a repository
root is the wrong answer for a multi-file document, which is the whole
reason the resolver exists. `config.latex.root` already accepted a
resolver function, so this added no new mechanism.
**Fake LSP** (`src/bin/pmacs_fake_lsp.rs`) modes — enumerated from the
binary, because this list had gone stale and a stale mode list is how a
test ends up covering the shape next to the defect. Capability matrix:
`fullonly`, `rangeonly`, `rangeonly16` (UTF-16 + fail-closed bounds
validation), `sighelp`, `prepare`, `preprefuse`, `rename`,
`inlaybounds`, `inlayrefresh`, `semantictokensrefresh`,
`applyeditplan`, `resourceops`, `posecho`, `defenv`, `wsconfig`,
`rooturi`, `leanprogress`. Failure shapes: `crash`, `error`, `garbage`,
`silent`. **File watchers (issue #233)**: `filewatch` (RelativePattern
`**/*.txt`), `filewatchabs` (plain-string ABSOLUTE glob),
`filewatchflat` (RelativePattern with no leading `**/`),
`filewatchbare` (bare relative string — the P1 regression guard),
`filewatchrereg` (same id twice, no unregister), `filewatchjoin`,
`filewatchretire`. Use these for capability-matrix tests, not real
servers.
## 5. Hard-won ops lessons ## 5. Hard-won ops lessons
@ -3170,6 +3195,20 @@ on screen forever.
## 6. Named deferrals (the standing backlog, consolidated) ## 6. Named deferrals (the standing backlog, consolidated)
**The GUI arc — OPEN, framing approved 2026-08-10, Stage 0 READY FOR PR (not yet opened).**
`docs/gui-arc-framing.md` is the arc-level frame and is also Stage 0's
own framing; Stages 1–10 each need their own before their branch. It
opened from a daily-driver report: the TUI is daily-drivable, the GUI is
not. **`COHERENCE.md` §16 now carries a PRODUCT subgrade (Weak) beside
its architectural one (Strong)**, and the arc is what closes it. §20
places Half A after Priority 1, and **reaching Stage 4b is a P2 START
GATE** — no later GUI stage begins until P2 has an approved framing and
an opened lane. The name is deliberate: **"Arc 8" means the Lean 4 arc
and nothing else** (Q#GA4). Several standing-backlog items below are
sequenced by this arc rather than deferred — §2.5 of the framing maps
every one to a stage or leaves it here explicitly.
**Fixture project-detection census — NOT STARTED, follow-on from R8.** **Fixture project-detection census — NOT STARTED, follow-on from R8.**
`tests/m4_acceptance.rs` alone constructs state through `tests/m4_acceptance.rs` alone constructs state through
`EditorState::new_with_roots` **113 times**, and an unknown number of `EditorState::new_with_roots` **113 times**, and an unknown number of

View File

@ -801,7 +801,15 @@ only compare a red run against a *different* run. Here both selectors
ran green **inside the same gate invocation**, minutes earlier, on the ran green **inside the same gate invocation**, minutes earlier, on the
same tree and machine — `03-lib` (1928 passed, 0 failed) and same tree and machine — `03-lib` (1928 passed, 0 failed) and
`04-lib-crdt` (2113 passed, 0 failed) — and then failed in `11-sweep`. `04-lib-crdt` (2113 passed, 0 failed) — and then failed in `11-sweep`.
Whatever this is, it is not the tree. So this is **not deterministic on this tree; causation and any rate
effect are unresolved.**
*The original wording here was "whatever this is, it is not the tree",
which this file's own rerun rule forbids: a same-tree green establishes
**intermittence only**, and a tree can raise an intermittent failure
**rate** without making it deterministic. Same-tree greens cannot
exonerate the tree. Corrected rather than deleted, because the wrong
claim is the one a later reader would otherwise reach for.*
| field | value | | field | value |
|---|---| |---|---|

973
docs/gui-arc-framing.md Normal file
View File

@ -0,0 +1,973 @@
# The GUI arc — framing
**Status: revision 3 — APPROVED 2026-08-10.** Approved on its design;
the seven accuracy and process edits requested with the approval are
folded in, and no further review round is required before the Stage 0
branch.
**This document is itself the framing for Stage 0**, which is docs-only.
Revision 3's opening previously said that *every* stage gets its own
framing while §10 proceeded straight from this document into Stage 0 —
the two could not both be true. The rule, stated exactly:
- **Stage 0 is framed by this document.** No separate framing round; it
ships documentation only, and its scope is enumerated in §5.
- **Stages 1 through 10 each require their own framing**, approved
before that stage's branch, as the arc-level contract-ownership rule
demands. This document owns ordering, gates and the arc-level bar —
never a stage's acceptance criteria.
**Revision 3 answers the second review round (two blocking findings,
seven required corrections, and nine rulings that close Q#GA4–Q#GA12).**
- **The closure comparison is falsifiable now** (blocking №1). Revision
2's ordinal ordered grades but never said how to *assign* one, and
"normalize to the head grade" silently mis-graded compound rows: a
step reading "Works but undiscoverable" took `Works` from its first
word while the failing half became a non-blocking annotation — even
though **discoverability is the substance of steps 4, 7 and 11**, not
a qualifier on them. §3.3 now defines observable criteria for each
grade, grades a compound step by its **weakest required subclaim**,
and separates **local TUI / attached TUI / GPU** into three columns.
The comparison is GPU against the **local TUI** — the canonical
`pmacs .` journey — with the attached TUI retained as evidence,
because it is what distinguishes a semantic-wire gap from a
frontend-local one.
- **Half B's dependency graph was inverted, and is re-ordered**
(blocking №2). Viewport facts cannot be designed before the
multi-window model decides whether semantic windows are daemon
projections or frontend-local objects, because that decision
determines **the identity a viewport fact is *about***. And a
framing-only stage cannot hand the sidebar geometry it consumes. Half
B is now 6 model framing → 7 viewport/window-identity substrate
(with smooth scroll and the scrollbar) → 8 splits **plus implemented**
non-bottom side geometry → 9 sidebar riding Stage 8 → 10 tabs.
- **"The GPU consumes daemon `view_top`" was backwards.** The backlog
says the opposite in as many words — "the GPU **never** consumes
daemon `view_top`" (`docs/side-quest-backlog.md:147`). Corrected.
- **Stage 4b owns save *and* restore as a pair.** Q#DS9 scopes both to
local mode and makes **both** no-ops under a daemon, enforced in Rust
(`desktop-save-framing.md:222`). Revision 2's Q#GA10 claimed only the
restore *trigger* remained open; snapshot ownership, save timing and
multi-frontend arbitration remain open too.
- **The Bell audit was wrong.** The daemon already emits
`InstanceSignal::Bell` (`src/daemon.rs:1373`), at the audit anchor
`4bc55e8` as well; `src/frontend.rs:349` consumes it and `pmacs-gpu`
does not. Bell is **consumer-only** work for Stage 1c, with no
producer question to answer.
- **Stage 0 no longer contradicts the portability rule.** Revision 2
put the absorption PR *before* committing this framing, which leaves
the approved framing living only in one worktree. The approved
framing and the Stage 0 lane are now the branch's **first commit**;
if synchronization stays a separate PR, that PR carries the framing
first.
- **No orphan scorecard row.** A GUI-product grade needs criteria and
ground truth under `COHERENCE.md` §16 — a distinct **product**
subgrade beside the architectural one — before the scorecard can
point at it.
- **§2.5 stops overclaiming.** It said the arc sequences the whole GPU
backlog while items had neither stage nor deferral. Every item is now
mapped or explicitly left standing, in a table.
- **Q#GA8's temporary island is withdrawn** — ruled against, so this
arc adds **no** off-path hardcode at all (§7).
- **Reconnect attribution covers the silent cases** (§7). EOF and crash
may deliver no `Goodbye` at all; "the daemon's stated reason" alone
would have been unsatisfiable exactly when it matters.
**One correction this round found on its own**, not raised in review:
§3's condition 1 listed the Q#GA3 goals as "Stages 5, 6 and 10" while
§5 marked **four** stages as Q#GA3 goals — the sidebar was missing from
the closure sentence. Fixed, and re-checked against the renumbering.
**Origin.** A daily-driver report, 2026-08-09: *the TUI is a suitable
daily driver; the GUI feels behind similar editors, enough that the
reporter would default to a different editor before using pmacs' GUI.*
This is the same shape that opened the QoL arc (one daily-driver
report → an arc that closed when the report's complaints were answered
on both frontends), at a larger scale — so it gets a standard and an
arc-level frame before any lane, not a framing per gap.
**Ground truth below was established 2026-08-09** by a three-lane audit
(GPU frontend inventory, TUI/grid inventory, documentation sweep) at
`main` @ `4bc55e8`, with the §2.2 producer matrix re-verified against
`src/semantic_render.rs` at the same anchor. Per `COHERENCE.md`'s
citation convention: symbols first, `file:line` second; line numbers
drift, symbols are authoritative.
Three arc-shaping questions were put to the user and ruled on
2026-08-09; they are recorded as resolved, not open:
- **Q#GA1 — RESOLVED: one arc, two halves.** Half A is maturity
(input, parity, chrome, robustness, hover); Half B is structure
(viewport facts, splits/multi-window, side surfaces, and the
presentation stages that depend on them). The arc closes only when
**both** halves land (§3).
- **Q#GA2 — RESOLVED: the closure bar is journey parity plus an empty
blocker list**, now stated as a conjunction over the whole stage map
(§3).
- **Q#GA3 — RESOLVED: all four GUI-native affordances are goals, none
is a non-goal.** Hover/signature popups, smooth scroll + scrollbar,
and a project/files sidebar are in-scope goals. **Tabs/tabline is a
declared goal at deliberately low priority, sequenced last** — the
user's ruling verbatim: it is not a non-goal and "would be nice down
the line," with care required. What the care means is Q#GA12 plus
the anti-patterns pinned at Stage 10.
Coherence sections this framing serves: §2 (the journey, which Stage 0
makes frontend-graded), §3 (the recommended default surface for the
graphical frontend), §6 (interaction islands — see §7's accounting),
**§7 (first-class workspaces — two stages are gated on it, §5.1)**,
§14 (workbench primitives — the sidebar is tree-primitive adoption),
§16 (the semantic-frontend grade this arc completes the product half
of), §20 (priority order — placement is Q#GA5).
---
## 1. Why an arc: the diagnosis
**The GPU frontend is a rendering showcase over a single buffer, not a
workbench.** It is genuinely ahead of the TUI on rendering richness —
a real minibuffer candidate dropdown where the grid has only an inline
`[candidate]` suffix, a minimap, inline math, pixel-precise squiggles,
correct grapheme shaping where the grid drops combining marks in body
text — and behind on the three things that make an editor a daily
driver. Those three are the arc's spine:
1. **The one-window ceiling.** `State` holds exactly one `buffer` and
one `current_buffer_id`; the daemon's entire per-frontend split
layout (`Layout::compute`, `core.views`) is invisible to a semantic
session. The bottom panel band (`PanelBand`) is a hand-built special
case of "a second region." Everything spatial queues behind the
general version: splits, side windows beyond `Side::Bottom`, the
project/files surface `COHERENCE.md` §3 names, per-window status
bands. The July roadmap called this "the largest unscoped design
problem" and it still is.
2. **The GUI cannot be driven like a GUI.** `keymap_stack::Scope` has
no frontend identity and `FrontendEvent` has no command-invocation
variant, so a GPU-only binding cannot exist (the reason #220 shipped
zoom as unbound commands — handoff §6's capability-aware keymap
item). Beneath that, `translate_key` and the winit handler consume
a narrow slice of desktop input; the rest lands in `_ => {}` (§2.3).
3. **The monolith has no test seam for input.** `pmacs-gpu/src/main.rs`
is ~11.7k production lines in one file; the
`gpu-terminal-input` framing already recorded that GPU key routing
is untestable because `App::window_event`'s logic is inline "with no
extractable seam," and called the refactor "a real refactor
[belonging] to its own lane." An input arc that skips the seam ships
blind.
**Why the standard never caught this drifting.** `COHERENCE.md` §16
grades the *semantic protocol* — degradation practiced, capability
negotiation, versioning — and that grade (Strong) is earned. But **no
scorecard row measures the GUI as a product**, and **the golden journey
has only ever been graded on the TUI**. "Semantic frontend: Strong" and
"I'd use a different editor before the GUI" stayed simultaneously true
because the standard only measured the first. This is §1.1's
substrate-without-surface at frontend scale: the substrate is the
protocol and the daemon's facts; the missing surface is the GPU
consumers of them (§2.2 below shows which halves exist). Stage 0
closes the measurement gap so it cannot reopen.
---
## 2. Ground truth (audited 2026-08-09)
### 2.1 What the GPU has
Code area with syntax/LSP styling, gutter (Off/Absolute/Relative/
Hybrid) with diagnostic signs, minimap with click/scrub, one bottom
status band (statusline segments validated and themed), bottom panel
band with divider drag, minibuffer with a 10-row candidate dropdown,
in-buffer completion popup with kind glyphs, right-click context menu,
isearch band UI, diagnostic squiggles (dedicated pipeline), selection
and search washes, peer presence (cursors + selections), inline math,
terminal mode, optimistic CRDT editing with unconfirmed-edit
journaling. Mouse: click, drag, double/triple-click, wheel (line-
quantized), edge auto-scroll, panel and divider gestures, minimap
scrub. Clipboard both directions via `arboard`.
### 2.2 Wire-capability matrix: produced vs consumed
The GPU's live-loop catch-all is one `_ => None` (`main.rs:5211`);
`FoldState` and `BlockAdornments` appear in `pmacs-gpu` **only** in a
debug-name helper. But "the GPU ignores it" means different work
depending on whether a producer exists — revision 1 conflated these,
and three items hid producer scope. Producer column verified against
`src/semantic_render.rs` at `4bc55e8`:
| Capability | Produced? | GPU consumes? | Work required |
|---|---|---|---|
| `FoldState` | **Yes** (`semantic_render.rs:1881`) | No | **Consumer-only** — Stage 3a |
| `InlineAdornments` | Only `(AtOffset, Text)`, from the inlay-hint store (`semantic_render.rs:1849`) | Exactly that same subset | **No live gap today.** Other placements/content are producer *and* consumer work; not claimed by this arc |
| `BlockAdornments` | **No** — a producer test asserts none is emitted (`semantic_render.rs:4543`) | No | Producer + consumer; Stage 3a's framing decides whether GPU folding renders from `FoldState` alone or needs placeholders |
| `ResourceOffer` / `AdornmentContent::Resource` | **No** | No | Producer + consumer; stays deferred (§6) |
| `InstanceSignal::Title` | **No producer found** in `src/` | No | **Not needed for a dynamic window title** — `StatusFacts` already carries the buffer name, so Stage 1c titles the window frontend-locally; a `Title` producer (e.g. terminal-set titles) is separate, unclaimed work |
| `InstanceSignal::Bell` | **Yes** (`daemon.rs:1373`, present at `4bc55e8`) | No — `frontend.rs:349` is the grid consumer; `pmacs-gpu` has no arm | **Consumer-only** — Stage 1c. *Revision 2 recorded "no producer found" and gave Stage 1c a producer question to answer; the producer was there the whole time, and the audit had searched the semantic-render path rather than the daemon's signal path.* |
| `Goodbye(reason)` post-handshake | Yes | Bootstrap only; live-loop reason discarded | **Consumer-only** — Stage 1c |
| `CompletionPopup.prefix_len`/`total` | Yes (on the wire) | Stored under `#[allow(dead_code)]`, unrendered | **Consumer-only** — minibuffer/completion refinement |
### 2.3 Input gaps (verified in-session, not carried from docs)
- **Escape quits the entire application** when no intercept/popup is
active (`main.rs:2769`; the comment says "otherwise it stays the
local quit"). No data is lost — the daemon holds state — but it
reads as a crash to anyone with Escape reflexes.
- `translate_key` produces **no `ProtocolKey::F(u8)`** — F1–F12 are
unbindable in the GUI though the protocol carries them. `BackTab`,
`Menu` also unmapped; `Key::Dead(_) → None` (dead keys silently
dropped); multi-codepoint `Key::Character` truncated to its first
char.
- **No `WindowEvent::Ime`, no `set_ime_allowed`** — CJK/compose input
is unusable. Undocumented anywhere before this audit.
- **No `ScaleFactorChanged` arm; `scale: 1.0` hardcoded** — HiDPI is
wrong (also recorded as a pre-existing gap in
`gpu-set-font-framing.md`).
- Sub-line wheel deltas are rounded then discarded with **no residual
accumulator** — precise-pixel trackpad scroll under ~½ line height
does nothing. Horizontal wheel x is discarded although
`code_scroll_left` exists; `MouseKind::ScrollLeft/Right` are never
emitted. Ctrl+wheel is ignored.
- No middle-click paste, no `DroppedFile`, no I-beam cursor over text,
minibuffer dropdown not clickable (audit F-007).
- **`FrontendEvent::FocusGained`/`FocusLost`/`Detach` are never
sent** — no `Focused` arm; `CloseRequested` exits without `Detach`,
so the daemon learns of departure by socket EOF.
### 2.4 Structure, robustness, chrome
- One document window forever (§1 cause 1). Daemon splits invisible.
- No auto-reconnect; the reconnect banner is an attach-TUI-only seam
(`Frontend::draw_status_overlay`). F-008 in
`gpu-attach-robustness-framing.md`.
- **Session restore is structurally never**: desktop save/restore
early-returns in Rust under a daemon (Q#DS9), and the GPU is always
semantic — so a GPU session can never restore. Journey step 12's
thin end, at its thinnest on this frontend.
- Chrome theming is half-applied: `MENU_BG`, completion popup
background, `MINIMAP_BG`, `CARET_COLOR`, `WINDOW_BG_RGBA`, and the
peer-presence palette are hardcoded constants; custom themes
fracture in the GUI. (The TUI's completion popup and menu are also
unthemed `Indexed` constants — the pair should be fixed together,
per no-privileged-frontend.)
- Cursor: fixed 2px bar, fixed color, no blink, no styles.
- Word wrap regressed at #221: the GPU had cosmic-text
`WordOrGlyph` since it existed and now gets `Wrap::Glyph`; the
long-lines framing already names `ui.line-wrap = "word"` as the
clean additive third value.
- LSP styling diverges by model: the grid **merges** LSP tokens over
tree-sitter (`LspStyleView`/`merge_styles`); the semantic wire is
single-authority — GUI highlighting is strictly poorer in
mixed-authority languages.
- `HoverView` and `SignatureView` exist in the core, **built and never
attached anywhere** (§1.1 dark matter) — relevant to Stage 5.
### 2.5 Already-recorded backlog: mapped or explicitly left standing
Revision 2 said this arc "sequences" `docs/side-quest-backlog.md`
§"GPU frontend mechanics (non-theme)" without restating it, which
claimed coverage it did not have — several items had no stage *and* no
deferral, and a reader checking whether the arc covered their complaint
had nothing to check. **Every item in that section is below. An item is
either mapped to a stage or explicitly left in the standing backlog;
there is no third state.** Handoff §6's capability-aware keymap item
and the folding framings' Stage 3 obligations are absorbed **by
reference** — each keeps its own framing.
| Backlog item | Disposition |
|---|---|
| Command/minibuffer chord forwarding; Meta/Super chords | Stage 1a |
| Rebindable local `Ctrl-V`/`Escape` | Escape half → Stage 1a; `Ctrl-V` half → Stage 2 (it is a keymap-vocabulary question, not an input-plumbing one) |
| Middle-click paste | Stage 1b |
| Right-click context menu | **Already shipped** (§2.1) — the backlog item is stale and Stage 0 retires the line |
| Frontend-local provisional selection | **Standing backlog** — a selection-ownership question, not a GUI-maturity gap |
| Minibuffer `i/total` hint (= `CompletionPopup.prefix_len`/`total`) | Stage 3d |
| Clickable minibuffer dropdown rows (audit F-007) | Stage 3d |
| Multibyte-exact band caret; nav highlight-wrap bug | Stage 3d |
| Telescope-style preview pane; candidate kind/doc annotations; unify TUI inline vs GPU dropdown | **Standing backlog** — the unification is a cross-frontend convergence design, and the other two ride it |
| Scrollbar scroll; pixel-smooth sub-line scroll | Stage 7 (the *discard* bug is Stage 1b; the smooth **model** needs Stage 7's facts) |
| Horizontal scroll / soft-wrap | wheel → Stage 1b; wrap → Stage 3b |
| Auto-reconnect + "reconnecting…" banner | Stage 4a |
| `AttachRequest.initial_size` cell-grid assumption | Stage 1c, with DPI — the assumption is only visible once scale is real |
| Capability renegotiation (relaunch daemon `--features crdt`) | **Standing backlog** — daemon lifecycle, not frontend maturity |
| Peer caret glyph + name label; own-vs-peer cursor merge; `SelectionSnapshot` vs `Decorations::Selection`; background-kind decorations painted | **Standing backlog** — collaboration/decoration rendering; no journey step and no §3.1 blocker depends on them |
| Inline adornment placements beyond `AtOffset` | **Standing backlog** — producer *and* consumer scope (§2.2) |
| Glyphon full-buffer `prepare` ceiling; `Renderer` sub-struct extraction | **Standing backlog** — perf and refactor; the `main.rs` split's first slice is Stage 1-pre and claims no more |
| Golden-PNG comparison harness | **Deferred by §8**, with its condition stated there |
Three gaps from §2.3/§2.4 are not in that backlog section and are
mapped here so they cannot fall through: **`DroppedFile`** → Stage 1b;
**cursor blink and styles** → Stage 3c (§7 registers the knob, and
Stage 3c is what ships it — revision 2 named the configuration with no
stage behind it); **the LSP merge-vs-single-authority divergence**
(§2.4) → **standing backlog**, explicitly, because it is a semantic-wire
authority question whose fix belongs to whoever owns multi-server token
policy, not to a GUI maturity stage.
---
## 3. Closure criterion (Q#GA2)
**The arc closes when all three of the following hold; none alone is
sufficient:**
1. **Every stage in §5 has landed** — or has been explicitly re-ruled
by the user at the time, with the ruling and its reason recorded in
§3.2. There is no stage outside the closure contract: revision 1's
"Tail" is dissolved, and the Q#GA3 goals are Stages **5, 7, 9 and
10** (hover/signature; smooth scroll + scrollbar; the project/files
sidebar; tabs). *Revision 2's sentence said "Stages 5, 6 and 10"
while §5 marked four stages as Q#GA3 goals — the sidebar was absent
from the closure sentence that is supposed to enumerate them.*
2. **The per-frontend journey table shows GPU ≥ local TUI at every
step**, under the grading rules in §3.3.
3. **The daily-driver blocker list (§3.1) is empty.**
Divergences that survive must be declared in §3.2, not accidental.
### 3.1 Blocker list (seed — membership is Q#GA11)
1. Escape quits the application (§2.3).
2. IME absent — CJK/compose input unusable.
3. `translate_key` holes: F-keys, BackTab, dead keys, multi-codepoint
text.
4. Sub-line scroll discard (trackpad feels broken); no horizontal
wheel.
5. No DPI/scale handling.
6. Folding silently dead on the GPU.
7. No session restore on the GPU, ever (Q#DS9).
8. No reconnect after daemon restart.
9. One-window ceiling (graded via the journey table's affected steps
rather than as a single line — listed here so the list cannot be
emptied while the ceiling stands).
### 3.2 Declared-divergence and re-ruling register
Divergences that survive the arc, and any stage the user re-rules out
of the closure contract, are recorded here with a reason (the model is
#221's honest-divergence ruling on word wrap). Seed: none — entries
are added by stage framings or user rulings as they happen.
### 3.3 How a (step, frontend) cell is graded
`COHERENCE.md` §2's existing verdicts are compound strings ("Works but
undiscoverable", "Partial (good once reached)") and do not order.
Revision 2 replaced them with an ordered set but never said how a cell
*acquires* a grade, and its normalization rule — take the head grade,
demote the rest to annotation — is unsound in the exact case it was
written for: **"Works but undiscoverable" would grade `Works`**, and the
undiscoverability would become prose that cannot block closure. That
inverts the standard, because discoverability is not a qualifier on
journey steps 4, 7 and 11 — it *is* their substance.
**Three columns, not two.** Stage 0 grades each step for **local TUI**
(`pmacs .`), **attached TUI** (`pmacs --attach`) and **GPU**
separately.
**The comparison is GPU against the local TUI.** That is the canonical
`pmacs .` journey and the frontend the daily-driver report calls
suitable, so it is the bar the GUI must meet.
**The attached TUI column is retained as evidence, not as the bar — and
what it is evidence *of* is narrower than revision 3 first claimed.**
The attached TUI is a **grid** frontend: it handshakes
`semantic_render: false`, and the field's own comment says it "never
consumes the SemanticFrame family" (`src/attach.rs`, the
`FrontendCapabilities` constructor). So a shared GPU/attached-TUI gap
cannot mean "the semantic wire is at fault" — the attached TUI is not
on that wire. What the three columns actually separate is:
- **local vs daemon-attached** behaviour (local TUI against attached
TUI), which isolates everything the daemon boundary introduces; and
- **attached-grid vs semantic/GPU** behaviour (attached TUI against
GPU), which isolates what is specific to semantic rendering.
**Neither comparison alone establishes producer-versus-consumer
ownership.** Reading the columns narrows where to look; **source
tracing is what assigns the gap**, exactly as §2.2's matrix had to be
verified against `src/semantic_render.rs` rather than inferred from
behaviour. A single TUI column would still have merged two distinct
diagnoses — that argument survives — but it was never going to hand out
owners for free.
**The grades, by observable criteria.** Each is a test someone else can
run and get the same answer:
> **Broken < Missing < Partial < Works**
- **Works** — every required subclaim holds with no qualifier, by a
route the step's own discoverability subclaim admits.
- **Partial** — every required subclaim is *satisfiable*, but at least
one is degraded: reachable only by a route the step does not admit
(e.g. only by typing an unlisted command), or holding only under a
stated precondition.
- **Missing** — a required subclaim has **no surface at all**: the
action is unavailable and attempting it produces neither effect nor
error.
- **Broken** — a surface exists and using it produces a **wrong
result**, data loss, or an application-level failure. Ranked *below*
`Missing` deliberately: an absent feature is honest, while a present
one that misleads costs the user work and trust.
**A compound step is graded by its weakest required subclaim.** Each
step in the table declares its required subclaims explicitly; the cell's
grade is the **minimum** over them, never the first word of a prose
verdict.
**The worked examples below are HISTORICAL, and `COHERENCE.md` §2b is
authoritative for every current grade.** They are kept because they are
what motivated this rule, and they are marked because a framing and the
standard it serves must not hand a reader two different grades for one
step. They quote the **pre-Stage-0** verdict strings, not today's table:
- **Step 7** — the row then read "Symbol: **works but
undiscoverable**". Subclaims *reachable* / *discoverable*: `Works` and
`Missing` ⇒ **`Missing`**. **Stage 0 graded it `Works`** on evidence
this example did not have: advertisement is transitive through the
help graph, and the advertised `M-x help` route reaches
`help.list-keybindings`, which names every registered binding.
- **Step 11** — the row then read "**Works but undiscoverable**". Same
shape, and here the outcome **stands**: Stage 0 also graded it
`Missing`, because **no binding opens `editor.list-workers`** for a
listing to name. The view does carry a buffer-local `C-c C-k`
(`workers.cancel-at-point`), which is reachable only once you are
already inside it.
The rule these examples exist to establish is untouched by either
outcome: **the head-grade rule would have graded both `Works`**, and
that is what makes it unsound. Step 7 moving on better evidence is the
system working; step 11 not moving is the defect surviving contact with
it.
Under revision 2's head-grade rule **both would have graded `Works`**,
and the undiscoverability that is the entire finding would have become
annotation text with no effect on closure. Two of the journey's twelve
steps is not an edge case.
**Annotations carry only what is not a required subclaim.** They cannot
absorb a failing subclaim; if something is load-bearing enough to
mention as a defect, it is load-bearing enough to be a subclaim and be
graded. Where the two frontends differ only in an annotation, the
difference is recorded and does not block closure — that remains true,
and is now narrow rather than a loophole.
**Stage 0 must publish the subclaim list per step**, not just the
grades. A grade whose subclaims are unstated is not falsifiable, which
is the whole objection this section answers.
---
## 4. Arc structure (Q#GA1)
**One arc, two halves; the name is "the GUI arc," deliberately a name
and not a number.** The roadmap's "Arc 8 — GPU structural parity"
label already collides (the Lean 4 framing also claims Arc 8; the
collision is recorded in `docs/dired-framing.md`). This arc subsumes
roadmap-Arc-8's scope as its Half B; the numeric label retires.
- **Half A — maturity** (Stages 0–5): the GPU behaves like a competent
desktop application over its existing one-window model. No
structural redesign; heavy protocol work only where §2.2 shows a
producer already exists, or the stage's framing names the producer
scope it adds.
- **Half B — structure** (Stages 6–10): the multi-window model, then
the viewport/window-identity substrate it defines, then splits and
side geometry, then the presentation stages that depend on
multi-window state (sidebar, tabs). The order is load-bearing — see
the note opening Half B in §5.
Half A ships visible value while Half B's model framing matures; the
arc does not close at the end of Half A (§3's condition 1 spans both
halves), so the early wins cannot quietly become the whole arc.
---
## 5. Stage map
**Stages 1–10 each get their own framing before their branch; Stage 0
is framed by this document** (see the status block). This document owns
the ordering rationale and the arc-level bar, never stage-level
acceptance criteria (the contract-ownership rule). **Every PR in this
arc opens with its `docs/active-work.md` lane written at the branch's
first commit** — the standing correction from #171/#215, missed again
at #224 and #225, and adopted here as an arc rule rather than re-hoped.
### 5.1 The P2 gate (blocking №2's resolution)
Two stages are **workspace-owned** and carry a hard gate: they may not
start before the P2 workspace arc has landed at least the workspace
object they consume.
- **Stage 4b (session save *and* restore)**: "what a session *is*" is
the workspace question — Q#DS9 failed precisely because a daemon
layout had "nothing principled to attach to" (`COHERENCE.md` §7). A
frontend-keyed convention invented here would be a new ownership
story P2 then has to unwind; revision 1 called that v1 "plausible",
revision 2 withdrew the recommendation, and Q#GA10 is now **ruled**
(both surfaces preserved, the save path owned here too). The gate is
what makes the ruling implementable: snapshot ownership and
multi-frontend arbitration have no answer without P2's object.
- **Stage 9 (project/files sidebar)**: a sidebar must show *something
rooted*, and §7 warns P2 must start "before a fifth subsystem grows
its own root convention — four have already diverged." The sidebar
is the fifth if it picks its own root.
**Reaching Stage 4b is a P2 START GATE, not merely a pause** (Q#GA5
ruling, revision 3). Revision 2 let the gated stage stall while
everything else proceeded. **It could not have let the arc formally
close around P2** — the gated stages are inside the closure contract
(§3, condition 1), so closure still blocked on them. What it *would*
have allowed is every **non-gated** stage finishing before P2 began,
leaving P2 as a **terminal closure blocker**: an arc sitting at 100%
of the work it could do, waiting on an arc nobody had started. The rule
is stronger:
1. When the arc reaches Stage 4b, **P2 starts**. That is the trigger.
2. **No later GUI stage starts** — gated or not — until P2 has **an
approved framing and an opened lane**. Those two are the observable
condition; P2 need not have *landed* anything.
3. Once P2 has both, **non-gated GUI work may interleave** freely while
the workspace object lands. Only the two gated stages (4b, 9) wait
on the object itself.
The gate is on *starting P2*, not on P2's completion, so the arc is
never blocked on work nobody has begun — and it cannot outrun the
model it depends on. A gated stage never proceeds on a local
convention; that was already true and stays true. The arc's
`docs/active-work.md` lane records the gate state whenever it is in
force.
### Half A — maturity
**Stage 0 — the standard sees the GUI (docs only).**
*The framing goes first, and that reverses revision 2's ordering.*
Revision 2 put the absorption PR ahead of committing this document,
which contradicts the portability rule it cites elsewhere: an approved
framing that lives only in one worktree is one `git clean` from gone
and does not travel to another machine. **The approved framing and this
arc's `docs/active-work.md` lane are the Stage 0 branch's first
commit.** If synchronization stays a separate PR, **that PR carries the
framing first** — absorption may precede the rest of Stage 0, never the
framing.
*The absorption pass*, whose scope is now enumerated rather than
described (it grew on 2026-08-10 when six lanes merged in one session):
- **Five stale lanes in `docs/active-work.md`** — #224 and #225 carried
as OPEN, #228 as OPEN and MERGE-BLOCKED, LSP LaTeX as "no PR yet"
(merged as #230), destination capture as "PR #231 OPEN" (merged as
`0e4c58d`). Durable facts into `docs/agent-handoff.md` first, then
remove the **PR-specific** block.
**#228 is the exception, and it must not be retired "per Rule 4" as
though Discovery were finished.** Rule 4 removes a lane when its
**arc** is done; Discovery's is not. Two entries exist — the
PR-specific block and the standing **Discovery lane (P4)**, which
already says "Rewritten, not removed" for exactly this reason. Stage
0 removes the first after re-homing its facts and **rewrites and
coalesces the second** to *"Stage 2 merged; later discovery work
remains"*. Still open there: **predicate evaluation**, **command
metadata** (title/category/aliases/flags), **help unification**, and
**the prefix decision**. Deleting that lane would drop four named
pieces of open work on the strength of one merged stage.
- **The authority/recovery anchor**, which points at `9a26ac8` while
`main` has moved well past the audit anchor `4bc55e8`.
- **`COHERENCE.md` §0 row 16 / §16's `v6..=v21` → `v6..=v23`.** The
ceiling moved **twice**: #221 took it to v22 for `LineWrapFacts`, and
**#228 took it to v23** for `MinibufferPromptRows`
(`PROTOCOL_VERSION = 23`, `SUPPORTED_PROTOCOL_VERSIONS = 6..=23`,
`pmacs-protocol/src/message.rs:1843`). *Revisions 2 and 3 both said
v22, having read the range at the audit anchor and not re-read it
after Discovery landed.* The same row's "production attach remains
v20" is **still correct** — `ADVERTISED_PROTOCOL_VERSION` is 20 — and
must not be swept along with the range.
- **The U4 correction and the U9 residue** in
`docs/ci-red-signatures.md`. U4's flavour field is not a matching key
(the same selector and fragments red on both macOS flavours) and one
of its four "occurrences" was a deliberate bite. **U9's text must be
fixed, not merely carried**: it says a same-tree green shows the
failure "is not the tree," which contradicts this file's own rerun
rule — a tree can raise an intermittent failure *rate* without making
it deterministic. The replacement claim is **"not deterministic on
this tree; causation and rate effect unresolved."**
- **The stale backlog line** for the right-click context menu, which
ships (§2.5).
- **Journey step 11's verdict**, which #232 falsified on 2026-08-10.
`COHERENCE.md` §2 still reads "**Works but undiscoverable** … no
keybinding, no statusline spinner/progress indicator anywhere (§9)".
#232 shipped exactly that indicator — a statusline provider showing
an in-flight count and the oldest job's purpose, absent when idle.
The row needs regrading under §3.3, and §9's own grade needs
re-reading: the mechanism-without-identity finding is partly
answered. **Found while grading the journey for this revision, not
in review** — which is the argument for §3.3's three columns, since
a stale row survives precisely as long as nobody has to assign it a
falsifiable grade.
*Then Stage 0 proper:* add the per-frontend journey verdicts to
`COHERENCE.md` §2 under §3.3's grading rules, **including the subclaim
list per step**; place the arc in §20 (Q#GA5); cross-reference from
`docs/agent-handoff.md` §6; retire the "Arc 8" numbering (Q#GA4). No
runtime code.
**No orphan scorecard row.** Revision 2 proposed adding a GUI-product
row to the scorecard with a grade attached ("Weak — renderer ahead,
workbench and input behind"). A scorecard row is a pointer to a graded
concern, and there is no graded concern for the GUI *as a product*:
`COHERENCE.md` §16's grade is architectural. So Stage 0 first
establishes **a distinct product subgrade beside the architectural one
in §16**, with its own criteria and audited ground truth, and *then*
the scorecard points at it. A row whose grade rests on nothing is the
thing §16 exists to prevent.
**Stage 1 — input foundation.**
- **1-pre: the input seam.** Extract `App::window_event`'s routing
into testable functions — the refactor `gpu-terminal-input` already
named as its own lane, plus the first slice of the recorded
`main.rs` split. This is the stage's first PR because everything
after it needs witnesses.
- **1a keyboard correctness**: Escape stops being the local quit
(round-trips like any key; quitting becomes a command/window
affordance — subsumes the backlog's "rebindable local Ctrl-V/Escape"
item on the Escape half); `translate_key` completion (`F(u8)`,
`BackTab`, `Menu`; dead keys held for 1d rather than dropped).
- **1b pointer/scroll correctness**: sub-line residual accumulator;
horizontal wheel → `code_scroll_left`; middle-click paste; I-beam
cursor over text; `DroppedFile`.
**Q#GA6 — RULED: land the TUI answer in this same stage; no declared
divergence.** The TUI half is smaller than revision 2 implied, and
the corrected trace is this: crossterm already delivers
`MouseEventKind::ScrollLeft`/`ScrollRight`; `src/protocol.rs` and
`pmacs-protocol` already carry them as `MouseKind::ScrollLeft`/
`ScrollRight`; and **attached** mouse events already round-trip
through `mouse_from_crossterm` / `mouse_to_crossterm`
(`src/protocol.rs:712`, `:777`). Local and attached document events
**converge on the one document handler, whose only wheel arms are
`ScrollUp`/`ScrollDown` at `src/editor.rs:3189`** — that single site
is where horizontal wheel is dropped.
*Revision 3 cited `src/editor.rs:5863-5864` and `:3407` here. Both
are **terminal-content** paths — `:3407` matches `TerminalMouseKind`
and drives `terminal_manager.scroll_view` — not the document window,
so they were the wrong sites for this ruling.*
So the TUI answer is one handler arm on an event that already
arrives, not new plumbing, and QoL Stage 5's reason for excluding
explicit-scroll surfaces — keeping the frontends agreeing — is
*served* by doing both here rather than traded against.
- **1c session/window signals**: `Focused` → `FocusGained`/`FocusLost`;
`CloseRequested` sends `Detach` before exit; post-handshake
`Goodbye` reason surfaced (consumer-only, §2.2); **window title
composed frontend-locally from `StatusFacts`** — no `Title` producer
required (§2.2); **`Bell` as a plain consumer** — the producer exists
and always did (§2.2), so there is no producer question and no option
to drop it; `ScaleFactorChanged`/DPI, and with it the
`AttachRequest.initial_size` cell-grid assumption, which is only
observable once scale is real.
- **1d IME — Q#GA7 RULED: the full preedit overlay, not a commit-string
minimum.** The scope is explicitly: commit string; **caret and
selection range within the preedit**; **cancellation**; and
**focus-loss cleanup** so a dropped composition cannot survive as
stale overlay text. Preedit needs a rendering surface, which is why
this is not folded into 1a — and why the ruling has real cost, stated
rather than discovered later.
**Stage 2 — capability-aware keymap resolution.**
**By reference, not absorption**: handoff §6 requires this to be its
own framing round and forbids starting it as a half-lane. This arc
sequences it here because Stage 1's seam makes its GPU consumers
testable, and consumes it for default zoom bindings and every future
GPU-native chord. **This arc feeds Stage 2's framing one explicit
input question: does the capability-aware vocabulary cover pointer
gestures (wheel-with-modifier), or keys only?** Revision 1 assumed
the former; nothing yet guarantees it (major №4).
**Q#GA8 — RULED: wait for Stage 2. The temporary Ctrl+wheel zoom
island is not created.** Zoom arrives through this stage's mechanism or
not at all. Consequently this arc adds **no** off-path hardcode, and
§7's island accounting is now unconditional rather than
"at most one" — there is no removal criterion to track because there is
nothing to remove.
**Stage 3 — parity consumers.**
- **3a folding Stage 3** — consumer-only per §2.2's matrix (the
producer exists). The obligations are already enumerated in the
folding framings (a `FoldState` consumer, the fold-mirror clear on
`BufferSnapshot` (R2-4), `fold_projection` flip, optimistic-edit
unfold (R2-3), caret/hit-test fold-awareness). Whether GPU folding
needs `BlockAdornments` placeholders — which would add producer
scope — is that framing's question. Its ordering precondition
(bottom-panel Stage 2's landed band) is satisfied.
- **3b word wrap** — `ui.line-wrap = "word"` as the declared third
value. Nearly free on the GPU (cosmic-text `WordOrGlyph`).
**Q#GA9 — RULED: implement the grid answer with UAX #14; no declared
divergence.** The dependency is accepted rather than traded for a
§3.2 entry, so both frontends wrap by the same rules.
- **3c chrome theming** — `ThemeFacts` adoption for menu, completion
popup, minimap, caret, window background, peer palette; the TUI's
unthemed popup/menu pair is fixed in the same stage or declared.
**Cursor blink and cursor styles ship here** — §7 registers the knob
through the config registry and this is the stage behind it (§2.5).
- **3d minibuffer/completion refinements** — `prefix_len`/`total`
rendered (consumer-only, already on the wire per §2.2); clickable
dropdown rows (audit F-007); multibyte-exact band caret; the nav
highlight-wrap bug. Grouped as its own sub-stage rather than
scattered, because §2.5 showed four backlog items landing in one
surface.
**Stage 4 — robustness.**
- **4a auto-reconnect** + a reconnecting banner (parity with the
attach TUI's seam; F-008). This adds a background reconnect loop and
therefore owes §20's background-work attribution — see §7 for the
contract its framing must satisfy (owner, lifetime, cancellation,
failure attribution).
- **4b session save *and* restore for semantic frontends** (Q#DS9) —
**P2-gated, §5.1.** The stage owns **both halves as a pair**, which
revision 2 got wrong by naming only restore. Q#DS9 scopes v1 to the
local `editor::run` path and makes `desktop_mode(true)` auto-save
**and** auto-restore no-ops in daemon mode, enforced in Rust by a
`DaemonMode` marker that `save_session`/`restore_session` early-return
on. So there is no snapshot being written under a daemon today: a
restore path alone would have nothing to read, and shipping restore
without save would be a stage that cannot work by construction.
**Q#GA10 — RULED: preserve both surfaces.** Automatic restore on the
**first eligible attach** when armed and no explicit target was
supplied, **plus** the existing explicit command — not one or the
other. What remains open is more than revision 2 admitted when it
said only the trigger shape was: **snapshot ownership** (who writes
it, keyed how, once the frontend is not the owner), **save timing**
(before-quit is a local-mode assumption; a daemon frontend can detach
without quitting anything), and **multi-frontend arbitration** (two
attached frontends with divergent layouts and one workspace key).
All three are decided in this stage's framing, on P2's object.
**Stage 5 — hover/signature popups** (Q#GA3 goal). The core's
never-attached `HoverView`/`SignatureView` are the data-model
precedent; the GPU needs a popup surface and a wire decision (ride an
existing family vs a new message — its framing decides; hover data
currently flows Lua → echo/`*lsp-help*`, so there is **producer scope
here by construction**, stated rather than hidden). Independent of
Half B; sequenced after Stage 3 so the popup is themed from birth.
### Half B — structure
**Revision 3 reorders this half.** Revision 2 ran viewport facts (6)
before the multi-window model (7), which is backwards twice over. A
viewport fact is *about* something — a window — and whether a semantic
window is a **daemon projection** or a **frontend-local object** is
exactly what the model stage decides; designing the facts first would
fix an identity the model then has to honour or break. Second, revision
2's model stage was **framing-only** yet the sidebar was told to "ride
Stage 7's geometry": a framing produces no geometry, so Stage 9
consumed something no stage shipped. Non-bottom side geometry is now
**implemented** in Stage 8.
**Stage 6 — the multi-window model framing.** The arc's center of
gravity and the reason Half B exists: how a daemon layout projects to
a semantic frontend (project the per-frontend layout tree vs
frontend-local layout over multiple buffer subscriptions — the wire
today assumes one document window per semantic session, with the panel
band as the only exception). **Its output that everything downstream
needs is the window-identity decision**, because that is what a
viewport fact, a split, a side slot and a tab all refer to. Framing
only; it ships no runtime code, and nothing downstream is told to
consume geometry from it.
**Stage 7 — the viewport/window-identity substrate, and the scroll
feel that reads it** (Q#GA3 goal). Viewport facts on the wire, carrying
the identity Stage 6 settled — **the GPU never consumes daemon
`view_top` today**, which the backlog names as the blocker for recenter
and every scroll command (`docs/side-quest-backlog.md:147`; revision 2
stated this exactly backwards). **Smooth scroll and the scrollbar live
here**, not in a tail: a scrollbar needs authoritative extent and
position, and pixel-smooth scrolling changes the scroll model those
facts feed.
**Stage 8 — splits shipped, and side geometry with them**: rendering,
input routing, per-window status bands, window-command parity
(`C-x 2/3/o/0/1`), **plus implemented side-window geometry beyond
`Side::Bottom`**. The geometry is here rather than in Stage 6 because
it is code, and because Stage 9 consumes it.
**Stage 9 — the project/files sidebar** (Q#GA3 goal) —
**P2-gated, §5.1.** Tree-primitive adoption (`COHERENCE.md` §14 names
project files as a future tree consumer; §3 names the surface). Rides
**Stage 8's implemented geometry** and P2's root object.
**Stage 10 — tabs/tabline** (Q#GA3 goal — last, low priority by
ruling). **Q#GA12 — RULED: a deliberate deferral to this stage**,
decided after P2 and the multi-window model exist, because both are
what make the readings meaningful — the lineage precedents disagree
(Emacs `tab-bar-mode` tabs are **window configurations**; tab lines and
Doom's centaur-tabs are **buffers**), and a workspace-keyed third
reading only becomes available once P2 has landed. Deferring is the
ruling, not an absence of one. The constraints hold regardless and are
pinned now: tabs present **existing objects** (whichever kind), **never
a parallel registry** with unvalidated references — the menu-label
mistake is the named anti-pattern — and the surface is **optional and
off by default**.
---
## 6. Non-goals and named deferrals
- **GUI as the default frontend.** Deliberately **not** the closure
bar (Q#GA2 chose journey parity). It remains
`gpu-initial-target-framing.md`'s deferral, to be *decided* — not
assumed — when the arc closes.
- **Git integration** (`COHERENCE.md` §15): editor-wide, not
GUI-specific; not this arc.
- **Settings/preferences GUI**, **native menu bar**, **multiple OS
windows**: out of scope; nothing below depends on them.
- **`ResourceOffer`/image rendering**: unproduced and unconsumed
(§2.2); stays deferred unless a stage (sidebar icons, hover docs)
pulls it in with a framing that owns both halves.
- **Proportional code fonts, ligature/feature toggles, font wire
transfer**: `gpu-set-font-framing.md`'s deferrals stand.
- **Remote GPU paths, daemon service management**: unchanged.
- **Multi-cursor**: pre-existing v0.1 non-goal, unchanged.
---
## 7. Coherence impact (per `CLAUDE.md` / `COHERENCE.md` §20)
- **Journey steps touched**: **3, 4, 5, 6, 7, 8, 10, 12** — *on the GPU
frontend*; Stage 0 makes the journey frontend-graded so the impact is
measured per step rather than asserted. Revision 3 listed five; three
were missing because the list was carried from revision 1's smaller
stage map and never re-derived against the stages this arc actually
ships. **Step 3** — `DroppedFile` (Stage 1b) is an open-a-file route.
**Step 5** — keyboard correctness, IME, folding, wrapping and
scrolling are all editing-surface work (Stages 1a, 1d, 3a, 3b, 7).
**Step 6** — completion refinements and hover/signature popups
(Stages 3d, 5).
- **Interaction islands**: this arc adds **no dispatch shadows** — the
count stays at six — and, after Q#GA8's ruling, **no off-path
hardcode either**. Revision 2 reserved one temporary island for the
Ctrl+wheel zoom intercept under a mandatory removal criterion; the
ruling declined it, so the census is untouched by this arc and there
is no removal criterion to track. Zoom arrives through Stage 2's
mechanism or not at all. A stage that believes it needs a new
*shadow*, or a new island, returns to this document first.
- **Config registry adoption**: every user-visible knob this arc adds
(smooth scroll, scrollbar, cursor blink, tabline toggle, IME
behavior if any) registers through the config registry — no new raw
Lua-table settings. The minimap's divergent tab width (4 vs the
shared 8) stays owned by config-registry Q#CR13, referenced not
absorbed.
- **Background-work attribution** (moderate №8): Stage 4a's reconnect
loop is background work and owes the §20 attribution regardless of
§9's unsolved general model. The contract its framing must satisfy:
**owner** — the GPU frontend process, scoped to its session, never
the daemon; **lifetime/cancellation** — bounded backoff, canceled
on user quit and on successful re-attach, never outliving the
window; **failure attribution** — every terminal failure surfaces
in-window with a reason, and the contract covers the case where the
daemon supplies none.
**The reason requirement is two-sided, because the silent cases are
the common ones.** Revision 2 required "the daemon's stated reason",
which is unsatisfiable exactly when it matters: a daemon that
**crashes or drops the socket delivers no `Goodbye` at all**, and the
frontend learns of departure by EOF (§2.3 records that the GPU
already loses its peer this way today). So: **use the daemon's reason
when one arrives** — which is why Stage 1c's post-handshake
`Goodbye`-reason consumer precedes this stage — **and otherwise
surface an explicitly locally-classified transport/EOF reason**,
labelled as locally inferred rather than reported. A banner that says
nothing because the daemon said nothing is the failure this clause
exists to prevent. §9's activity-indicator gap is *not* claimed by
this arc.
---
## 8. Verification shape
- **What already exists is used, not rebuilt** (major №5): the real
offscreen `render_to_view` composition harness, the readback path,
the smoke tests, and the required-GPU CI job are **landed**. Stages
3c, 8 and any pixel-visible change add pixel assertions against
that harness immediately. What is deferred from
`gpu-golden-harness-framing.md` is only **golden-PNG comparison and
the case gallery**; a stage adopts those if image diffing beats
direct assertions for its witnesses, with that framing.
- **The a37 problem is confronted, not inherited.** Real-GPU
end-to-end tests compile only when `pmacs-gpu` is built, return
`ok` without running otherwise, and are load-sensitive — the
recorded footing hazard. Stage 1-pre's seam exists so input stages
are witnessed *without* a display; stages that genuinely need a
real frontend say so and name their witness (`PMACS_REQUIRE_GPU`
discipline).
- **The arc ratchet**: extend `tests/journey_acceptance.rs` with
GPU-frontend rows where headlessly drivable; stages add rows, none
removes them — same rule as the existing ratchet.
---
## 9. Rulings — Q#GA4 through Q#GA12, all closed
**Every arc-level question is ruled as of revision 3.** They are kept
here with their answers rather than deleted, because a stage framing
that wants to revisit one needs to see what was decided and why it is
not open.
- **Q#GA4 — RULED.** The name is "the GUI arc"; the numeric **Arc 8
label retires** at Stage 0, resolving the collision with the Lean 4
framing's claim on the same number.
- **Q#GA5 — RULED, with a hardening.** Half A slots after P1. Reaching
Stage 4b is a **P2 start gate**: no later GUI stage starts until P2
has an approved framing and an opened lane, after which non-gated
work interleaves freely while the object lands (§5.1). Stronger than
the recommendation carried in revision 2, which would have let every
**non-gated** stage finish before P2 began — leaving P2 a terminal
closure blocker rather than letting the arc close around it.
- **Q#GA6 — RULED.** Land the TUI answer in Stage 1b; no declared
divergence. The events already arrive and are dropped by the
document-window handler (Stage 1b records the sites).
- **Q#GA7 — RULED.** Full preedit overlay: commit string, caret and
selection range, cancellation, focus-loss cleanup. Not the
commit-string minimum.
- **Q#GA8 — RULED.** Wait for Stage 2. **No temporary Ctrl+wheel
island**, so this arc adds no off-path hardcode (§7).
- **Q#GA9 — RULED.** Implement the grid answer with UAX #14. No
declared divergence.
- **Q#GA10 — RULED.** Preserve **both** surfaces: automatic restore on
the first eligible attach when armed and no explicit target was
supplied, plus the existing explicit command. Stage 4b owns the
paired **save** path too, and three questions remain live inside that
stage — snapshot ownership, save timing, multi-frontend arbitration.
- **Q#GA11 — RULED.** The §3.1 blocker seed stands **unchanged at nine
items**.
- **Q#GA12 — RULED as a deliberate deferral** to Stage 10, taken after
P2 and the multi-window model exist. The existing-object,
no-parallel-registry and optional/off-by-default constraints hold
from now, not from Stage 10.
---
## 10. Sequencing against #227 (git Stage 1)
Settled with the user on 2026-08-10, and recorded here because it
constrains when Stage 0 may start:
1. **Revision 3 → approval.**
2. **The approved framing and the Stage 0 lane are committed and
pushed** on the Stage 0 branch, as its first commit. Until that
happens this document is not portable and nothing downstream is
safe to rely on.
3. **#227 is finished and merged** before Stage 0 implementation.
4. **Stage 0 rebases and performs the absorption**, which by then
includes **#227's own newly merged lane** alongside the five already
enumerated.
The reason #227 goes first rather than riding alongside: its ref is
**72 `main` commits behind**, and it touches `COHERENCE.md`,
`docs/active-work.md` and `builtin/runtime/listview.lua` — the three
files Stage 0's absorption rewrites. Carrying it across the arc would
compound exactly the conflicts Stage 0 exists to retire.