Commit Graph

654 Commits

Author SHA1 Message Date
Levi Neuwirth a38296583b docs: frame Lean 4 mode (Arc 8)
The approved framing for Arc 8, revision 4, after three review rounds.
Seven stages: grammar/mode, multi-root LSP affinity, the Lean language
server, the Unicode input method, the goal view, the #eval output
channel, and module hierarchy. 19 decisions, 64 acceptance criteria.

Committed as this branch first commit per the house workflow; the
implementation of Stage 1 follows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 09:44:53 -04:00
Levi Neuwirth e74506879f
Merge pull request #155 from levineuwirth/bottom-panel
Bottom panel Stage 1: window placement + TUI side windows
2026-07-25 00:21:07 +00:00
Levi Neuwirth 9110f9f32c fix(window): keep pmacs.window.buffer() infallible with no argument
PR #155 review round 2, self-review of the round-2 commit.

The round-2 change labelled "minor" — resolving both arms of
pmacs.window.buffer() through the acting frontend for uniformity — made
the NO-ARGUMENT arm fallible. `acting_frontend` follows the interactive
origin, which can name a frontend that has no registered view: a bare
`dispatch_key` from an unattached peer does exactly that. `selected_window`
then raises "acting frontend has no layout" instead of answering.

Nothing surfaced that error, because the runtime callers do not pcall it.
killring, syntax, autosave, pair, indent and comment all read
pmacs.window.buffer() on ordinary edits, so the raise silently dropped
the operation: kill_ring_acceptance went 30/30 to 25/5, with
frontend_detached_drops_per_frontend_state reporting only "B has kill
state". main is 30/30, and reverting this one file restored it.

The no-arg arm is back on ambient active_buffer_id() and now documents
why that is deliberate rather than an oversight: dispatch sets
active_frontend to the acting frontend before running a command, so the
two agree on every real path, while only the ambient resolver has the
fallback that makes it total. The explicit-window arm keeps its Q#BP11
layout validation, which is what the arc actually needed.

acc19c pins it through the real path — a buffer.after-edit subscriber
reading pmacs.window.buffer() during a viewless peer's dispatch_key —
rather than by calling the binding directly. Bite-verified:
scripts/bite bbe4152 src/lua_bindings/mod.rs --test
bottom_panel_stage1_acceptance -- acc19c goes red with the exact
"acting frontend has no layout" traceback.

The ledger also records two gating facts found on the way: the workspace
sweep must run with an isolated XDG_CONFIG_HOME, because the real user
init.lua installs a local package and the losing race leaks a status
message into painted-frame comparisons; and a latent pre-existing main
bug in the buffer CRDT undo path, which is not this branch's and whose
proptest seed is deliberately not committed here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012j4omtTMn9v1UfmHQb9ap6
2026-07-24 20:05:51 -04:00
Levi Neuwirth bbe4152048 docs: record bottom-panel review round 2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 18:42:04 -04:00
Levi Neuwirth 52e7598da0 test(window): press the peer's own mode line in acc30c
The round-2 peer press landed in the peer's CONTENT area, so it never
reached `arm_window_drag` — the exact path Finding 5 names — and the
case bit nothing. It now presses the peer's own mode-line row, where a
single global drag slot is overwritten (and, since that lone window owns
no boundary, cleared outright).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 18:37:10 -04:00
Levi Neuwirth 4d44be5d7b fix(terminal): implement the Q#BP7 growth re-arm and pin it honestly
PR #155 review round 2.

Finding 1 (must fix): Q#BP7 item 1 — "growth reaching the live tail
re-arms follow (top -> None), only when no selection is active" — was
never implemented. `at_bottom` is the instantaneous geometric readout
`scroll_offset == 0`, which a still-anchored view satisfies whenever it
happens to be tall enough to reach the tail, so the round-1 assertion
could not see the gap: the next rows the child printed pushed the
anchored view back into history.

`rearm_follow_on_growth` now clears `top` when a viewport-size
declaration makes the view cover the tail and no selection is frozen,
and every size-declaring path (`snapshot_for_view`, `record_view_size`,
`view_status_for_size`) routes through one `declare_view_size` helper so
grid and semantic declarations cannot disagree. `scroll_view` and
`begin_selection` deliberately stay out: they write `top` themselves,
and `scroll_view` already owns the scroll-driven arm.

New acc32b is the pin the review asked for: scroll into history, grow
past the tail, then release a SECOND burst of child output through a
filesystem gate and assert the view moved with it.

Finding 2: the PTY fixtures emitted LF-only output, which staircases
rightward until every row clips to blanks past the viewport width — so
the round-1 anchor assertions compared "" with "" and could not fail.
Both fixtures now emit CRLF, and each anchor comparison is guarded by
`assert!(!top_before.is_empty())`.

Finding 3: acc33's contrast case asserted nothing, and the behavior it
claimed was false as coded. With the re-arm in place it is true and now
asserted: clearing the selection at the same geometry re-arms follow and
leaves the frozen anchor.

Finding 4: `start_run` gated the panel branch on `display == "panel" or
already_in_panel(..)`, so an explicit `display = "current"` lost to the
inference — and that value is the documented user-facing opt-out from
the Stage 3 default flip. Now gated on OMISSION. acc19b gains the
explicit-"current" case.

