Commit Graph

29 Commits

Author SHA1 Message Date
Levi Neuwirth 85e4ee03bb feat(bootstrap): make the ambient storage roots a parameter
`EditorState::new` resolves two storage roots from the process
environment before it returns: the data root, which the bundled-package
materialization then WRITES into unconditionally (outside every `cfg`
guard), and the config root, from which `init.lua` is read.

The `#[cfg(not(test))]` guard on the second was written to stop the
crate's own unit tests picking up a developer's real `init.lua`. It does
exactly that and nothing more: `cfg(test)` is set only while compiling
the lib's own tests, so an integration test in `tests/` — compiled
without it — reads the real config and writes the real data root. On a
machine with a real `~/.config/pmacs/init.lua`, that is 11 deterministic
failures in `compile_mode_acceptance`, attributed to whatever branch is
checked out.

Tests cannot fix that themselves: `std::env::set_var` is `unsafe` and
this crate is `#![forbid(unsafe_code)]` — the same constraint that
produced `Installer::with_install_root_override`. So isolation arrives as
a parameter.

`BootstrapRoots` names the four storage roots (config, data, state,
cache). `ambient()` leaves every one `None` and every resolution goes to
the environment exactly as today, so production is unchanged.
`new_with_roots` and `open_with_roots` take it — both, because `open`
calls `Self::new()` internally and a constructor-only parameter would
leave every open-path test ambient. `install_state_dirs` consults it
too: it runs after construction, so resolving from the environment there
would reopen the hole the constructor closed.

The redirected branch changes WHICH directory is read, never WHETHER the
block runs. Config loading shares one conditional with
`set_init_complete()`, and `tests/m8_2_acceptance.rs:75` documents its
dependence on integration-test construction finishing init-complete.

`child_env()` translates the same value into the environment a spawned
`pmacs` needs. Five variables, not four: `PMACS_STATE_HOME` outranks
`XDG_STATE_HOME`, so a child given only the XDG four still resolves an
inherited state override — invisible on a machine that exports none.

The `src/editor.rs` comment claimed a protection it does not provide and
said nothing about the write above it; both are corrected in place. The
guard is deliberately NOT widened to cover integration tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 18:48:07 -04:00
Levi Neuwirth 313b1ff77a feat(fold): Stage 2 — grid (daemon-rendered) collapse
Implements docs/folding-stage2-framing.md rev 4. The daemon grid
renderer now consults the fold store: hidden lines are omitted, rows
below shift up, and every consumer that assumed
`display_row = source_line - view_top` routes through one shared
projection. No wire schema change and no protocol bump (Bet B6) — the
collapse is entirely daemon-side; the GPU path is Stage 3.

