Introduce a reserved left gutter column with absolute line numbers in the
TUI/grid frontend — the foundational piece of the UX arc (docs/ux-arc-
framing.md). Default OFF (Emacs tradition), so zero layout/coordinate
change until a window opts in.
- window.rs: LineNumberMode { Off, Absolute } + per-window `line_numbers`
field + `gutter_width()` (digits(line_count) + PAD) + `decimal_digits`.
- editor.rs: the gutter is one viewport shift at the paint site
(cell_origin.col += gutter_w, cell_size.cols -= gutter_w) — every
viewport-relative painter (text, syntax, diag underline, search) stays
gutter-agnostic. The sites that read rect.origin.col directly get a
manual +gutter_w: cursor placement, local selection, mouse hit-test
(a gutter click maps to line start, Q#UX6). paint_line_number_gutter
writes right-aligned dim digits alloc-free.
- overlay_paint.rs: remote-presence cursor/selection shift by gutter_w.
- Lua: pmacs.window.set_line_numbers/line_numbers +
window.toggle-line-numbers command.
No protocol/daemon change (frontend-local, Q#UX1). Tests: gutter render
(right-aligned digits + past-EOF blanks) + decimal_digits.
Validated: fmt clean; clippy --lib clean both flavors; 1439 lib tests pass
both flavors. Needs a human eyeball (coordinate-math change) before the PR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
The wiring that makes the menu and OS clipboard work end-to-end. The
protocol bump touches every exhaustive match on the wire enums, so the
daemon / frontend / GPU consumers all land together.
Protocol v11 (additive; SUPPORTED = [6..11]):
- `PointerKind::Context` (right-click), `FrontendEvent::MenuPointer`
(GPU->daemon navigation, index-only), `InstanceMessage::MenuPrompt` +
`MenuPromptRow` (daemon->GPU rows + highlight, daemon-gated >= 11).
Dispatch + producer:
- `EditorState`: menu interception in `dispatch_key`/`dispatch_mouse`,
`MenuKey`, `dispatch_menu_key`/`_mouse`, `open_context_menu` (TUI) /
`open_menu_at_byte` + `dispatch_menu_pointer` (GPU), `build_menu_rows`
(calls the Lua resolver), `dispatch_idle` now false while a menu is
open. `dispatch_pointer` gains the `Context` arm.
- daemon: routes `Context` -> open, `MenuPointer` -> navigate; gates
`MenuPrompt` >= 11; drains the clipboard publish as
`InstanceSignal::Clipboard`; honors the previously-dropped
`FrontendEvent::Paste` (so paste works for the first time).
- `semantic_render`: `MenuPrompt` producer with cached-compare.
Frontends:
- TUI (`frontend.rs`): OSC 52 clipboard write; ignores `MenuPrompt`
(the cell overlay renders the menu).
- GPU (`pmacs-gpu`): `arboard` dep; clipboard write/read + Ctrl-V inbound
paste; right-click -> `Context`; `MenuLocal` + `MenuPrompt` handler;
the popup (a second `TextRenderer` over bg quads) at the click pixel;
hover/click -> `MenuPointer`; key intercept while open.
Also folds a pre-existing clippy `unnested_or_patterns` nit in a search
test (`Color::Indexed(11 | 3)`) that newer CI clippy surfaced.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
Wires regex matching into the search session and the terminal
frontend.
SearchSession gains `regex` and `invalid` flags. search_begin takes a
`regex` argument; recompute dispatches find_all_regex (regex) vs
find_all (literal), recording `invalid` when the pattern won't
compile (an invalid pattern clears the matches and shows [invalid]
rather than a stale count). search_toggle_regex flips the mode and
re-runs the current query.
Input: C-M-s / C-M-r start a regex search (search.forward-regex /
search.backward-regex commands → ed.search_start(forward, regex)).
M-r toggles literal <-> regex mid-search — a new SearchKey decoded in
dispatch_search_key, so it works the same in both frontends (the GUI
already round-trips every key while searching). The TUI prompt reads
"Regex I-search:" in regex mode and "[invalid]" when the pattern
won't compile.
Multi-line: SearchView now washes each row a match spans, mirroring
paint_local_selection's per-row clip (newline excluded so a spanning
match doesn't paint a phantom trailing cell). Single-line matches —
every literal match — touch exactly one row, unchanged. The GPU
already fans multi-line ranges per-line, so it needs no change here.
Tests: regex match / smart-case / invalid-flags-and-recovers /
toggle-reinterprets-query (core); C-M-s starts regex + M-r toggles
mid-search (dispatch); multi-line per-row wash (SearchView render).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix for "seems to only search for the first character." The TUI's
`SearchView` overlay was written (commit 2) but never attached to a
window, so the terminal frontend painted no match highlights — the
only feedback was the cursor jumping to the first match, which made
refining the query past the first character look like a no-op even
though the search was working (verified: the query accumulates
correctly through the full run-loop path).
`search_begin` now attaches a `SearchView` to the active window
(deduped by overlay kind, so repeat searches don't stack it). The
view self-suppresses when the store has no matches or is stale, so a
persistent attach is safe — it paints only while a search has live
matches and stops the moment an edit invalidates them.
`SearchView` now keys on the *rendered* buffer (`Buffer::id`) instead
of a fixed id captured at construction, so one attached instance
keeps highlighting correctly even if the window later switches
buffers (the store is per-buffer; a buffer with no entry paints
nothing).
Tests: a render-level test that paints a real frame mid-search and
asserts both the match wash (bright `Indexed(11)` on the active
match) and the full `I-search: foo` prompt land on the grid — the
coverage that was missing, which would have caught the unattached
overlay. Plus a run-loop-fidelity test (renders interleaved with
keystrokes) pinning that the query accumulates rather than sticking
at the first character.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings incremental search to pmacs-gpu, which has no minibuffer, by
reusing the shared daemon-side search core from the previous commit.
Key routing needs no new mechanism: `dispatch_idle` now also reports
false while a search is running, so the GPU's existing M11.6
optimistic-apply gate round-trips every keystroke to the daemon —
where `dispatch_search_key` extends the query / steps — instead of
self-inserting it. The match highlights were already wired (commit
2's SearchMatch / SearchMatchActive decoration colors), so they
light up live the moment keys round-trip.
The one thing a semantic frontend can't derive locally is the query
text, so a new additive `InstanceMessage::SearchPrompt { buffer_id,
query, active, total }` carries it (protocol v9, SUPPORTED grows to
[6,7,8,9]). The producer emits it cached-compare-suppressed like
StatusFacts — `query: Some` while searching, `None` to clear on
accept/cancel (matches keep highlighting via decorations), and
stays silent on a fresh buffer that never searched. The daemon's
per-session filter keeps the variant off wires negotiated < 9. The
GPU mirrors it into the status band: while searching, the band's
left side shows `I-search: <query> (n/m)` (or `[no match]`) in
place of the buffer name, returning to the name when the search
ends.
Tests: protocol version pin + SearchPrompt postcard round-trip
(active / failing / cleared shapes); producer emit-on-change +
suppress + clear-on-accept + first-sight silence; dispatch_idle
flips false during search (the GPU round-trip contract).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the live-typing half of in-buffer search for the terminal
frontend, on a frontend-agnostic core so the GPU (next commit) can
share it.
EditorCore gains a `search: Option<SearchSession>` (query + origin
cursor + direction) and the `search_*` methods that drive it:
begin records the origin, input_char/backspace re-run `find_all`
against the origin buffer and refocus the match nearest the origin
(failing searches anchor the cursor back at the origin), step walks
the store's active match (wrapping, also usable post-accept), and
finish either keeps the cursor + matches (accept) or restores the
origin and clears them (cancel). The matches live in the shared
`search_store`, so the decorations producer and the TUI SearchView
light up live as you type.
Input routing is intercepted in `EditorState::dispatch_key`: while
a search runs, every key flows through `dispatch_search_key`
(SearchKey::from_chord) instead of the global keymap — printable
chars extend the query, C-s/C-r (and Down/Up) step, RET accepts,
C-g/Esc cancel, BS shortens. This is the same dispatch path the
daemon's `FrontendEvent::Key` round-trip uses, so the daemon-side
search already works; the GPU just needs to route keys + show the
prompt (commit 4). The TUI paints an `I-search: <query> (n/m)`
prompt on the bottom row while keeping the terminal cursor in the
buffer at the active match.
C-s / C-r start the search (search.forward / search.backward Lua
commands → ed.search_start). Both keys were free in the default
map (save is C-x C-s, redo is C-x r), so isearch lands without
disturbing the CUA / Emacs editing keys — no cursor.right rebind
needed (the framing doc had flagged C-f for veto; C-s is cleaner
and Emacs-faithful).
Any edit now marks the buffer's matches stale in apply_active_edit
(M11.8), closing the headline "stale-after-edit linger" bet:
accepted highlights vanish the moment the text they described
changes, rather than painting at wrong offsets.
Tests: EditorCore-level (begin/type/step/wrap/focus-from-origin/
cancel/accept/backspace/smart-case/stale-on-edit) and dispatch-
level acceptance (C-s drives the whole loop; Esc restores; query
keys never self-insert).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PointerKind::TripleDown — the cheap additive bump shape returns:
PROTOCOL_VERSION 7, SUPPORTED [6, 7], the new variant kept off
pre-v7 wires by a frontend send-gate that downgrades it to the
plain Down a third click produced before. The GPU's click history
deepens to a chain count (1 → Down, 2 → DoubleDown, 3 →
TripleDown, then restart). Daemon side, select_line_at_cursor
selects the line including its trailing newline, so consecutive
triple-click lines abut.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dispatch_pointer consults the mods it has carried since v5: a Down
with SHIFT keeps the existing anchor (or, with no selection,
anchors at the pre-click cursor) and only moves the cursor — the
universal extend convention. Zero wire change. Frontend-side, a
Shift-click neither advances nor inherits the multi-click chain,
so two Shift-clicks can't become a word select.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mode-line counts (a0bd4d7) took the diag-store mutex before the
window loop and held it through overlay rendering. DiagnosticView —
attached as a window overlay the moment a file with an LSP opens —
locks the same mutex in its render, and std's Mutex is not
reentrant: the daemon's main loop deadlocked on the first frame
after C-x C-f, unresponsive even to SIGINT (parked in futex_wait,
confirmed on the live process). No render test attached a
diagnostic overlay, which is how it slipped through.
The lock is now scoped to the per-window summary computation, after
overlays have rendered and released it. Regression test renders the
full paint_frame path with a real DiagnosticView attached.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
M4.6 follow-up piece 1: the mode line's right segment now shows
error/warning counts for the window's buffer, computed from the
shared diag store at paint time. Counts are suppressed while the
URI's diagnostics are stale (mid-edit, pre-publish) so the readout
never describes text that no longer exists. Info/hint severities
stay off the mode line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per docs/pmacs-gpu-mouse-framing.md (resolves the deferred Q#B5):
a pixel frontend cannot express the daemon's cell coordinates —
inline adornments shift visual columns invisibly to cell space and
the design contract forbids hit-test round trips — so the frontend
hit-tests locally and ships source-byte gestures.
- protocol v5: FrontendEvent::Pointer { buffer_id, byte, kind, mods }
with PointerKind { Down, Drag, Up, DoubleDown }. Double-click
detection is frontend-side (only it knows pixel proximity).
SUPPORTED_PROTOCOL_VERSIONS gains 5; the send gate runs in the
frontend (an older instance cannot decode the variant).
- daemon: dispatch_pointer replays the existing mouse gesture
semantics in byte space against the semantic session's window —
Down places + anchors, Drag grows, Up collapses an empty click,
DoubleDown selects the word. Routed by the authenticated source
(CrdtOp/Viewport trust rule); hit bytes clamp + snap to UTF-8
boundaries (a hit can race an in-flight edit).
- word_range_at fix (pre-existing CUA bug the new test surfaced):
double-clicking a word's FIRST character selected the previous
word too — backward_word from pos sees the non-word char behind
the hit and crosses over; walk from pos + ch_len instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- S-<arrows>/S-<home>/S-<end> (+C-S word/paragraph variants) extend a
selection; the TUI grid paints it reverse-video; double-click
selects the word at point.
- Backspace / Delete consume the active region (delete_region first,
falling back to single-codepoint semantics).
- Typing replaces the region: buffer.self-insert / newline / tab
delete_region before inserting. pmacs-gpu cooperates by
round-tripping keys while an own-window selection is active, so
the region-aware commands run instead of a raw optimistic op.
- tests/cua_region_acceptance.rs drives the real dispatch path:
select -> BS/DEL/char/Enter, plus the no-region fallbacks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The attach-mode optimistic-apply layer (M10.10) classifies any
plain-char keystroke as `Insert(c)` and applies it directly to the
local CRDT mirror, bypassing the daemon's keymap dispatcher. The
documented limitation ("the optimistic layer doesn't track keymap-
prefix state") also covered the minibuffer-active case, which
surfaced during session-5 manual validation: characters typed into a
`C-x C-f` prompt were optimistically inserted into the previously-
active document instead of routed to the minibuffer.
The fix is a daemon→frontend wire signal indicating whether the
daemon's *next* key event would be intercepted (minibuffer or pending
prefix) vs would self-insert. The frontend gates the optimistic-apply
path on this; when not idle, every keystroke round-trips as
`FrontendEvent::Key`.
Protocol changes (pmacs-protocol):
- `PROTOCOL_VERSION` 3 → 4; `SUPPORTED_PROTOCOL_VERSIONS` adds 4.
- New `InstanceMessage::DispatchIdle { idle: bool }`.
Daemon (`src/editor.rs`, `src/daemon.rs`):
- `EditorState::dispatch_idle()` — true iff `dispatcher.pending`
empty AND `minibuffer.is_active() == false`.
- Per-tick emission: `last_dispatch_idle_sent: HashMap<FrontendId,
bool>` tracks the last-broadcast value per session; emission fires
on first frame after attach (absent entry) and on transitions.
- Gated on `crdt_replica` AND `negotiated_protocol_version >= 4` so
older peers don't hard-error on the unknown variant. Same gating
shape as the M10.5 CrdtOp and M11.1 SemanticFrame bumps.
Frontend (`src/attach.rs`):
- New `dispatch_idle: bool` (cfg `crdt`); default `false`
(pessimistic — optimistic apply only activates after the daemon
explicitly says idle).
- DispatchIdle messages consumed in the drain loop; they don't
participate in `present_messages` batches.
- Optimistic-apply branch gated on `dispatch_idle`. When false, the
branch returns false (forces fallthrough to the round-trip
`forward_event` path).
Tests:
- `editor::tests::dispatch_idle_*` — fresh, prefix-pending, prefix-
resolved, minibuffer-open/cancelled.
- `protocol::tests::dispatch_idle_round_trips_through_postcard` —
wire encoding both polarities.
- `protocol::tests::protocol_version_is_four_for_dispatch_idle` +
`supported_protocol_versions_includes_one_through_four` — pin the
new version constants.
Gates: cargo fmt + clippy (workspace, with/without `crdt`) clean;
lib 1474 (+5 from 1469 baseline) with crdt; 1312 (+4) without;
m4 83; m11_5 (--features crdt) 2.
Acknowledged remaining gap: plain-char Lua bindings (e.g. binding
`q` to a command) still surface optimistic-apply divergence —
optimistic doesn't know "is this char bound to a non-self-insert
command in the current keymap." Rare in practice; revisit if anyone
hits it. Documented at session-5 finding time.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Closes the visible "C++ has no syntax coloring in the grid TUI" gap.
Sibling of SyntaxHighlightView: a View impl that paints LSP semantic
tokens as cell styles, attached for buffers with no bundled
tree-sitter grammar. Same policy A (one styling authority per buffer)
the semantic-frontend producer arc enforces, applied to the grid
renderer the user actually uses today.
Mechanics: every render re-derives the buffer's URI from
buf.file_path() and pulls (encoding, legend) via the existing
LspManager::semantic_style_context plus tokens via for_uri. Per
visible line, tokens are converted from LSP encoding units to byte
ranges via char_to_byte, then to display columns via the existing
byte_range_to_display_cols (UTF-8 + tab aware). Theme::lookup
resolves token type names through the same dotted-prefix mechanism
the tree-sitter capture names use, so "function", "variable",
"type", "keyword" land on the existing theme vocabulary with no new
style names. Default-styled spans skip the per-cell loop, matching
SyntaxHighlightView's short-circuit.
Wiring: pmacs.lsp._attach_style binding pushes the overlay on the
active window (mirrors pmacs.parse._attach_highlight). install_lsp
and make_lsp_manager take SharedSyntaxRegistry so the binding can
hand the LspStyleView the shared ThemeHandle; editor.rs caller
updated. builtin/runtime/lsp.lua's attach_buffer attaches the view
when pmacs.parse.language_for_path returns nil (grammar-less
signal), dedup'd via a styled_buffers set that mirrors syntax.lua's
highlighted_buffers.
Test: lsp_style_view_paints_cells_from_semantic_tokens — seeds an
Initialized fake LSP client (using the cfg(test) helper from the
producer arc) on a /tmp/x.cpp buffer with one token, asserts the
expected cells are styled per the theme face and the cell just past
the token range is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lays the groundwork for WorkspaceEdit/rename (L2+) by making
navigation cross-file-correct.
- Relocate file_path/file_meta from the EditorCore global onto
Buffer itself, so each buffer keeps its own filesystem identity
across cross-file navigation. Accessors + registry/editor/lua/
semantic_render call sites migrated; zero behavioural change for
single-file flows.
- uri->path: project_index::uri_to_path made pub; pmacs.lsp.path_for_uri.
- find-or-open: BufferRegistry::find_by_path + pmacs.buffer.find_or_open
dedups an already-open file instead of spawning a duplicate buffer
(SP-4 Gap A).
- Bounded jump ring on EditorCore (cap 64, oldest-evict, stale-buffer
skip): push_jump/jump_back + pmacs.editor.* bindings + lsp.jump-back
command bound to M-,.
- pmacs.lsp.go_to_definition cross-file branch: decode URI ->
push_jump -> find_or_open -> reposition, with a failure path that
unwinds the pushed origin. ensure_server now passes cfg.env through.
Tests: 5 jump-ring unit tests; m4_12_cross_file_go_to_definition_and_
jump_back end-to-end via a new `defenv` fake-LSP mode. All gates green
(lib 1262/0, m4 67/0, m8_1/m8_9/m8_10, m9_1, m11_5 --features crdt 2/0).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The async bridge settles awaiters inside `tick_lsp`/`tick_mcp` by
posting to the message bus; `tick_async` drains that bus and resumes
the parked coroutine. With `tick_async` running *first* (historical
accretion from M3.3, predating processes/LSP/MCP), every LSP/MCP
`:await()` resumption was deferred a full frame: the response
absorbed in frame N's `tick_lsp` wasn't observed until frame N+1's
`tick_async` (~33ms structural floor @ 60Hz, plus a render frame).
Reordering both production loops (`editor::run` and the daemon loop)
to `processes → lsp → mcp → async` makes settle→resume happen in the
same frame, halving the floor to one frame. The only documented
ordering invariant — `tick_processes → tick_lsp → tick_mcp` for
same-batch supervisor I/O — is preserved; settle (bus post) and
resume (bus drain) are bus-decoupled, so the move cannot regress
correctness in either direction.
Acceptance tests open-code their own per-test tick orders and never
drive `editor::run`, so none covered production ordering. Added
`m4_5_await_resolves_same_frame_as_response_absorbed`, which drives
the exact production order and asserts the awaited request resolves
in the same frame its response is absorbed (absorbed_cycle ==
done_cycle); it fails if anyone reverts to `tick_async`-first.
Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1223/0; m4_acceptance 59/0; m9_1 18/0; m8_1/m8_9/m8_10 green
(SP-7 outline-aggregate "one async tick" pin unaffected).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the editor-blocking `poll_until` tick-loop in the LSP UX
with the M9.1 external-settle pattern: each `textDocument/*` request
registers a pending entry via `AsyncRuntime::register_external` and
returns the job id; the JSON-RPC response (or a server-teardown /
cancel / timeout) settles it, resuming a `Handle:await()` coroutine.
No worker thread is occupied for the round-trip.
Hybrid result delivery (operator decision): the response is absorbed
into the typed stores *and* carried through the Handle. The
completion popup and diagnostics gutter keep reading the stores
untouched; request/response command code awaits the value directly.
Core (src/lsp.rs):
- `LspManager` gains `runtime: SharedAsyncRuntime` (threaded through
`make_lsp_manager` / editor.rs, mirroring `make_mcp_manager`) plus
a `(server, request_id)` -> PendingExternal awaiter table parallel
to `pending_routes`.
- `request_*` return the async `JobId` (`= u64`, signature
unchanged; no caller consumed the old JSON-RPC id).
- `handle_response` settles every non-cancelled awaiter ok/failed
alongside store absorption; null result still wakes await with nil.
- Awaiters drain-cancelled at all three `pending_routes` purge sites
(restart generation flip / terminal exit / forget) so a coroutine
cannot park on a server that went away.
- Per-tick sweep: per-awaiter cancellation (Handle:cancel() or
supersede via a stable `lsp:{method}:{sid}:{uri}` key), with
`$/cancelRequest` + `cancelled_rids` on abandonment to drop the
cancel/response race silently. Mirrors mcp.rs.
- Per-request timeout (default 10s, `pmacs.lsp.set_request_timeout_ms`):
an alive-but-silent server fails the await instead of hanging.
Lua surface:
- `_request_*_raw` job-id bindings (mirror `pmacs.mcp._send_request_raw`).
- builtin/runtime/lsp.lua: Handle wrappers + the four commands
rewritten to spawn `pmacs.async` coroutines that `:await()`;
`poll_until` removed. Server-gone / error surface as structured
await failures in the modeline.
Tests:
- pmacs_fake_lsp: `error` / `silent` modes for deterministic
failure-path coverage.
- 5 end-to-end await-path tests (success+store, server-error->failed,
server-stop->cancelled, timeout->failed, supersede->cancelled).
Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1223/0; m4_acceptance 58/0; m9_1_acceptance 18/0 (MCP unaffected).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pulls a set of planned V0.2 prerequisites forward to ship in v1.0,
plus the clean audit-review round over that work.
V0.2-prereq implementations (documented promotions, not M11
surprises; operator raised the v1.0 public-API ceiling to absorb
them — see V0.2-PREREQUISITES.md "v1.0 pull-forward"):
- CC-1: `bypass_intercept` opts on buffer insert/delete/replace —
skips the Lua intercept chain only; preserves the same-buffer
re-entry guard, undo/dirty bookkeeping, view notifications, and
CRDT broadcast queueing.
- CC-2: `pmacs.buffer.on_removed(buf, cb)` + idempotent `:remove()`
handle; buffer-local keymaps pruned on removal. Fires for both
`pmacs.buffer.remove` and `.kill` (incl. interactive C-x k);
callback errors logged to *errors* without failing the removal.
- SP-4: `pmacs.buffer.from_file`.
- SP-5: `pmacs.fs.watch` (polling; `:cancel()`/`:is_cancelled()`).
- SP-7: `pmacs.async.yield_to_next_tick` (worker-free next-tick
yield); outline-aggregate repaint now uses it instead of
workers.sleep(0):await(), pinning propagation to one async tick.
- SP-1: `pmacs.editor.move_to_line` (0-based, clamps out-of-range).
- SP-6: `pmacs.outline.query` published by pmacs-outline.
- SP-3: audit rule 15 `reach-around-require-field` (Info).
- CC-3: runtime API-availability documented (docs-only).
Clean audit-review round (M10.11 framing stop-condition pass):
- Finding 1 (fixed): clippy needless_raw_string_hashes blocked
`clippy -D warnings` on both lanes; raw-string delimiter fixed.
- Finding 2 (fixed): fs_watch acceptance test was racy — the
`pending == 1` gate could not distinguish the in-flight baseline
stat from the steady-state poll sleep, so under load the mutation
raced the baseline (~1/3 fail in the default lane). Rewritten to
re-emit a distinct change each pump iteration; 6/6 on the
previously-failing invocation.
- Finding 3 (fixed): documented fs.watch's async-baseline startup
window and size+mtime-granularity detection limit.
- Finding 4 / SP-8 (logged, non-blocking, out of diff): a
pre-existing PTY-lifecycle test timing flake under severe CPU
oversubscription; src/process.rs untouched here.
CC-1's opts-extension-counts question resolved explicitly
(consistent treatment: counted; ceiling raised to fit).
Gate at normal load, both lanes: fmt clean; clippy --all-targets
-D warnings clean; non-crdt lib 1223/0; crdt lib 1377/0;
m8_1/m8_9/m8_10 green.
Not in scope here: v1.0 CHANGELOG body, version bump, the M10.11
Finding-4 (reattach undo) user-facing artifact, and the recorded
two-laptop manual acceptance — tracked as the remaining v1.0 steps.
.gitignore: M*-FRAMING.md added to the internal-only block for
consistency with the M*-AUDIT.md / M*-SHIP-GATE.md siblings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Land the optimistic local-edit-application layer on top of the M10 CRDT
foundation: frontend-side rope replica with local edit application,
daemon-authoritative broadcast, and bidirectional cursor reconciliation.
Keystrokes feel instantaneous because the local replica answers next-render
queries before the daemon round-trip completes, while the daemon remains
the single source of truth for conflict resolution and broadcast to remote
replicas.
Architecture beats:
- BufferMirror (src/buffer_mirror.rs) holds a per-frontend rope replica
with explicit cursor-staleness tracking. Every event that may move the
active cursor or swap the active buffer marks the mirror stale; the
next CursorByte from the daemon clears it.
- CrdtOpOrigin {OptimisticReplica(FrontendId), DaemonKey} routes broadcast.
OptimisticReplica skips re-application on the originating frontend
(already applied locally); DaemonKey broadcasts to all replicas including
source -- covers Lua-driven and generated-buffer edits that bypass the
optimistic path.
- Generated buffers (*help*, *workers*, *pmacs-instance*, *errors*) funnel
apply_edit output through queue_daemon_origin_crdt_op so post-attach
CRDT upgrades don't drop their edits.
- forbid(unsafe_code) preserved throughout; loro 1.12 added as the CRDT
engine.
Audit posture: M10.10 shipped through six post-audit review rounds with
twenty-eight cumulative findings, most categorized as "incomplete
application of a prior round's mechanism." The audit doc records
grep-driven exhaustiveness as the standing countermeasure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Land the Model Context Protocol (MCP) integration as a transport binding,
not a built-in feature. Six Lua functions plus userdata methods expose
the substance of three MCP feature areas (resources, tools, prompts), a
notification dispatcher, and a non-trivial AI-assistance example package
that meets the architectural ship gate (spec/pmacs-spec.tex:1572): zero
direct calls into the Rust core, zero special-cased MCP handling outside
the public API, source under 2000 lines of Lua.
The M9.5 -> M9.6 -> M9.7 -> M9.8 layered composition validates the claim
"AI is a transport binding, not a feature" -- pmacs-mcp-ai composes with
pmacs-mcp-prompts.render and inherits notification handling transitively
through M9.7's package, demonstrating that the AI domain is a layer
above MCP, not a thread woven through the core.
Subtask shape:
M9.1 stdio transport + initialize handshake + restart policy
M9.2 resources with in-flight + settled cache and per-uri invalidation
M9.3 tools with isError-vs-JSON-RPC-error semantics + cancellation
M9.4 prompts with required-argument validation
M9.5 notification dispatcher (on_notification, off_notification)
M9.6 tools-as-commands fixture package + 12 audit findings disposed
M9.7 prompts-as-result-buffers fixture package + tree-sitter-md grammar
M9.8 AI-assistance fixture package (363+ LoC; 17/17 acceptance tests)
M9.9 formal package audit -- PASS on all three criteria
M9.10 release: TRANSITION-M9.md + MCP-for-package-authors guide
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>