Finding 5: `window_drag` is a `HashMap<FrontendId, WindowDragState>`, so
a peer's mode-line press can no longer steal or clear another
frontend's in-flight gesture, and concurrent drags are legal. Cleared on
detach. acc30c gains the mode-line-press case.

Minor: `pmacs.window.buffer()` resolves both arms through the acting
frontend using the shared `lookup_window` / `selected_window` validators
rather than re-implementing them beside an ambient `active_buffer_id()`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 18:32:23 -04:00
Levi Neuwirth 7e1bfb6dc5 test(window): pin the terminal anchor, not the tail-relative offset
`TerminalViewStatus.scroll_offset` is the retained rows between the
VIEWPORT and the live tail, so it necessarily tracks viewport height: an
assertion that it survives a panel height change unchanged is either
vacuous or wrong, and it went red once under a loaded sweep for exactly
that reason. Q#BP7's invariant is that the ANCHOR is frozen, so acc32
and acc33 now compare the first visible row's text across the change,
and additionally pin the follow behavior that distinguishes them: a
shrink never re-arms follow, growth reaching the tail does, and growth
with a frozen selection does not.

Both also wait for the child's last line before sampling, so neither
races further output.

Also records the round in docs/active-work.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 15:21:48 -04:00
Levi Neuwirth 352bf0b2ab
Merge pull request #154 from levineuwirth/inline-math-rescout
docs(math): re-scout inline-math framing against landed state (rev 2)
2026-07-24 19:08:27 +00:00
Levi Neuwirth 90fc7a913e fix(window): wire the side-window split guard and scope the divider drag
PR #155 review round 1.

Finding 1 (must fix): `try_split_active` had no production caller —
`pmacs.window.split_horizontal` / `split_vertical`, and therefore
`C-x 2` / `C-x 3`, still went through plain `split_active`. Splitting a
focused panel made the root wrapper's final child a split rather than
`Leaf(side)`, which both `Layout::compute`'s fixed pass and
`document_subtree` key on: the panel band reverted to 1:1 weight
division and an ordinary window ended up living inside it. Both bindings
now route through the guard, and acc26 asserts through the real Lua
path — a direct core call passes with the guard unwired, which is how it
survived the first round.

Finding 2: the armed-drag early return now checks the arming frontend,
so one frontend's in-flight gesture cannot cancel or swallow another's
mouse events. New acc30c.

Finding 3: `paint_mode_line_graphemes`'s doc block was left heading
`paint_divider_segment`; moved back.

Finding 4: a recompile carries no `display`, so it took the raw switch
and duplicated a panel-placed `*compilation*` into the document window.
`start_run` now detects that the buffer already owns the panel slot.
`pmacs.window.buffer` gained an optional window argument so an adopter
can ask without selecting the panel first. New acc19b.

Stage-2 hazard pins the review asked for, both in `src/daemon.rs`:
a fresh attach while LOCAL is focused in a panel inherits LOCAL's
document buffer, and an initial-target bootstrap whose `after-load`
hook creates and selects a panel still reasserts into a document window.

Minor: dropped listview's dead `p.side`; documented `focus_window`'s
caller-validates contract; `jump_back` restores through `focus_window`
so the "every focus change" contract holds; `params` / `resize` default
to the acting frontend's selected window rather than the ambient one;
widened the flexible-division math to u64 intermediates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 15:07:35 -04:00
Levi Neuwirth dd414d4d2d docs(math): name the ttf-parser feature set the MATH table needs
Review finding, verified: rev 2's "no new build cost" was true only under
an unstated condition. ttf-parser re-exports `math` behind
`#[cfg(feature = "opentype-layout")]`, and it is compiled today only
because fontdb requests that feature — with `default-features = false` and
a set that is NOT ttf-parser's own default (fontdb's adds no-std-float and
omits std). A plain `ttf-parser = "0.25"` therefore unions std in and
forces a one-time rebuild of ttf-parser, fontdb, cosmic-text and glyphon.

Record the zero-rebuild spelling, `default-features = false, features =
["opentype-layout"]`, in Tier 3 §A and in the component table, so the
Tier 3 implementer declares it deliberately rather than tripping over it.
The C1 row points at the detail rather than repeating it.

Also note in the header that every anchor was re-checked at f07b75b. The
scout pin stays at ddaa80d because that is when the scouting happened;
#153 landed between the two and is test-only, moving no anchor cited here.

Framing only; no implementation, no runtime code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 14:59:21 -04:00
Levi Neuwirth f07b75b4a3
Merge pull request #153 from levineuwirth/vterm-pty-flake
test(vterm): match host output past the differ's cell-skipping
2026-07-24 18:37:23 +00:00
Levi Neuwirth 85a07f378c test(vterm): gate terminal readiness on a file, not on host bytes
Both earlier attempts in this branch were wrong, and the diagnostic they
added is what proved it: the macOS failure reported a stable
`rendered prefix: 6/15 ("VTERM_")` BEFORE and AFTER the CRLF change, with an
identical cursor, across two completely different child layouts. Identical
truncation under different layouts cannot be a layout problem.