The spine (Q#FD12) is `src/fold_view.rs`: a `VisibleLineMap` derived
from `FoldRegistry::folds` plus a window's line offsets and never
stored. Its unit is a merged **hidden component** — overlapping OR
adjacent hidden intervals unioned, each keeping the one visible
`head_line` and that line's exact `head_position`. Adjacent intervals
merge because the later fold's head is itself hidden, which is what
makes nesting, shared heads, and crossing overlap all resolve to a
head that can actually render (round-3 F2).

Instances are short-lived and built **per rendered window** and **per
command/event operation**, never once per frame: `paint_frame` renders
several windows that may show different buffers, so a singleton would
leak one pane's folds into another (round-2 F2). The render instance
rides on a lifetime-bearing `Viewport<'a>` as `Option<&'a
VisibleLineMap>` — a shared ref is `Copy`, so `Viewport` stays `Copy`
(Bet B7).

Rendering:
- `TextView::render` walks visible lines; the head line gets a
  trailing content-area ellipsis (Q#FD13).
- The gutter walks visible lines too: Absolute keeps the raw `line+1`,
  Relative/Hybrid measure VISIBLE distance anchored on the cursor's
  visible head (Q#FD14). The fold glyph takes the col-0 sign cell only
  when a gutter exists — line numbers default to Off, so with no gutter
  the ellipsis is the sole marker (Q#FD20, round-1 F3). A diagnostic
  clamped onto the head wins that cell by paint order.
- A diagnostic on a hidden line clamps its SIGN to the outermost
  visible head (most-severe merge); the squiggle needs a real row, so
  only the sign clamps (Q#FD15).
- Caret, local selection endpoints, and peer cursors project via
  `visible_position_of` — the head row AND the head's end-of-content
  column, never an arbitrary column (round-2 F3). Peer presence derives
  the RECIPIENT window's map.
- Style/search/completion overlays route through
  `Viewport::row_offset_of`; the mode-line indicator reckons in
  visible-line space.

Command/event time is scoped per frontend (Q#FD21): a
`fold_projection` flag on `FrontendView`, set at attach from the
negotiated `semantic_render` bit (grid ⇒ true, semantic ⇒ false until
Stage 3, LOCAL ⇒ true) and never inferred from a `FrontendId` (Bet
B8). Without it, shared `EditorCore` motion would make a simultaneous
unfolded GPU session's cursor skip lines it still displays. The map's
two axes stay separate (round-3 F1): the acting frontend supplies the
policy, the operation's TARGET window supplies the buffer — a wheel
event names a pane without activating it.

Motion (Q#FD17, ruled: include), paging, wheel, the click inverse, and
the auto-scroll clamp all step by visible lines under that gate;
motion from a hidden logical cursor normalizes to the visible head
first. `view_top` stays a source-line index (Bet B5), set only via
`clamp_view_top` so it never rests hidden.

Unfold widening (Q#FD19): the pre-edit unfold moves to the top of
`apply_active_edit` — one funnel that subsumes the six primitives'
calls and covers yank + query-replace, both of which place point at
the edit site first. Interactive Lua mutators hook the common
`run_buffer_edit`, above the managed/bypass split, gated on
`InteractiveCommandOrigin` AND the edit targeting that frontend's
active-window buffer. The remote/optimistic-CRDT path stays excluded
(Stage 3); undo/redo unfold stays deferred.

Acceptance: `tests/folding_stage2_acceptance.rs`, 35 tests asserting
on the real `paint_frame` cell grid, covering framing items 1–14
including crossing folds, a nested deeply-hidden cursor, a split of
two different buffers with an inactive-pane wheel, and simultaneous
grid+semantic motion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
2026-07-23 19:24:07 -04:00
Levi Neuwirth 3b411dbb2a feat(fold): Arc 6 Stage 1 — instance fold engine (headless)
The fold engine behind `docs/folding-framing.md` (approved rev 5): a
per-buffer fold store, a structural tree-sitter fold source, the
state-aware Lua command + data-API surface with the Emacs hideshow
`C-c @` bindings, the dispatch-layer pre-edit unfold, and `FoldState`
production. No rendering — Stages 2 (grid) and 3 (GPU) consume the store.

- `src/fold.rs`: `FoldStore` (a buffer-attached `View` that translates
  ranges on every edit and drops any whose head/tail the edit crosses,
  provenance-blind — Q#FD6), the structural source (nearest block-like
  node >= 2 source lines -> introducer<->body -> **derived head line**,
  the line immediately above the first hidden line, so wrapped signatures
  and `where` clauses stay visible per R3-1 -> **closer-aware tail**, a
  closing-delimiter line stays visible per R2-5), injection-layer walk,
  `(start, end]` containment, and the state-aware ops (close innermost
  open / open outermost closed / org-TAB cycle). Stale/absent tree
  refuses (Q#FD10).
- `src/lua_bindings/fold.rs`: `pmacs.fold.*` — explicit-buffer data API
  (`fold`/`unfold`/`folds`/`toggle`) + interactive helpers, validation
  (Q#FD11: document buffer, UTF-8 boundaries, >= 1 hidden line — Q#FD9
  falls out of the last clause), point-moves-to-head (Q#FD3).
- `builtin/runtime/fold.lua`: `fold.toggle/close/open/close-all/open-all`
  commands + the `C-c @` prefix set (Q#FD4).
- `src/editor_core.rs`: the six point-anchored edit primitives run the
  pre-edit unfold keyed on the authenticated source's point (Q#FD5,
  command path); `EditorCore` owns the shared `FoldRegistry`.
- `src/semantic_render.rs`: the `FoldState` producer —
  authoritative-empty, diff-suppressed, baseline resets on
  `BufferSnapshot` (Q#FD8); the "never emitted" pin split so
  `BlockAdornments` stays unproduced.
- `tests/folding_acceptance.rs` (16) over real Rust/Python/markdown
  grammars + `fold_state_producer_transitions` + 15 engine unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
2026-07-23 12:14:00 -04:00
Levi Neuwirth 9f7bc77f44 feat(render): unify tab-width projection
Share one fixed eight-column tab-stop contract across core and GPU renderers. Consolidate byte-to-display-column accounting, expand GPU code tabs with source provenance, align caret/hit/decoration geometry, and refresh minimap projection on edits.
2026-07-22 15:03:30 -04:00
Levi Neuwirth 6844262495 feat(config): typed configuration registry with buffer-local scope
A third registry beside CommandRegistry and HookRegistry, per
docs/config-registry-framing.md. Unblocks the per-buffer auto-pair
toggle, the first of the five backlog items the missing config surface
was gating.

Substrate (src/config_registry.rs):

  * ConfigRegistry keyed by name with definition order preserved, R42
    mandatory descriptions, R50 typo detection, duplicate rejection,
    and SourceLocation provenance -- the command/hook vocabulary.
  * Closed scalar kinds: boolean, integer, number, string, enum. Owned
    Rust values; Lua tables, functions and userdata are never stored.
    Integer exactness is checked by value, never math.type, so the
    luajit and lua54 builds agree.
  * Two scopes. get(name, buf) resolves buffer-local -> global ->
    default; get(name) with no buffer resolves the global chain only
    and never consults an ambient buffer. Buffer-locals live in a
    registry-owned side table purged at after_buffer_removed, beside
    the keymap purge already there.
  * An override is ALWAYS stored, even when equal to the value it
    shadows; only value_epoch and listener dispatch key on effective
    change. Without this a buffer pinned to the current value stores
    nothing and a later global set flips it -- the pin silently never
    existed. equal_valued_local_override_is_still_stored_and_shields_buffer
    fails against the naive reading.

Bindings (src/lua_bindings/config.rs):

  * define/get/set/set_local/reset/is_set/describe/list/on_change.
    Spec tables are read raw, so neither an unknown key nor a
    metatable-provided value can smuggle a field in.
  * Listeners commit inside the borrow, snapshot, drop the borrow, and
    only then re-enter Lua -- verified by holding the borrow and
    watching the test panic with "RefCell already borrowed". A raising
    listener is logged without blocking later ones or rolling back, and
    a depth bound turns an accidental cycle into a pointed error.
    Listeners persist until explicitly disposed; there is no Gc path,
    matching the rest of the codebase.
  * StartupOnly freezes off the existing InitCompleteFlag at write
    time, so this arc adds no editor.rs call at all.

Adopters, each defining its own key so SourceLocation names the owning
module: editing.auto-pair (pair.lua, read per-buffer against the typed
edit's SOURCE buffer), editing.trim-on-save (editops.lua),
autosave.interval-ms (autosave.lua). No public function is removed or
deprecated, and both migration wrappers keep their legacy coercion --
trim_on_save("yes") still enables, interval_ms(1500.7) still floors to
1500 -- coercing before handing the strict registry a conforming value.

M-x describe-setting renders into *help*, modeled on describe-command.

Framing revision 3 records four defects implementation found in the
document itself: acceptance 30 and 31 contradicted each other; the
planned builtin/runtime/config.lua had nothing to hold and would have
broken the source-location contract had it held the one helper it might
have; F5 asked define to police a call it cannot see, moved to
set_local; and list() ordering was underspecified.

No protocol change; SUPPORTED stays [6..18]. No wire surface. Zero
changes to src/editor.rs.

Gates: fmt, clippy -D warnings, --lib (1683), --lib --features crdt
(1857), the new config_registry_acceptance (13) plus auto_pair (45),
editops (72), autosave (29) and m9_6 (25), m4 --skip basedpyright
(114), PMACS_REQUIRE_GPU=1 pmacs-gpu (109), the lua54 backend build,
and the full workspace sweep (2795 tests, exit 0). git diff --check
clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 18:29:18 -04:00
Levi Neuwirth bbc1f33a7c feat(vterm): add Stage 1 terminal core
Add compatibility-preserving full-screen ANSI operations, the bounded terminal
screen and input encoders, and a transactional TerminalManager owning one
read-only identity buffer, PTY process, and screen per session.

Drain terminal-owned process events before process.after-tick, retain exact
final output and PID/outcome annotations, reap killed buffers and shutdown
children safely, and enforce buffer-owned read-only checks across ordinary,
host, undo/redo, and CRDT mutation paths.

Cover split parser and grapheme boundaries, screen/reflow/history invariants,
device responses, lifecycle cleanup, and a real adversarial alternate-screen
PTY. Record the fully gated Stage 1 delivery and downstream TUI/GPU contracts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-21 14:20:28 -04:00
Levi Neuwirth 4b65b9e1e5 feat(statusline): add composable modeline segments at protocol v18
Add the strict pmacs.statusline provider registry, deterministic
borrow-released per-window evaluation, context-scoped failure latches,
and a pure built-in LSP provider.

Preserve the legacy TUI modeline while composing faced custom runs,
and append authoritative complete StatuslineSegments replacements for
semantic frontends. Expand dynamic ThemeFacts, reset producer/frontend
baselines symmetrically, and gate all provider work off protocol v18.

Teach the GPU to atomically validate, resolve, shape, clip, and cache
custom modeline runs without displacing the protected status suffix.
Document the public Lua lifecycle, wire ownership, snapshot semantics,
and the fully gated Arc 4 stage-3 delivery state.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-21 12:01:25 -04:00
Levi Neuwirth 661b4968d9 feat(font): FontFacts wire + daemon half (v17, FontPref, producer)
Protocol v16->17: InstanceMessage::FontFacts { family,
size_centi_px } appended after ThemeFacts (integer hundredths of a
logical pixel -- the enum derives Eq, f32 cannot; range 600..=7200
documented on the wire). Pins updated: version 17, ladder accepts
6..=17 rejects 18, FontFacts round-trip (populated + all-None), and
a ThemeFacts byte pin ([23, 0]) guarding the appended placement.

Daemon half: FontPref { family, size_centi_px, epoch } behind a
shared handle on EditorState, installed with the new pmacs.gpu Lua
module BEFORE load_user_config so init.lua set_font lands in the
state the first attachment reads. set_font is strict plain data
(raw_get, unknown raw keys rejected by name, metatables never
consulted, parse/validate/quantize fully before locking -- range-
check the ORIGINAL value so 5.999 errors, then nearest-hundredth
round); pmacs.gpu.font() returns a fresh quantized table.

Producer: font_facts_msg (the theme_facts_msg discipline --
Option-seeded epoch + payload baselines, advance on computation,
one authoritative send per attachment incl (None, None),
bufferless so on_buffer_snapshot_sent never touches it); for_peer
gains peer_knows_font_facts (>= 17); daemon write-loop skip arm;
TUI silent-drop arm + regression test; first-frame count test now
expects 6 messages. GPU application follows in the next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
2026-07-18 11:45:51 +01:00
Levi Neuwirth ec42526652 feat(persistence): autosave + crash recovery (Arc 3 phase 3)
Framing: docs/autosave-recovery-framing.md (Q#AS1-11). Closes the
persistence arc. Every modified file buffer is periodically written to a
private recovery copy; if pmacs dies, the next session says so and
`M-x recover-file` installs it. Emacs's auto-save-mode + recover-file.

Hybrid, forced by the same two gaps as phase 2: Lua has no per-buffer
path getter and FileMeta is neither Lua-visible nor serde. Rust owns the
sweep and the external-change guard; Lua owns cadence, config, and UX.

src/autosave.rs (new):
- One atomic envelope per recovery: a JSON header line + `\n` + raw
  buffer bytes. Split at the FIRST newline, so contents may hold newlines
  and non-UTF-8. A crash can never leave a torn header/contents pair.
- `origin` is NULLABLE: a `[new file]` buffer (a path with nothing on
  disk) has no FileMeta, and its unsaved contents are exactly the work
  most worth recovering.
- status(): Fresh / Stale / Corrupt / None. Only Fresh is announced;
  Stale (file changed, deleted, or created underneath us) is never
  auto-offered; Corrupt is typed, quiet, and discardable.
- sweep(): all modified file buffers, skipping clean/scratch and those
  unchanged since their last copy. The skip cache is keyed
  BufferId -> (path_hash, revision), not revision alone: a buffer keeps
  its BufferId across a path change (LSP WorkspaceEdit rename), so a
  revision-only cache would skip the write and orphan the old key.
- pending(): enumerates ALL open file buffers in Rust, which is what
  covers argv `[new file]` buffers -- they fire no hook at all.

Private storage (Q#AS11, a precondition for default-on): autosave stores
unsaved FILE CONTENTS, not metadata. New `file_io::save_atomic_with_mode`
sets the temp's mode BEFORE the rename (a chmod-after-write leaves a
window where the file is 0644), and `state::write_private` creates the
dir 0700 and the file 0600. Plus `state::read_bytes` (state::read is
read_to_string, which non-UTF-8 buffer contents would fail).

builtin/runtime/autosave.lua:
- Cadence is `process.after-tick` + monotonic_ms, NOT workers.sleep: a
  long sleep parks one of only `available_parallelism - 1` pool threads,
  and re-reading the interval each tick makes it live-reconfigurable.
- pmacs.autosave.interval_ms([ms]) -- validated getter/setter following
  the async_config.frame_target_ms shape. Default 30000, floor 1000.
  pmacs.autosave.enable(on). On by default.
- Notify, never prompt: `after-load` only raises a flag; the tick emits
  ONE aggregate message ("3 files have autosave recovery"). A modal
  prompt from after-load would stack N modals during a desktop restore.
- recover-file confirms, pins to the origin buffer, replaces contents,
  then explicitly fires `buffer.after-edit` -- the mutators only notify
  windows and queue CRDT, and after-edit comes from dispatch_key's
  post-command check, which the minibuffer shadow returns before. Without
  the explicit fire, LSP didChange and the syntax reparse never see the
  recovery. discard-recovery deletes a copy (including a Corrupt one).
- Cleanup: after-save discards; per-buffer on_removed discards on kill
  (there is no global kill hook); before-quit does one final synchronous
  sweep and never vetoes.

src/hash.rs (new): one pub(crate) sha256_hex, shared by desktop, autosave,
and packages::fetcher -- which had two private duplicates (Q#AS9).

Not daemon-gated (unlike desktop-save): autosave is per-buffer, not
per-frontend, and a daemon holds the unsaved work.

Tests: 8 autosave units + 13 state/hash units + tests/autosave_acceptance
(15): sweep round-trip, non-UTF-8 envelope, [new file] null-origin
Fresh->Stale, 0600/0700 perms, skip clean/scratch/unchanged, path-change
rewrites new key + discards old, save/kill cleanup, Stale not offered,
Corrupt typed+quiet+discardable, recover-file installs + fires after-edit
+ leaves modified, tick aggregation (3 loads -> 1 message, no repeat),
single-file naming, interval validation + live change, enable gate,
before-quit sweeps without vetoing.

Gates: fmt + workspace clippy clean; lib 1499; crdt 1670; autosave 15;
desktop 11; persistence 5; m4 90; m7_8 5; m8 10; GPU 58; git diff --check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 10:24:08 -04:00
Levi Neuwirth 3607df0afe feat(persistence): desktop-save --- buffers + layout + positions (Arc 3 phase 2)
Framing: docs/desktop-save-framing.md (Q#DS1-10). Save the open file
buffers, window layout, and per-window positions on quit; rebuild them
on startup. Emacs desktop.el, opt-in, local-mode only in v1.

All-Rust (Q#DS1) --- the core window enums are not serde and there is
no Lua tree API, so the layout mirror + structural rebuild live in Rust.
Lua adds only the opt-in switch and manual commands.

src/desktop.rs (new):
- Serde mirror (SavedDesktop / SavedBuffer / SavedNode / SavedLeaf /
  SavedOrientation): every open file buffer (visible OR hidden, so a
  switched-away file survives), the layout tree with orientation +
  weights + nesting, per-leaf cursor/view_top, and an active-leaf
  preorder index with a nearest-neighbor fallback (Q#DS10).
- session_key: SHA-256, name.<hex> when a socket name is set else
  cwd.<hex> (charset-safe for the pmacs.state key).
- save_session / restore_session take the &Lua that carries the
  SharedCore / StateDir / LocalInstanceInfo app-data, so they run
  identically from a pmacs.session.* binding and the startup trigger.
- restore ordering (Q#DS3): open all buffers; prune EVERY window of the
  old LOCAL layout (not just scratch); rebuild the tree; then per leaf
  in preorder activate its window and fire buffer.after-load once per
  newly-loaded buffer (hooks read active state), and write the exact
  cursor/view_top AFTER so desktop wins over saveplace (same file in two
  panes keeps distinct positions). A missing file collapses its leaf.

src/editor_core.rs: get_or_load_buffer(path) --- find_by_path else
load fresh, WITHOUT switching the active window; returns (id, newly).

src/lua_bindings: pmacs.session.{save_desktop, restore_desktop,
arm_restore, is_daemon}; DesktopRestoreArmed + DaemonMode markers;
fire_after_load_hook seam.

builtin/runtime/desktop.lua: pmacs.session.desktop_mode(on) wires
before-quit save + arms restore; desktop-save / desktop-restore
commands. No-op under a daemon (Q#DS9).

Startup trigger (Q#DS7): editor::run captures had_file before the match
consumes `file`, and restore_desktop_if_armed runs INSIDE the RunLocal
arm (after attach dispatch) so a hand-off to attach never populates an
EditorState it is about to drop. Daemon marks DaemonMode → desktop
stays local-only.

Tests: src/desktop.rs units (tree collapse, active-leaf fallback,
key/json round-trip) + tests/desktop_acceptance.rs (9): nested weighted
round-trip, hidden-buffer survival, after-load-active probe, same-file
two-pane distinct positions, missing-file collapse + focus fallback, no
orphan windows, name-vs-cwd key scoping, modified warning, startup gate.

Gates: fmt + workspace clippy clean; lib 1487; crdt 1658; desktop 9;
persistence 5; m4 90; m8 daemon 10/15; query-replace/completion/
listview/overlay/cua green; GPU 58; git diff --check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 20:28:43 -04:00
Levi Neuwirth 4dd4b9ab97 feat(persistence): state foundation + saveplace + recentf (Arc 3 phase 1)
Framing: docs/persistence-framing.md. The four Rust primitives the
Lua-vs-Rust scout said were unavoidable, plus two Lua policy modules.

Rust:
- src/state.rs: state_dir(xdg,home) returning .../pmacs (generalizes
  the baked-in history path). Deliberate empty-XDG fix (Q#PS2): a blank
  XDG_STATE_HOME fell through to a RELATIVE pmacs/... path (a cwd-write
  bug); now treated as absent so it falls to HOME. Confined key->file
  store: validate_name rejects absolute / .. / empty / // / control
  chars, plus a canonical-prefix belt; read/write/remove go through
  file_io::save_atomic, never raw io.open. A PMACS_STATE_HOME override
  lets CI / privacy-conscious users / integration harnesses redirect
  all state to a scratch dir. History routed through the shared
  resolver so it honors the override too.
- pmacs.state.{write,read,remove,path,available}: a no-op when the
  state dir is unconfigured (cfg(test) / no HOME), so default-on
  builtins write nothing in the lib suite. Configured once at startup
  like history_dir, skipped under cfg(test).
- pmacs.editor.goto_byte / set_view_top: byte-exact restore (switch
  zeroes the cursor).

Lua (builtin/runtime):
- saveplace.lua: record the active file's cursor+view_top on
  before-save / before-quit; restore on after-load. LRU-capped places
  state file. On by default; pmacs.saveplace.enable(false).
- recentf.lua: MRU record on after-load AND after-switch (re-visits
  refresh the order); deduped/capped recentf file; a recent-files
  command bound C-x C-r opens the minibuffer picker.

Tests: state.rs units (validate/resolve/round-trip/empty-XDG),
tests/persistence_acceptance.rs (state round-trip + confinement
rejections, inert-when-unconfigured, recentf MRU/dedup, saveplace
restore-on-reload, disable knob) injecting a tempdir state root. One
describe-hook test made robust to a builtin now subscribing to
buffer.before-save.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:57:39 -04:00
Levi Neuwirth 3b630bfee4 docs(features): document the Lua feature matrix; drop unreachable compile_error idea (F-002)
`--all-features` can't build pmacs — luajit and lua54 select mlua's
mutually-exclusive Lua backends. Document the model so generic tooling
(CI, cargo hack, distro packaging) doesn't trip over it:

- README §Build: a feature-matrix table (luajit default / lua54 fallback /
  orthogonal crdt), the supported build lines, and an explicit "don't use
  --all-features".
- src/lib.rs crate docs: a "Lua flavor features" section stating the
  exactly-one-flavor rule.
- Cargo.toml [features]: expanded comment on the mutual exclusivity.

CI already iterates the flavors explicitly (never --all-features), so no
CI change was needed.

The audit's suggested crate-local compile_error! for the wrong-flavor case
was investigated and rejected as unreachable: the flavor check lives in
the mlua-sys *build script*, which cargo compiles before the pmacs crate,
so a mis-set flavor (both or neither) fails there first and pmacs's own
compile_error! never evaluates — confirmed empirically for both cases. A
dependent crate can't preempt a dependency's build failure, so the docs
are the honest mitigation and they name mlua-sys as the actual error
surface.

Validated: fmt clean; clippy clean under both Lua flavors; both flavors
build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-03 19:54:35 -04:00
Levi Neuwirth 487c12cca9 pmacs context menu: registry + open-menu state types (Q#CM1/Q#CM2)
The data types for the right-click context menu, with no behavior yet
(nothing opens a menu until the dispatch wiring lands).

- `MenuRegistry` / `MenuItem` / `MenuError` (Q#CM2): the Lua-facing item
  registry, mirroring `CommandRegistry`. Items carry id / label / command
  / context tag / predicate / group / order; `context` is validated
  against a known vocabulary (typo -> hard error, R50-style), and a
  matching `id` replaces in place so config reloads and user overrides
  are idempotent.
- `MenuState` / `MenuRow` / `SharedMenu` (Q#CM1): the open-menu runtime
  state, plus the self-suppressing TUI `MenuView` overlay (the
  `SearchView` pattern). `MenuRow` is Item|Separator; navigation and
  hit-testing skip separators.

Pure additions behind `pub mod menu` — the lib still builds with the
module unused.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-06-27 21:42:28 -04:00
Levi Neuwirth b167b9a6df search: core SearchStore + smart-case find_all (Q#SR1/SR2)
The per-buffer in-buffer-search store, mirroring DiagnosticStore:
keyed Arc<Mutex>, sorted match ranges + active index, next/prev
stepping with wrap, focus-from-cursor, and stale-on-edit tracking
(M11.8 model — an edit suppresses matches at pre-edit byte
positions until re-search). find_all is smart-case substring
(case-insensitive unless the query has an uppercase char), ASCII
case-folded to keep byte offsets exact, non-overlapping matches.

Pure core, no wiring yet. 8 unit tests (case folding, overlap,
wrap, focus, stale, active-clamp-on-reset).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:38:22 -04:00
Levi Neuwirth 878ef7a80a T M4.5: textDocument/prepareRename (backlog item 3)
Built on #25 + #26 (branched off main after both merged).

- src/prepare_rename.rs: parse the prepareRename union
  null | Range | { range, placeholder } | { defaultBehavior } into
  { allowed, placeholder?, range? }. Store/Key/Shared keyed
  (server, uri). 7 unit tests.
- src/lsp.rs: prepare_rename_store + accessor,
  ResponseRoute::PrepareRename + absorb, request_prepare_rename;
  capability rename.prepareSupport=true +
  prepareSupportDefaultBehavior=1.
- src/lua_bindings.rs: _request_prepare_rename_raw,
  pmacs.prepare_rename.{result,clear}.
- builtin/runtime/lsp.lua: pmacs.lsp.rename() gates on
  caps.renameProvider.prepareProvider. When supported: prepareRename
  round-trip first — null/not-allowed -> "cannot rename here", the
  prompt never opens; otherwise the prompt opens pre-filled with the
  server's placeholder (minibuffer `initial`). Servers without
  prepare keep the unchanged L2 path (on_accept factored into a
  shared local; no behavior change there).
- pmacs_fake_lsp.rs: `prepare`/`preprefuse` modes inject
  renameProvider.prepareProvider + a prepareRename arm
  ({range,placeholder} or null). Default `rename` mode untouched so
  m4_13 still exercises the no-prepare path.
- tests/m4_acceptance.rs: m4_22 (prompt waits for the async
  prepare, placeholder pre-filled, rename applies), m4_23 (refusal:
  prompt never opens, buffer untouched).

Gates: lib 1299/0, m4 78/0, m8_1 10/0, m8_9 26/0, m8_10 19/0,
m9_1 18/0, m5_8 5/0, m11_5 (--features crdt) 2/0; fmt + clippy
clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 14:23:57 -04:00
Levi Neuwirth cf2947f7f9 T M4.5: semantic tokens (textDocument/semanticTokens/full)
LSP data layer only — independent of the M11 semantic-render
protocol (semantic_render.rs / semantic_client.rs, tree-sitter →
frontend wire families). No InstanceMessage family added; wiring
LSP tokens into styling is a separate rendering milestone. Same
shape as every sibling LSP feature: typed store + async request
+ Lua surface + command + modeline summary.

- src/semantic_tokens.rs: decode the 5-int relative encoding
  (deltaLine, deltaStartChar, length, tokenType, tokenModifiers)
  into absolute SemanticToken{line,start,length,token_type,
  token_modifiers}, with the same-line-vs-new-line deltaStartChar
  rule and defensive truncation of a malformed trailing group.
  SemanticTokensLegend::from_capabilities parses
  semanticTokensProvider.legend and resolves type index / modifier
  bitset to names. Store keyed (server, uri). 6 unit tests.
- src/lsp.rs: store + accessor, ResponseRoute::SemanticTokens +
  absorb, request_semantic_tokens (/full; v1 no range/delta),
  textDocument.semanticTokens client capability (full-only,
  formats=[relative], standard LSP legend).
- src/lua_bindings.rs: _request_semantic_tokens_raw,
  pmacs.semantic_tokens.{tokens, legend, clear} (legend reads the
  per-server initialize capabilities).
- pmacs_fake_lsp.rs: semanticTokensProvider.legend in initialize;
  textDocument/semanticTokens/full arm with relative-encoded data.
- builtin/runtime/lsp.lua: pmacs.lsp.semantic_tokens() requests
  full, stores, modeline summary (first token's type resolved via
  legend); lsp.semantic-tokens command + C-c y.
- tests/m4_acceptance.rs: m4_17 drives the request via the Lua
  surface, asserts decoded absolute tokens (incl. deltaLine!=0 ⇒
  absolute startChar) and legend index→name resolution.

Gates: lib 1285/0, m4 72/0, m8_1 10/0, m8_9 26/0, m8_10 19/0,
m9_1 18/0, m5_8 5/0, m11_5 (--features crdt) 2/0 (confirms no
collision with the M11 render protocol); fmt + clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 13:15:19 -04:00
Levi Neuwirth 1a0ccd0247 T M4.5: inlay hints (textDocument/inlayHint)
Independent LSP feature (not part of the L1-L4 cross-file arc),
shipped in the same shape as every sibling: typed store + async
request + Lua surface + command + modeline summary, with the
inline renderer deferred as its own milestone.

- src/inlay_hint.rs: parse InlayHint[]|null — position, label
  (string OR InlayHintLabelPart[] flattened), kind (type/
  parameter), paddingLeft/Right, tooltip (string|MarkupContent).
  Store keyed (server, uri). 5 unit tests.
- src/lsp.rs: inlay_hint_store + accessor, ResponseRoute::InlayHint
  + absorb, request_inlay_hint (range params), textDocument.
  inlayHint client capability (no resolveSupport/refreshSupport —
  full hints, on-demand re-query is the v1 model).
- src/lua_bindings.rs: _request_inlay_hint_raw,
  pmacs.inlay_hint.{hints,clear}.
- pmacs_fake_lsp.rs: textDocument/inlayHint arm returning a
  string-label type hint and a label-parts parameter hint.
- builtin/runtime/lsp.lua: pmacs.lsp.inlay_hints() requests over
  the whole-buffer range, stores, modeline summary;
  lsp.inlay-hints command + C-c i; scope header notes the inline
  renderer is a later milestone.
- tests/m4_acceptance.rs: m4_16 drives the request via the Lua
  surface, asserts both label shapes / kinds / padding parsed.

Deferred (scoping, not a regression): inline virtual-text
rendering. The VirtualCellOverlay model only overwrites existing
cells; rendering hints inline needs a column-inserting/reflowing
renderer — a rendering milestone, not an LSP task — staged like
the hover panel / references list. pmacs.inlay_hint is the data
surface a future render layer subscribes to.

Gates: lib 1279/0, m4 71/0, m8_1 10/0, m8_9 26/0, m8_10 19/0,
m9_1 18/0, m5_8 5/0, m11_5 (--features crdt) 2/0; fmt + clippy
clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 12:58:36 -04:00
Levi Neuwirth a71e4a0ac0 T M4.5 L3: code actions + executeCommand + workspace/applyEdit
Third cross-file layer: code actions and the server→client edit
channel that executeCommand-driven actions depend on.

- src/code_action.rs: normalise (Command | CodeAction)[] into one
  CodeActionItem (bare-Command vs nested-Command disambiguated by
  whether top-level `command` is a string; inline `edit` reuses
  rename::WorkspaceEditResponse). Store keyed (server, uri). 5 tests.
- src/lsp.rs: code_action_store + accessor, ResponseRoute::CodeAction
  + absorb, request_code_action, request_execute_command (awaiter
  only — effect arrives out of band). Capabilities: codeAction
  (+codeActionLiteralSupport), workspace.executeCommand, and
  workspace.applyEdit flipped to true.
- src/lua_bindings.rs: _request_code_action_raw,
  _request_execute_command_raw, _parse_workspace_edit (any raw
  WorkspaceEdit JSON -> applier input shape), pmacs.code_action.*.
- pmacs_fake_lsp.rs: textDocument/codeAction arm (command action
  first, inline-edit action second) + workspace/executeCommand arm
  that emits a server→client workspace/applyEdit before responding.
- builtin/runtime/lsp.lua: pmacs.lsp.code_actions (apply first
  action: inline edit and/or executeCommand); the applyEdit pump
  (chained on pmacs._async.tick, drains only attachment-server
  events, snapshots server ids before applying since find_or_open
  can mutate `attachments`, replies { applied }); lsp.code-actions
  command + C-c a keybind.
- tests/m4_acceptance.rs: m4_14 drives the full
  codeAction→executeCommand→applyEdit chain end to end.

Gates: lib 1273/0, m4 69/0, m8_1 10/0, m8_9 26/0, m8_10 19/0,
m9_1 18/0, m11_5 (--features crdt) 2/0; fmt + clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 11:11:08 -04:00
Levi Neuwirth cbfb7f5e71 T M4.5 L2: WorkspaceEdit applier + textDocument/rename
Second cross-file layer on the L1 foundations: a multi-file
WorkspaceEdit applier and an LSP rename UX driving it.

- src/rename.rs: parse a WorkspaceEdit (both `changes` map and
  `documentChanges`, the latter preferred per spec; AnnotatedTextEdit
  handled; create/rename/delete resource ops counted into
  `unsupported_ops` for L4) into per-file TextEdit lists. RenameStore
  keyed by the request's origin URI. 6 unit tests.
- src/lsp.rs: rename_store + accessor, ResponseRoute::Rename,
  request_rename, and the textDocument.rename client capability
  (prepareSupport=false — L2 renames from the cursor position).
- src/lua_bindings.rs: _request_rename_raw + pmacs.rename.{file_edits,
  unsupported,clear}.
- pmacs_fake_lsp.rs: textDocument/rename arm; `rename` mode returns a
  2-file documentChanges plus a create resource op.
- builtin/runtime/lsp.lua: apply_workspace_edit (preflight rejects
  unresolvable URIs before mutating anything; per-file reverse-sorted
  application; origin buffer restored), pmacs.lsp.rename with a
  minibuffer prompt, lsp.rename command, C-c r keybind.
- tests/m4_acceptance.rs: m4_13 drives rename end-to-end through the
  minibuffer and asserts both files mutated + origin restored.

Gates: lib 1268/0, m4 68/0, m8_1 10/0, m8_9 26/0, m8_10 19/0,
m9_1 18/0, m11_5 (--features crdt) 2/0; fmt + clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 10:42:03 -04:00
Levi Neuwirth 80c0679a5b T M4.5: documentSymbol / workspace symbol / documentHighlight
Completes the read-only LSP feature set (everything except edits).
Same request→store→Lua pattern as the nav batch; these three need
new parsing (new response shapes), so they were deliberately split
from the Location-family PR (#16).

- src/symbol.rs: one flat Symbol type for both symbol requests.
  from_lsp_value handles BOTH LSP shapes — hierarchical
  DocumentSymbol[] (flattened with depth + parent chain) and flat
  SymbolInformation[]/WorkspaceSymbol[] (location.uri, range
  optional for WorkspaceSymbol). Scope-keyed (Document(uri) vs
  Workspace(query)) so an outline and a query don't collide.
- src/document_highlight.rs: range + DocumentHighlightKind (absent
  defaults to Text=1 per spec), (server,uri)-keyed.
- lsp.rs: three ResponseRoute variants + absorb arms + request
  methods. documentSymbol/documentHighlight ranges convert via the
  requested-doc codec; workspace/symbol results are cross-file →
  route uri "" → non-destructive passthrough (same rule as
  cross-file definition).
- Lua: raw bindings + pmacs.document_symbol / .workspace_symbol /
  .document_highlight read surfaces (the new LSP Symbol is aliased
  to avoid the pre-existing project_index::Symbol name clash);
  lsp.lua wrappers + an lsp.document-symbols command on C-c o
  (modeline summary; outline buffer is future UX).
- Tests: 6 parser unit tests (hierarchical depth/parent, flat
  SymbolInformation, range-less WorkspaceSymbol, highlight kind
  default, scope non-collision) + an e2e driving all three through
  the async bridge asserting shape correctness.

Also includes a pre-existing rustfmt normalization of the #15
semantic-frontend files (protocol.rs / semantic_client.rs /
semantic_render.rs) — main was not rustfmt-clean there after the #15
merge; bundled here per operator decision so the fmt gate is green.

Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1257/0; m4_acceptance 66/0; m9_1 18/0; m8_1/m8_9/m8_10 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:15:03 -04:00
Levi Neuwirth 2f20c8a82f
Merge pull request #16 from levineuwirth/lsp-nav-features
T M4.5: nav requests — references / declaration / typeDefinition / implementation
2026-05-19 01:40:10 +00:00
Levi Neuwirth 96780b547d T M4.5: nav requests — references/declaration/typeDefinition/implementation
The "cheap batch" subset that is genuine template-fill: these four
return the exact `Location | Location[] | LocationLink[] | null`
shape `textDocument/definition` already parses, so no new parsing —
only a kind discriminator so they don't collide on (server, uri).

- src/locations.rs: a (server, uri, kind)-keyed store whose value
  type is the reused crate::definition::DefinitionResponse. The
  proven definition store + Lua API are untouched.
- lsp.rs: ResponseRoute::Locations { uri, kind }; one absorb arm; a
  DRY request_locations helper + request_references /
  request_declaration / request_type_definition /
  request_implementation. references sends
  context.includeDeclaration. Supersede keys derive from each
  kind's distinct method, so the four don't cancel each other.
- Lua: _request_*_raw bindings + install_locations exposing
  pmacs.references / .declaration / .type_definition /
  .implementation ({ locations, clear }, mirroring pmacs.definition,
  reusing definition_response_to_lua). lsp.lua Handle wrappers + a
  lsp.find-references command bound to M-? (modeline summary;
  references-list buffer is future UX, like the hover panel).
- Tests: locations.rs unit tests (kind labels distinct; keys don't
  collide); e2e driving all four through the async bridge and
  asserting each routes to its own kind slot (fake returns distinct
  lines 11/21/31/41).

Scoped: documentSymbol / workspaceSymbol / documentHighlight return
different shapes (new parsing) — a separate follow-up, not crammed
in here.

Gate: fmt clean; clippy --all-targets -D warnings clean; lib
1232/0; m4_acceptance 65/0; m9_1 18/0; m8_1/m8_9/m8_10 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:39:02 -04:00
Levi Neuwirth 2ca011c368 M11.5: semantic frontend<->instance glue (SemanticClient + e2e)
Completes the M11 arc with the consumer side. pmacs has no GUI
toolkit, so per the design note's testability strategy the
deliverable is the bounded testable glue, not a GPU renderer.

- src/semantic_client.rs (crdt-gated): headless SemanticClient
  composing the BufferMirror rope replica (M10.10) with a tile-based
  SemanticModel that reconstructs styling/decorations from the full +
  dirty-segment deltas (M11.4). Emits FrontendEvent::Viewport;
  read-back accessors (text / effective_style_at /
  decoration_kinds_at / tile ranges). The M11.4 contract (segments
  carry every current item intersecting their range) makes tiles
  self-contained → incremental apply is per-tile replacement with
  edge-clipping, no cross-span surgery. 7 unit tests.
- tests/m11_5_semantic_acceptance.rs: (a) reconstruction-equivalence
  — incrementally-driven client asserted byte-for-byte identical to
  a fresh full projection across a scripted viewport/edit/selection
  sequence incl. a viewport jump (golden discipline, no snapshot
  crate); (b) end-to-end — a real daemon routes StyleSpans/
  Decorations to a semantic session (after it declares a Viewport)
  and never to a grid session, CellDelta vice versa, validating the
  M11.2 per-session projection through the socket.

Lib (1404 crdt / 1242 non-crdt) + integration green on both feature
flavors; clippy -D warnings clean on both.

M11 arc complete (M11.1–M11.5). Inline/Block/Fold/ResourceOffer
remain honest stubs pending their source features.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:24:36 -04:00
Levi Neuwirth 4ba6fd7bb8 M11.2: semantic projection seam (SemanticRenderState)
The first real producer of the semantic-frontend arc. The instance
projects syntax styling to semantic_render sessions without
rasterizing to a cell grid.

- src/semantic_render.rs: SemanticRenderState, sibling of
  instance_render::RenderState. Reads the same EditorState, emits
  InstanceMessage::StyleSpans (tree-sitter spans via the active
  Theme), scoped + clipped to the FrontendEvent::Viewport byte range.
  Emits nothing until a viewport is declared; suppresses
  byte-identical frames (per-span delta encoding deferred to M11.4).
- CrdtState::version_scalar(): oplog version vector summed to a
  monotonic non-decreasing u64 — the StyleSpans.generation anchor.
- daemon dispatcher: semantic_states map parallel to render_states;
  projection selected per session. Semantic sessions get StyleSpans +
  CursorByte + BufferSnapshot + CrdtOp + presence, never CellDelta /
  grid Cursor. FrontendEvent::Viewport consumed (routed by
  authenticated source). SessionEstablished body extracted to
  handle_session_established (clippy 100-line ceiling). Grid-less
  sessions no longer panic the _ => apply_event arm.
- InstanceCapabilities default semantic_render flipped to
  cfg!(feature = "crdt") — the "M11.2 enables semantic" moment,
  analogous to the M10.8 Day-4 flip. M11.1 negotiation test comment
  updated for the flip (frontend-side default still false keeps M10.7
  outcomes unperturbed).

Lib (1392 crdt / 1237 non-crdt) + integration suites green on both
feature flavors; clippy -D warnings clean on both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:14:08 -04:00
Levi Neuwirth 45be65b026 M10.10 ship gate
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>
2026-05-13 16:28:46 -04:00
Levi Neuwirth 587a2a15de M9 ship gate
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>
2026-05-09 15:04:23 -04:00
Levi Neuwirth 3a35d0b0f8 M8 ship gate 2026-05-07 16:55:14 -04:00
Levi Neuwirth c8d0d67615 Fix PTY final-output drain race 2026-05-04 09:44:30 -04:00
Levi Neuwirth 4da4b09d5d Initial commit: v0.1.0 2026-05-03 19:51:06 -04:00