The real mechanism is pinned by the repository's own unit test,
`cell::tests::diff_split_by_unchanged_cell_is_two_spans`: `cell::diff` splits
a run at any cell where `prev == next` and never transmits that cell. So when
a character of the marker already happens to sit at its destination, the host
receives the marker with that byte MISSING, not merely escaped around. The
constant 6 is the distance to the first such hole.

That makes escape-stripped matching unsound in kind rather than merely
insufficient: no matching strategy recovers a byte that was never sent. It is
removed, and `wait_for_output` is strict again. What remains asserted through
host bytes are protocol escapes pmacs writes directly — the OSC 52 clipboard
reply, the alternate-screen and bracketed-paste resets — which are not painted
cells and which the differ never touches.

Readiness now gates on a file the child publishes, the pattern the reliable
sibling test in this file already uses. That the child's output reaches the
SCREEN stays asserted in-process over `snapshot_text`, at the layer that can
actually see it; this test keeps what it uniquely owns, the host lifecycle.

`strip_ansi` and `longest_rendered_prefix` are kept as failure diagnostics
only, and now carry a case pinning the dropped-cell shape so the wrong remedy
is not reached for again. The new readiness wait reports startup breadcrumbs
on timeout; the plain helper reports only the missing path, which is the least
useful thing to know at exactly that moment.

Both the readiness gate and its timeout diagnostic were falsified by pointing
the child at a path the test does not watch.

Test-only; no runtime code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 14:27:21 -04:00
Levi Neuwirth 2a9c11461c docs: record the bottom-panel Stage 1 lane
Adds the lane to docs/active-work.md: branch, base, what Stage 1
implemented, the verification run, and the two known local-only test
caveats (the parallel-load GPU flake and compile_mode_acceptance's
single-thread requirement).

The durable handoff snapshot stays untouched until the PR merges, per
its own update protocol.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 14:20:16 -04:00
Levi Neuwirth 5dcc7d70a0 docs(math): re-scout inline-math framing against landed state
Revision 1 was written against protocol v18, before LaTeX Stage 1 (#144),
web grammars (#146), folding Stages 1-2 (#142/#149) and the GPU initial
target (#148) landed. Revision 2 changes no design decision; it corrects
the ground truth those merges invalidated and records the staging decision
the sibling substrate framing already took. A new section 0 lists every
correction so a reader who knows revision 1 can read it alone.

Two corrections change implementation choices rather than line numbers:

- The MATH-table dependency story was wrong in both directions. Revision 1
  said a crate must be added and that "neither is in the tree today";
  ttf-parser 0.25.1 already reaches pmacs-gpu non-optionally through
  fontdb -> cosmic-text -> glyphon, the same fontdb the frontend already
  calls. And the choice is not "one of ttf-parser or read-fonts": only
  ttf-parser exposes the MATH table, supplying exactly the constants Tier
  3 names. read-fonts 0.37.0 is present but has none, so selecting it
  would be a dead end.

- Tier 2's staging was already decided elsewhere and this note did not say
  so. The sibling framing's Q#LX5 puts the parser beside its Tier 3
  consumer, never ahead of it, because MathNode's shape is only validated
  by a layout consumer. That makes Tier 2 not independently shippable,
  which is worth stating explicitly: it is pure and conflict-free, so
  landing it alone while other lanes hold the render path is exactly the
  tempting move Q#LX5 refused.

Tier 1 is materially de-risked: the LaTeX grammar already exposes
math_environment and math_delimiter, and the in-repo query overlay this
tier proposed already exists and captures both, so the mechanism is proven
rather than speculative. The guessed node name (math_expression) is
corrected to the grammar's own. Markdown still needs the overlay
treatment.

Tier 4 gains a contention note. Revision 1 described the GPU render path
as though math were its only claimant; folding Stage 3 and the
bottom-panel arc's Stage 2 now converge on it, so whichever lands second
re-scouts against the first.

Framing only; no implementation, no runtime code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 14:10:46 -04:00
Levi Neuwirth 683c9b86aa feat(window): adopter placement opt-in and the Stage 1 acceptance suite
- `listview.open`, `compile.run`, and `pmacs.terminal.open` all take the
  same strict `display = "current" | "panel"`, validated before any
  buffer, session, process, or wrapper exists. Omission keeps today's
  behavior; Stage 3 flips the default.
- `listview.quit` / `compile.quit` delegate to `window.quit` only when
  the buffer really is in a side window, so the presentation is deleted
  or restored instead of leaving a source buffer stranded in the slot.
- LSP `visit_location`, LSP go-to-definition, and compile `visit_error`
  route through `display_file`, so a visit from a panel lands in the
  document target and fires its hook with that window active.
- `window.quit`'s Delete arm focuses the revalidated remembered origin.
- Capability fallback discards an accompanying `height` rather than
  rejecting the call.
- `window.min-height` clamps a below-floor value on read instead of
  refusing the write.
- `tests/bottom_panel_stage1_acceptance.rs`: 42 tests over the framing's
  Stage 1 criteria, including the two production `Layout::compute`
  callers, the recursive minima, hide/reappear, the final-focus matrix,
  quit chains at the depth cap, per-frontend jump origins, the divider,
  and a real-PTY pin of Bet B1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 14:10:45 -04:00
Levi Neuwirth f77ff3074d test(vterm): write CRLF from the raw-mode probe
The diagnostic added in the previous commit answered the question on its
first macOS run:

    rendered prefix: 6/15 bytes ("VTERM_") — child text rendered only partially

So the child wrote and the host received part of the marker, but stripping
escapes did not rejoin the rest: other repainted cells sit between the two
pieces, not just cursor moves.

Six characters is exactly what fits before the right margin of this
session's 40-column child. The probe writes bare `\n`, and the supervisor's
PTY trampoline runs `stty raw`, which clears OPOST — so a lone `\n` moves
down without returning to column 1 and every line staircases five columns
right. After twenty lines the marker starts in the right margin, wraps
mid-word, and reaches the host as two pieces that no contiguous match can
join.

That also explains the intermittency: the wrap column depends on whether
pmacs has already resized the PTY from the requested 40 columns to the
window width, which races the child's first writes.

Write explicit carriage returns so every line returns to column 1 and the
markers start there. The fixture was wrong about its own line discipline;
the emulator was behaving correctly throughout.

The escape-stripped matching and the rendered-prefix diagnostic from the
previous commit are kept: they are what produced this answer, and they keep
the next such failure legible.

Test-only; no runtime code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 14:02:25 -04:00
Levi Neuwirth 6c8a76e235 feat(window): window parameters, fixed extents, and the display policy
Stage 1 substrate for the bottom-panel arc (docs/bottom-panel-framing.md).

- `WindowParams` (side / fixed_rows / dedicated + implementation-owned
  quit action and remembered document origin), `Side`, `QuitAction` with
  a bounded replacement history, and the `MIN_WINDOW_OUTER_ROWS` floor.
- `Layout::compute(area, fixed)` allocates fixed rows before dividing the
  remainder by weight; both production callers feed the same shared map,
  including the peer-presence overlay pass that derives its own rect.
- `subtree_min_rows` / `interactive_min_rows`: the recursive minima, and
  `boundary_below` for the shared drag / keyboard resize boundary rule.
- `FrontendView` gains `panel_capable`, `frame_geometry`, and the derived
  `panel_hidden`, each spelled explicitly at every construction site.
- `EditorCore`: `primary_document_window`, the non-side target rule,
  `display_buffer` + placement policy, `quit_window`, side-window removal
  on `kill_buffer`, per-frontend jump entries with origin windows, and the
  shared resolve/load-without-switch seam the initial-target bootstrap now
  uses too.
- `EditorState`: the panel reconciliation transaction, geometry
  declaration, the side-window `dispatch_idle_for` gate, divider paint,
  and divider drag.
- `pmacs.window.display / display_file / quit / panel / params /
  set_params / resize / display_target`, plus `builtin/runtime/window.lua`
  with `window.panel-height`, `window.min-height`, and the resize commands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 13:53:14 -04:00
Levi Neuwirth 8ef2fa0793 test(vterm): match host output past the differ's cell-skipping
The macOS `VTERM_ALT_READY` flake finally reported itself through #151's
breadcrumbs, identically in two runs: pmacs still running, `init.lua`
reached, `terminal.open` ok, and a settled screen whose tail is nothing but
`\x1b[22;42H` repeated 133 times.

That cursor is the evidence. A blank terminal parks at 1;1. Column 42 is
where the cursor lands after writing a 15-byte marker that ends at column
41 — so the child DID write and the emulator DID receive it. What failed
was the assertion: `wait_for_output` required the needle to appear as
contiguous bytes, but the TUI differ paints only changed cells and skips
ones that already match, so a run held contiguously on one screen row can
still reach the host as `PREF<cursor-move>IX`.

Match over escape-stripped bytes when the needle is plain text. This cannot
mask the failure that matters: text the child never wrote is absent from
the stripped stream too, so a genuinely silent child still fails. Needles
carrying their own escape (the OSC 52 clipboard reply) keep the strict path,
since stripping would consume the bytes under test.

The failure arm now also reports how much of the needle rendered, so the
next occurrence distinguishes "nothing reached the host" — a PTY/spawn
fault — from a partial render, instead of leaving a tail of pure escapes
that cannot tell them apart.

Both helpers are pinned directly, including that stripping rejoins a split
run without inventing absent text. `strip_ansi`'s first draft mishandled
`ESC ( B`, whose intermediate byte makes it three bytes rather than two;
its test caught that.

Test-only; no runtime code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 13:49:14 -04:00
Levi Neuwirth c27f75a1b2 docs: bottom panel framing (revision 4, amended)
Adds the approved framing for the bottom-panel arc: a buffer displayed
in a fixed-height window pinned to the bottom of the frame, targeted by
policy rather than by stealing the selected window, plus the missing
display-buffer/window-parameter concept underneath it.

Revision 4 follows three review rounds, an integration review, and a
landed-state audit against GPU initial target (#148, protocol v20) and
folding Stage 2 (#149).

Amended before branching by the pre-implementation dependency
verification recorded in section 0.6:

- the folding dependency is cleared and re-verified against canonical
  main at ddaa80d (nothing in flight, folding Stage 2 acceptance 48/48
  green, every borrowed anchor reproducing, and folding's only window.rs
  edit confined to one 22-line hunk that leaves the layout functions
  pre-folding code);

- R5-B1: Layout::compute has TWO production callers, not one. The
  second, the peer-presence overlay pass in src/overlay_paint.rs, builds
  its own text-area rect from active_layout() and never routes through
  window_placements, so the planned compute(area, fixed) signature change
  would otherwise leave every peer cursor painted at its no-panel row.
  Corrected in section 1.1 and Q#BP2, pinned by acceptance 1, and the
  fixed map is now specified to come from one shared helper rather than
  being assembled per call site.

Stage 1 (window placement + TUI side windows) changes no wire shape.
2026-07-24 13:00:46 -04:00
Levi Neuwirth ddaa80d4a8
Merge pull request #152 from levineuwirth/docs-gpu-initial-target-landed
docs: record GPU initial-target (#148) landing
2026-07-24 15:32:01 +00:00
Levi Neuwirth dcef343702 docs: record GPU initial-target (#148) landing
Move the lane from active-work.md into its Closed section, retire the
protocol v20 / main-hash references to LANDED form in agent-handoff.md, and
record both review-round lessons (failure-socket containment, upgrade-gated
replica publication) in the ops-lessons ledger.
2026-07-24 11:21:50 -04:00
Levi Neuwirth 0dd16a56e8
Merge pull request #148 from levineuwirth/gpu-initial-target
feat(gpu): open an initial file before window startup
2026-07-24 15:18:03 +00:00
Levi Neuwirth b4af798041
Merge pull request #151 from levineuwirth/ci-macos-flakes
test(ci): diagnose the vterm PTY flake; drop the macOS outline budget
2026-07-24 14:59:51 +00:00
Levi Neuwirth 861a048c88 test(ci): diagnose the vterm PTY flake; drop the macOS outline budget
18 of the last 60 CI runs failed (30%), including repeatedly on `main`.
Every failure is macOS-only and hits BOTH Lua flavors, so it is the
runner, not LuaJIT. Sampling 7 showed only two tests.

**m8_9 outline budget** — `outline_5_level_100_entry_renders_within_100ms`
is a wall-clock budget observed at 147ms and 149ms against 100ms on
GitHub's shared macOS runners, while Linux lands comfortably under. The
measurement and its printout now always run; only the ASSERTION is gated
on `!cfg!(target_os = "macos")`, exactly as
`composition_overhead_under_ten_percent` already is in `src/editor.rs`
for the same reason. A real regression still surfaces on Linux, on the
perf gates, and in the number printed to the log.

**vterm PTY smoke** — deliberately NOT a timeout bump. Instrumenting
locally showed the failing wait completes in 40ms against a 10s budget
(250x headroom), while the genuinely tight wait in the same test (2.86s
against 5s) never fails. Four hypotheses were eliminated with evidence:

  - python3 cold start: the sibling test at :494 spawns the same
    /usr/bin/python3 with a TIGHTER 5s budget and passes in the very
    runs where the smoke fails ("3 passed; 1 failed");
  - config resolution: XDG_CONFIG_HOME is read straight from the env
    (src/config.rs:49), no platform branch;
  - a stalled idle loop: the run loop polls on a 60Hz frame timeout and
    ticks the supervisor every frame (src/editor.rs:2522), so child
    output drains without input;
  - a too-small budget: see the 250x headroom above.

The real defect this commit fixes is that NONE of those could be
distinguished from the CI log, which carried only "host output never
contained VTERM_ALT_READY" plus a tail of escape bytes. init.lua now
writes breadcrumbs (reached / terminal.open ok-or-error) and the timeout
reports whether pmacs is still alive plus each breadcrumb, so the next
occurrence names its own cause:

  startup: pmacs still running; init.lua reached="1"; terminal.open="ok"

Verified non-vacuous: forcing the needle to never appear produces the
line above, which also proves io.open works in pmacs's Lua — otherwise
the breadcrumbs would silently read MISSING and mislead.

Test-only; no runtime code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
2026-07-24 10:34:36 -04:00
Levi Neuwirth 907e4d87fc docs: record current-main integration
Point the active-work recovery ledger at the published merge of folding Stage 2
and the GPU initial-target second-review fixes.
2026-07-24 10:22:20 -04:00
Levi Neuwirth 6c06815ee4 Merge githubsucks/main into gpu-initial-target
Integrate folding Stage 2 and its landed-state documentation with the
protocol-v20 GPU initial-target branch. Preserve per-session fold projection
selection in the target bootstrap transaction and retain v19 compatibility
coverage after the later protocol bump.
2026-07-24 10:21:37 -04:00
Levi Neuwirth b3ed40cf60 docs: record published second-review fixes
Point the active-work ledger at the failure-containment and dedup-publication
checkpoint while leaving PR #148 open for user review.
2026-07-24 10:11:24 -04:00
Levi Neuwirth be8c67c30c fix(daemon): contain failed target sessions
Shut down bootstrap sockets on every dispatcher-side failure and reject
frontend events whose session state was never installed. This prevents a
lingering failed client from reaching absent render/size state.

Track target-side CRDT upgrades independently from load/create status so a
deduplicated hidden buffer is published to every existing grid replica. Add
real-daemon regressions for both failure containment and replica publication.
2026-07-24 10:10:31 -04:00
Levi Neuwirth b168dcad16
Merge pull request #150 from levineuwirth/docs-folding-stage2-landed
docs: record folding Stage 2 (#149) landing; open the Stage 3 lane
2026-07-24 14:00:23 +00:00
Levi Neuwirth 7fc7420b91 docs: record folding Stage 2 (#149) landing; open the Stage 3 lane
Post-merge housekeeping owed from #149, kept as its own docs-only PR per
the #138-#140 / #147 convention. No runtime code.

- active-work.md: base snapshot and the recovery check bump 47581f4 ->
  6ed4fe9. The Stage 2 lane is retired and replaced by a folding lane that
  records both stages as merged with nothing in flight, and states Stage 3
  (GPU) has no branch and no framing yet — carrying its named obligations
  (GPU collapse at TUI parity, caret/hit-test fold-awareness, the
  BufferSnapshot fold-mirror clear, CRDT-origin unfold, and flipping
  FrontendView.fold_projection true for semantic frontends) as that
  framing's starting point. "Closed since the last snapshot" gains #149
  and #147.
- agent-handoff.md §1: main @ 6ed4fe9, the "Last updated" line and section
  date, the Stage 2 bullet flipped from IMPLEMENTED/PR-OPEN to LANDED with
  the Stage 3 obligations attached, and the roadmap entry (remaining arcs
  now read "6 folding Stage 3").

Both stages' design points are recorded as traps Stage 3 inherits rather
than as history: the merged-hidden-component unit, per-window/per-target
map instances, per-frontend projection, position-not-row normalization,
and the post-intercept edit site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
2026-07-24 09:58:10 -04:00
Levi Neuwirth 6ed4fe99b7
Merge pull request #149 from levineuwirth/folding-tui
feat(fold): Stage 2 — grid (daemon-rendered) collapse
2026-07-24 13:43:10 +00:00
Levi Neuwirth 0a479ae8d0
Merge pull request #147 from levineuwirth/docs-folding-stage1-landed
docs: record folding Stage 1 (#142) landing; open Stage 2 lane
2026-07-24 13:41:18 +00:00
Levi Neuwirth f16d72fded docs: refresh the ledger to current main and Stage 2's real state
PR #149 review round 5 flagged this PR as stale: it still claimed `main`
@ `c49a8c7`, Stage 2 framing "rev 2, under review; no implementation, no
PR", while `main` is `47581f4` and Stage 2 is implemented and open.

- Base snapshot and the recovery check bump `c49a8c7` -> `47581f4`.
- The folding Stage 2 lane becomes IMPLEMENTED / PR #149 OPEN: framing
  rev 4 approved, the `VisibleLineMap` spine, the base-moved merge (and
  why it was merged rather than rebased), and the five review rounds'
  design-changing findings — each of which is a trap Stage 3 inherits.
- `main`'s ledger had gone unrefreshed through four merges, not one, so
  "Closed since the last snapshot" now also records web grammars HTML +
  CSS (#146) and LaTeX Stage 1 (#144) with its inline-math framing
  (#145), including their durable lessons.
- agent-handoff §1: `main` @ `47581f4`, the "Last updated" line, the
  Stage 2 substrate bullet, and the roadmap entry.

Rebased onto `47581f4` so it stays one documentation-only commit
directly off canonical main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
2026-07-23 21:58:02 -04:00
Levi Neuwirth 5f7270d26a docs: record folding Stage 1 (#142) landing; open Stage 2 lane
Post-merge housekeeping owed from #142, kept as its own docs PR (no
runtime code).

- agent-handoff.md §1: bump main to c49a8c7, add the folding Stage 1
  substrate bullet (store/View, structural source, C-c @ surface,
  command-path unfold, FoldState production; no protocol bump), refresh
  the "Last updated" line and the roadmap Arc 6 entry, and note Stage 2
  is in framing on folding-tui (the visible-line-map reframe).
- active-work.md: retire the Stage 1 folding lane (PR #142 was OPEN),
  add a "Closed since the last snapshot" entry for #142, open the
  Stage 2 (grid/daemon collapse) framing lane on folding-tui, and
  refresh the canonical base snapshot to c49a8c7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
2026-07-23 21:58:02 -04:00
Levi Neuwirth b750000d06 fix(fold): key the managed Lua widening on the EFFECTIVE edit site
PR #149 review round 5, finding 1 — correct, and both round-4 bugs did
survive through the intercept path.

Round 4 moved the widening off the point and onto `edit_start_of(&op)`,
but ran it in `run_buffer_edit` BEFORE `run_managed_edit`. A managed
buffer intercept may legally rewrite `pos` / `start` / `end`
(`LuaInterceptView::intercept_edit`), so the requested op is not where
the edit lands:

  - requested outside -> intercept relocates inside: the edit stayed
    hidden;
  - requested inside -> intercept relocates outside: an unrelated fold
    opened.

The seam still covers BOTH paths — hooking only `run_managed_edit` would
let an interactive `bypass_intercept` edit escape, which is why the
framing put it on the common entry — but each path now keys on its own
effective site:

  - `run_bypass_edit` applies its op verbatim, so `run_buffer_edit`
    hooks it there;
  - `run_managed_edit` hooks after the intercept chain settles and
    before the apply, on the op the chain returned. A chain that raises
    applies nothing, so it unfolds nothing.

The registry borrow is released at that point, and the helper reads only
`SharedCore` + the fold registry, so no borrow conflicts with phase 3.

Three new tests, all through real `M-x` with a real
`pmacs.buffer.add_intercept`: relocate-into-a-fold must unfold,
relocate-out-of-a-fold must not, and a rejected chain must not. Each
asserts the buffer text first, so the test fails loudly if the intercept
stops relocating rather than silently passing.

Suite 45 -> 48.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
2026-07-23 21:45:33 -04:00
Levi Neuwirth 65e500eba1 docs: record published GPU review fixes
Point the active-work recovery ledger at the verified review-fix checkpoint
and record that PR #148 remains open for user review.
2026-07-23 21:27:04 -04:00
Levi Neuwirth bef1c08133 fix(gpu): preserve session isolation during target publish
Keep foreign BufferSnapshot publications out of existing semantic GPU
sessions while retaining grid-replica coherence. Treat dead peer writes as
peer-local failures, restore active-frontend cleanup, deterministic probe
readiness, GPU logging, shared tilde expansion, and accurate docs.

Add focused publication and cleanup coverage and record the two-window
Wayland/Vulkan smoke plus the complete post-review gate results.
2026-07-23 21:25:33 -04:00
Levi Neuwirth 90bb86d355 fix(fold): address PR #149 review round 4
All four findings were correct.

F1 (major) — the Lua widening keyed on the POINT, not the edit site.
Q#FD19 condition (iii) is `edit.range.start`; `run_buffer_edit` discarded
`op` and opened whatever fold contained `window.cursor`. Two wrong
behaviors followed: a command inserting elsewhere opened an unrelated
fold at the point, and — worse — a command editing INTO a fold from an
outside point left its edit hidden. `run_buffer_edit` now reads
`edit_start_of(&op)` before `op` is consumed and passes it down. The
`apply_active_edit` funnel stays point-keyed, which is correct and
deliberate: the six primitives, yank, and query-replace all place point
at the edit site first, and only the Lua path can diverge — now said so
in the doc comment.

The acceptance test encoded the wrong behavior (inserted at byte 0,
expected the cursor's fold to open). Replaced by two tests that pin both
directions, plus the framing's named **comment-toggle** case driven
through the real `M-;` on a file-backed Rust buffer rather than a
synthetic mutator.

F2 (major) — hidden-cursor motion normalized only the row. `move_up` /
`move_down` clamped `coord.row` but derived `goal` from the hidden
line's raw column, and returned early at a buffer boundary without
normalizing at all, so `Up` inside a fold headed on line 0 left the
logical cursor hidden. Added `normalize_cursor_to_visible`, which
projects the whole POSITION through `visible_position` as a real
mutation before any step is computed (and drops the sticky goal column,
since the jump is discontinuous). Paging shares it — the same latent
bug. The old test used equal-width lines and asserted only the line, so
it could not tell the two apart; the new fixture is deliberately ragged
and asserts the resulting BYTE in both directions, plus the boundary
case.

F3 (moderate) — `set_view_top` bypassed the fold clamp. Q#FD12/Q#FD18
say `view_top` is always set through `clamp_view_top`, naming the
`set_view_top` contract specifically. Rendering repaired it at the next
frame, but until then `view_top()` handed out a hidden line and
command/event reckoning could start from a non-visible origin. The
clamp now lives in the setter — the contract's home, what `saveplace`
and `pmacs.editor.set_view_top` call. New test reads `view_top()`
directly after the setter, before any paint.

F4 (moderate) — the suite claimed items 1–14 but omitted approved
clauses. Added: peer SELECTION endpoint projection with hidden interiors
dropped; the crossing-fold repeat for the peer cursor AND a peer
selection endpoint; a style/syntax span straddling a fold (alignment on
the shifted row); the completion popup anchored below a fold; and peer
presence in the split-buffer case using the RECIPIENT window's map.

Suite: 35 -> 45 tests. Eight new bite cases, each falsifying exactly the
line that implements its claim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
2026-07-23 20:46:47 -04:00
Levi Neuwirth 97a27e9873 Merge branch 'main' into folding-tui
`main` moved from c49a8c7 (folding Stage 1, #142) to 47581f4 (web
grammars, #146) while Stage 2 was in framing and implementation. The
text merge is clean, but it is NOT semantically clean: #146 added three
new `Viewport { .. }` literals to `src/highlight.rs`'s unit tests, and
Stage 2 gives `Viewport` a `folds` field. Merged alone, `cargo test
--lib` fails to compile — so the carry-over is resolved here rather
than left for CI to discover.

Merged (not rebased) so the four framing revisions the review rounds
cite by SHA (59410c0, e221f13, 8160d66, 4222ffa) stay reachable.

Full gate suite re-run on the merged tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
2026-07-23 19:43:13 -04:00
Levi Neuwirth e6ace5c320 fix(fold): choose the projection in the crdt-gated daemon fixtures
`cargo test --lib --features crdt` compiles two `#[cfg(feature =
"crdt")]` daemon tests that call `build_fresh_frontend_view`. Both model
a semantic session, so they pass `false` — the Q#FD21 value a
`semantic_render` attach gets until Stage 3. Bet B8 in the framing is
exactly this: the projection is chosen explicitly at every construction
site, never inferred, so a new call site cannot silently default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
2026-07-23 19:37:18 -04:00
Levi Neuwirth b3e18150cc test(fold): make the inactive-buffer unfold guard actually bite
Bite-verification found the Q#FD19 active-window-buffer requirement's
test vacuous: `other` held no fold, so removing the guard changed
nothing — `unfold_containing` on a store-less buffer is a no-op either
way. The guard's real job is to stop the invoking frontend's POINT from
naming a place in a buffer it is not looking at, so the fixture now
gives `other` a fold whose range contains the active window's cursor
byte. Reverting the guard now opens it (0 != 1).

Verified: with `if window.buffer_id != id { return; }` replaced by a
no-op, the test fails on a clean assertion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
2026-07-23 19:33:35 -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 4f1651c8f0 Record GPU initial target PR
Update the active-work recovery ledger with PR #148 and the published
pre-PR checkpoint.
2026-07-23 19:17:11 -04:00
Levi Neuwirth 010a4b52eb Record portable GPU target checkpoint
Pin the published implementation branch and leave PR creation as the next
recoverable action.
2026-07-23 19:15:05 -04:00
Levi Neuwirth 19674a47ea Record integrated GPU target verification
Update the framing, durable handoff, and active-work ledger after integrating
current canonical main and completing the required gates and real GPU smoke.
2026-07-23 19:13:34 -04:00
Levi Neuwirth d6d4be6cfb Merge remote-tracking branch 'githubsucks/main' into gpu-initial-target 2026-07-23 19:03:41 -04:00
Levi Neuwirth 2dd30ec730 Implement session-scoped GPU initial targets
Add protocol-v20 semantic bootstrap and readiness result framing so
`pmacs --gpu FILE` opens the requested path before the GPU window becomes
ready. Keep target identity scoped to the authenticated frontend, preserve
legacy/no-target attach behavior, and publish fresh buffers coherently to
existing replicas.

Carry Unix path bytes and launcher cwd through the root broker, resolve paths
lexically in the daemon, reuse or create buffers without ambient-view state,
and preserve the managed daemon lifecycle from #141. Add focused parser,
wire, lifecycle, hook, isolation, and real-connector acceptance coverage.
2026-07-23 19:03:25 -04:00
Levi Neuwirth 4222ffae6e docs(folding): Stage 2 framing rev 4 — address review round 3
Route command-time visible-line maps through each operation's target
window, while retaining the acting frontend as the projection-policy
owner. Model nested and crossing folds as merged hidden components so
row and byte clamps always resolve to one actually visible head.

Also key projection on the negotiated render selection and correct the
unmerged status of the separate Stage 1 housekeeping PR.
2026-07-23 17:25:55 -04:00
Levi Neuwirth 8160d660a0 docs(folding): Stage 2 framing rev 3 — address review round 2
Round 2's three findings + two nits, all verified against c49a8c7:

- F1 (major): fold-aware motion must be frontend-projection scoped. Shared
  EditorCore::move_up/down/page_* would make a simultaneous unfolded GPU
  session skip source lines it still displays (a grid + a semantic session
  can attach to one buffer, daemon.rs:876). Add a per-FrontendView
  `fold_projection_active` flag (editor_core.rs:240, set at attach / cleared
  at detach); gate ALL command-time visible-line reckoning (motion, paging,
  wheel, click, auto-scroll) on it. Render-time clamps are already
  grid-path-only. New Q#FD21 + simultaneous TUI+semantic acceptance.
- F2 (major): render maps must be per WINDOW, not per frame — paint_frame
  and the presence pass iterate windows with distinct buffer_ids
  (editor.rs:2922, overlay_paint.rs:124). Specify one map per rendered
  nonterminal window (keyed on window buffer_id + TextView); peer presence
  uses the recipient window's map. New split-of-different-buffers acceptance.
- F3 (moderate): hidden positions need COLUMN projection, not only row
  clamping. Add `visible_position_of(pos)` -> outermost fold's range.start
  (end of visible head line, Stage 1's point-move target) for local/peer
  carets and selection endpoints; hidden interiors still drop. New
  hidden-cursor-column-differs acceptance.
- Nits: fix the Viewport<'a> typo; state build cost honestly as O(folds)
  with a byte->line lookup per fold (B4).

PR #147 (the #142 housekeeping) confirmed clean by the reviewer, no findings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5BkezMppbpCgGAYk2ftxV
2026-07-23 16:55:25 -04:00
Levi Neuwirth 47581f4034
Merge pull request #146 from levineuwirth/web-grammars
feat(web): HTML + CSS grammars + HTML script/style injections
2026-07-23 20:23:33 +00:00