Commit Graph

601 Commits

Author SHA1 Message Date
Levi Neuwirth 708054e8d5 ci: put a timeout on every job, cancel superseded PR runs, lint the protocol
Lane 3a of the testing arc --- the three cheap, deterministic items of
`TEST_IMPROVEMENT.md` §5-6. The larger ones (nextest, the
serial/parallel split, a parallel canary leg, the nightly cron, the
macOS matrix trim) are deliberately NOT here: each changes what CI
certifies or how it runs, and each deserves its own decision rather
than riding in on a timeout patch.

`timeout-minutes` on every job (§5.2). Measured before changing rather
than assumed: SEVEN of eight jobs had none and inherited GitHub's
360-minute default; only `m6-perf-gates` had one, at 15. So a single
hung test burnt six hours --- times four on the test matrix --- and
reported nothing useful at the end of it. Set to 25 against a measured
~14.6 min critical path (macOS/luajit), which leaves ample headroom for
a slow runner while catching a hang in under half an hour.

This is the gate that has to exist before `PMACS_REQUIRE_PYRIGHT` can
ever be set. Lane 2 left basedpyright unarmed *because* this did not
exist; the two decisions are the same decision, half a lane apart.

`concurrency` with `cancel-in-progress` (§6.1), scoped to pull
requests. This project rebases heavily --- the ledger re-conflicts on
nearly every merge --- so branches take several pushes while earlier
runs are still going, and macOS minutes are both the expensive ones and
the critical path. Pushes to `main` are deliberately exempt:
`github.event.pull_request.number` is empty there, so the fallback keys
those runs by SHA and none can cancel another. Cancelling a `main` run
would leave the branch-protection record ambiguous about a commit that
has already landed, which is the one place the saving is not worth
having.

`-p pmacs-protocol` clippy (§5.7). The root-package clippy never
covered it --- the workspace default member is only `pmacs` --- so a
warning introduced through a protocol-only change would reach `main`
unseen. Verified passing locally BEFORE proposing it, so it cannot turn
CI red on arrival.

The timeout rationale is stated once above the job list rather than
copied onto each job: the first draft duplicated a seven-line comment
across seven jobs, which is the same degraded-copy shape this arc keeps
removing elsewhere.

Verified: YAML parses; all eight jobs carry a timeout (seven at 25,
m6-perf-gates keeping its tighter 15); `cargo fmt --all --check`,
`clippy -p pmacs-protocol` and `clippy -p pmacs-gpu` all exit 0;
`git diff --check` clean. The diff touches `ci.yml` and the ledger and
nothing else, so no code gate is affected.
2026-07-29 13:05:56 -04:00
Levi Neuwirth cb7fe818bd fix(lsp): close review gaps in workspace edit reporting
Normalize batch dependency paths through the registry's lexical
canonical form so equivalent URI spellings do not revive the
initial-state preflight bug.

Separate execution-started state from the count of completed plan
items. Preflight failures retain the no-mutation guarantee, while
runtime failures conservatively acknowledge that the failing item may
itself have changed a buffer or the filesystem.

Add real-server-pump acceptance for dot-path dependency aliases,
partial text edits within one item, and resource-operation side
effects, and record the review-round corrections in the framing.
2026-07-29 12:29:50 -04:00
Levi Neuwirth 57f85a6fcb merge: integrate main @ 27b1185 (PR #188) into the silent-skip lane
One conflict, in `docs/active-work.md`, with an extra strand: main
gained #188's lane while this branch had removed the documentation
lane, so the two sides disagreed about a region neither had edited
against the other.

Resolved by construction rather than by editing markers --- main's file
taken whole, the documentation lane removed, this lane's block
inserted before the parked lane. Verified against both parents: exactly
26 lines removed relative to main, which is the documentation lane and
nothing else, and the lane-2 block byte-identical to this branch's.

Records, without fixing, that #188's own lane now reads "OPEN,
PROPOSED" on a merged PR. Rule 4 forbids relabelling and allows removal
only once the facts reach `docs/agent-handoff.md`, which #188 did not
touch. That absorption belongs to the immutability arc's next PR, not
to a testing lane reaching across into it.
2026-07-29 12:28:33 -04:00
Levi Neuwirth a07767d310 merge: integrate main after PR 188 lands
Bring the approved generated-buffer immutability framing onto the Stage
1 branch and update both active-work lanes to the landed #188 state.
Main @ 27b1185 changes documentation only relative to the prior base.
2026-07-29 12:28:25 -04:00
Levi Neuwirth 4bc37ab386 docs(active-work): close PR 191 review round 2
Record the revision-7 selection and acceptance reconciliation, the
exact code checkpoint, the non-vacuous fan-out bite, and the final gate
results. Keep PR 188's proposed status and merge ordering explicit.
2026-07-29 12:27:03 -04:00
Levi Neuwirth 0b92d4705f docs: record the pipe-masking gate trap in the handoff
Review round 2 found no new defects; this is the one durable item it
asked to be carried further than a commit message.

`cmd | tail -2` returns TAIL's exit status, not `cmd`'s, in fish and
bash alike. So a gate chain of `cargo test ... | tail -2 && ... && echo
"ALL GATES CLEAN"` prints the clean line even when a suite failed, and
that is what happened while gating this lane: a `pmacs-gpu` failure was
summarized as clean. The point worth keeping is that this is not
carelessness a closer read would catch --- the verdict is structurally
absent from the summary the PR then cites. §5 now says to check
`$pipestatus[1]`, or better to redirect each gate to a file and read it
afterwards, which also preserves the full log that section already asks
for. Filed beside the skip-reports-`ok` lesson, which is the same
family: the thing that summarizes a gate must not be able to lose the
gate's verdict.

Also fixes the doc-comment splice in `tests/support/mod.rs`, where the
why-two-directories paragraph landed mid-sentence and left the
include-mechanics explanation stranded inside it. Cosmetic, and review
called it not worth a round on its own --- folded in here because the
file was being touched anyway.
2026-07-29 12:25:29 -04:00
Levi Neuwirth a81ff917d8 review round 1: stop installing rust-analyzer on macOS, and paste the base
P1 --- the workflow contradicted this lane's own claim. `components:
rust-analyzer` rode the shared `dtolnay/rust-toolchain` step, which
matrixes over ubuntu AND macos, so the binary would have been present
on both. PRESENCE, not PMACS_REQUIRE_LSP, is what decides whether a
gated test body runs --- the unset variables on macOS only meant
absence would be tolerated there, not that the tests stay skipped. Two
rust-analyzer tests would therefore have executed on macOS for the
first time ever, on the legs that are simultaneously the CI critical
path and the documented flake surface, one of them doing real indexing,
none of it covered by the Linux-only local runs behind this lane. Moved
into the Linux-gated step so the text and the workflow agree.

P2 --- the ledger promised a base and pasted nothing. Worse than the
review knew: a script was run to substitute it, reported success,
matched no text, and the result was never re-read. The claim shipped on
the strength of a tool's exit status. The entry now carries the pasted
base and a recovery command, and records the lesson in the terms this
ledger keeps relearning --- asserting a measurement is not making one,
and a tool reporting success is not the measurement either.

P3 --- tool versions pinned (gopls v0.16.2,
vscode-langservers-extracted 4.10.0, yaml-language-server 1.15.0), so
CI no longer drifts with upstream publishes and a break has a commit
here to bisect against. `tests/support/` now states why it exists
beside `tests/common/`: the latter re-exports daemon and PTY
machinery, and pulling that into three binaries that spawn neither to
reach a six-line environment check is the wrong trade. Recorded as a
cost, with the rule that a third such directory means consolidating
rather than continuing.

Also recorded, because §1.2 is NOT fully closed by this lane: the
guards arm the ENTRY skip only, and m4_acceptance's mid-test
"workspace likely still indexing; skipping" survives --- so even armed,
that test's one assertion can still vanish under load, which is exactly
when a regression would show. Mid-test skips want their own pass. And
the follow-up this lane creates: removing the documentation lane
removes the only pointer to githubsucks/handoff-2026-07-20, so that
branch needs deleting after merge or nothing will ever mention it
again.

P4 --- the consolidated lua skip message names PMACS_TEST_LUAJIT again;
the `or_else(locate_shell("luajit"))` path still honours it and the
escape hatch had become undiscoverable. Double blank line before the
parked lane collapsed.

One gate note worth carrying rather than burying. A `PMACS_REQUIRE_GPU=1
cargo test -p pmacs-gpu` run failed once during this round, naming the
`--bin pmacs-gpu` target, and my gate chain still printed a clean
summary because `cmd | tail -2` in an `&&` chain returns TAIL's exit
status, not cargo's. Four subsequent runs pass 202/202 and the failure
has not reproduced, so it is recorded as observed-and-unreproduced
rather than explained. The masking is the durable part: a gate summary
assembled through a pipe can report success over a failure.

Gates re-run after the fix: fmt; clippy -D warnings; --lib 1863; --lib
--features crdt 2048; m4_acceptance 121; m6_5 11; m6_8 8; required GPU
202 (x4); git diff --check clean.
2026-07-29 12:20:28 -04:00
Levi Neuwirth f6dad71190 merge: integrate main @ 5e186c7 after revision 7
Absorb #193 without rewriting the reviewed revision 6 or revision 7
commits. Resolve docs/active-work.md by retaining PR #188's proposed
lane and accepting main's removal of the landed bite lane.

Record that the intervening base change touches no product source, so
revision 7's source readings remain current.
2026-07-29 12:03:06 -04:00
Levi Neuwirth 35b119700f test: arm the silent skips, so external-tool tests stop passing vacuously
Lane 2 of the testing arc (`TEST_IMPROVEMENT.md` §1.2, §5.4).

The shape being fixed reports GREEN when the tool is missing:

    let Ok(_) = which_binary("gopls") else {
        eprintln!("gopls not on PATH; skipping");
        return;
    };

CI installed none of these tools, so a block of real-language-server
and multi-shell tests had never once executed their bodies while
reporting success on every run. A suite that cannot distinguish
"passed" from "never ran" is worse than a missing suite, because it
reads as coverage in exactly the place someone would go looking for it.

The fix is this project's own pattern rather than a new one:
PMACS_REQUIRE_GPU already turns a missing adapter into a hard failure
for the headless render job. This adds PMACS_REQUIRE_LSP,
PMACS_REQUIRE_SHELLS and PMACS_REQUIRE_LUA, and the CI step that
installs the tools they promise. Per-tool variables rather than one
blanket flag, so a tool that must stay unarmed keeps that decision
visible at the call site instead of buried in a workflow file.

basedpyright is deliberately NOT installed and NOT armed. Its test has
no timeout and hangs forever; the root cause is the non-interruptible
reader-thread join in `RuntimeHandles::drop`, already a named deferral
in `src/process.rs`, and the `test` job has no `timeout-minutes`.
Arming it today would trade a vacuous green for a six-hour hang across
four legs. PMACS_REQUIRE_PYRIGHT exists and is never set, so the flip
is one line after the hang fix and the CI timeouts land.

A trap found while writing the workflow rather than after: the natural
Actions idiom

    PMACS_REQUIRE_LSP: ${{ runner.os == 'Linux' && '1' || '' }}

sets the variable to the EMPTY STRING on every other platform, and
`var_os(..).is_some()` is true for `Some("")`. That would have armed
the guard on precisely the runners with none of the tools installed
and failed every one of them. The helper treats empty as unset, which
makes the common spelling safe instead of subtly wrong.

The helper is SHARED via `#[path = "support/mod.rs"]` rather than
copied into three test binaries. `m6_8_multi_repl_acceptance.rs`
carried a comment saying cross-test-binary sharing "would need a
fixture crate"; it does not, and a correct helper in one file beside a
degraded copy in another is this suite's most repeated defect.

Verified by execution in all three states, using a tool genuinely
absent from this machine (vscode-json-language-server): unset skips
green; armed fails hard, naming the CI step that should have installed
it; empty string skips green. On `main` the armed state cannot fail at
all, because no guard exists.

And the question none of this could answer until now --- whether the
tests pass when they actually run --- is answered: armed locally, 11
m6_5 and 8 m6_8 REPL tests are green, and all six real-LSP tests
(clangd x2, gopls x2, rust-analyzer x2) pass individually. The coverage
was real the whole time. It just never ran.

Linux only for now, deliberately: macOS needs the brew equivalents and
roughly doubles install cost on the slowest matrix leg. The variables
stay unset there, so those tests skip cleanly as before.

Also removes the documentation lane from the ledger. Its disposition
was left undecided pending confirmation that its branch carried
nothing unique; measured, `githubsucks/handoff-2026-07-20` is 1 ahead
and 365 behind, and its whole unique diff is four doc files at 42
insertions against 88 deletions --- merging it would REVERT current
documentation. The section asked whoever confirmed that to remove it.

Gates: fmt; clippy -D warnings; --lib 1863; --lib --features crdt
2048; m4_acceptance 121 (unarmed, per CLAUDE.md); m6_5 11; m6_8 8;
PMACS_REQUIRE_GPU=1 -p pmacs-gpu 202; git diff --check clean.
2026-07-29 11:59:08 -04:00
Levi Neuwirth c804dd523c docs(active-work): stop pasting a count that invalidates itself
The entry pasted `4 0`, measured immediately before the commit that
wrote it; at the pushed head the same command said `5 0`. An
ahead-count is self-referential in the file that carries it, so the
entry now pastes only the merge-base — which is stable — and says to
run the count on demand. Behind-count stays the thing that matters,
and the merge-base tells you whether it can still be zero.
2026-07-29 11:58:11 -04:00
Levi Neuwirth 34041a4c85 docs(framing): generated-buffer immutability revision 7
Close review round 6 by moving CRDT quarantine to common divergence
detection, guarding every snapshot and later owner-write route, and
making the acceptance pre-image classifications honest.

Advance the active-work lane to the actual integration checkpoint and
replace the obsolete delete-to-insert recovery rule.
2026-07-29 11:58:06 -04:00
Levi Neuwirth 990bc3e8a7 docs(active-work): re-take the lane position after the #193 merge
The count in the previous commit was measured against `64883eb` and
was stale the moment that merge landed. Re-measured and pasted as
command output. `main` moved twice during this round; the entry now
says so, and says why a stale base matters under branch protection.
2026-07-29 11:51:51 -04:00
Levi Neuwirth 05eaa70df4 merge: integrate main @ 5e186c7 (PR #193, the test-suite audit)
Resolution is a deletion, not a rewrite: `main` REMOVED the
`scripts/bite` lane entry when #192 landed, so this side drops it too
rather than resurrecting a merged lane. This lane keeps its own entry;
its pasted position measurement is re-taken in the next commit,
because the merge itself moves it.
2026-07-29 11:50:37 -04:00
Levi Neuwirth dab0c184e1 docs(active-work): re-measure the base after #193, and say why once was not enough
The previous commit pasted `git rev-parse`/`git log`/`merge-base` output
showing `main` at `64883eb` and "main IS integrated". #193 merged between
writing that and pushing it, so the pasted output was false in the tree
that carried it --- and "main is integrated" is exactly the kind of claim
this review round was about: the tree asserting something the record does
not support.

Re-measured and re-integrated, and the bullet now says the thing the
first attempt got wrong. Pasting command output is necessary and NOT
sufficient: a base measured before the last push is a claim about a tree
nobody has, so it has to be re-measured at push time and treated as
expired on sight. `main` moved three times inside this one lane
(#187 -> #192 -> #193).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-29 11:47:55 -04:00
Levi Neuwirth fe6d38ebfb Merge remote-tracking branch 'githubsucks/main' into generated-buffer-immutability-stage1 2026-07-29 11:45:44 -04:00
Levi Neuwirth 6b21e93e93 fix(lsp): close review round 1 on the resource-op delete guard
Four findings, all reproduced by the reviewer, all accepted. Two of
them are one defect class — a guard whose scope was REASONED ABOUT
rather than enumerated — so both are recorded in the framing's new §9
together with a sweep of every other place this lane decides something
is "affected".

P1 — the delete preflight broke ordered resource operations
(§9.3). Every delete was judged against the filesystem's INITIAL
state, at plan-construction time, so a valid `create X -> delete X`
was refused with a fabricated NotFound about a path the batch was
about to create; likewise `rename A -> B -> delete B`. This was a
regression this lane introduced, not a pre-existing defect.

Decision: DEFER, do not simulate. A delete whose target is related by
component-aware path containment to a path an EARLIER op in the same
plan creates, renames onto, renames away from, or removes is not
judged at plan time; the primitive judges it when it runs. Q#RD3
already calls this check a filter, not a transaction, so declining to
judge is inside its contract and refusing a legal batch is not.
Simulating instead would mean modelling filesystem presence AND the
registry's path bindings across create/rename/edit — the transaction
Q#RD3 declines to build — and a wrong simulation emits false `clear`
verdicts, which is the dangerous direction. `edit` ops are excluded
from the deferral set on purpose: an edit changes no path's existence,
so it can only turn a plan-time `clear` into a primitive-time
refusal, which Q#RD3 already documents and accepts. The
buffer-and-filesystem half therefore still fires early for any target
no prior op touches, which is what criterion 11c pins.

P1 — the production-boundary acceptances are landed (§9.5).
Criteria 11, 11a-11d, 12 (both directions), 13 and 15 now drive a real
`pmacs_fake_lsp` child over a real transport. One parameterized mode,
`applyeditplan`, replaces the eight the framing named: it reads its
whole WorkspaceEdit from a test-written file and publishes the
client's response to a sink, so each of the eight fixtures sits next
to the assertions that depend on it instead of being mirrored across
two files. Fail-closed — an unreadable plan sends no applyEdit and
reports itself through the sink, so a broken fixture cannot read as a
pass — and the sink is written `.part`-then-rename so a polling reader
never sees a partial record. There is no skip-and-return-ok arm
anywhere: `fake_lsp_path` resolves through `env!("CARGO_BIN_EXE_...")`,
a compile-time constant, so a missing binary is a build failure.

P1 — mid-batch failures were misreported as complete aborts (§9.4).
`apply_workspace_edit` now returns `nil, message, applied_op_count`,
and ONE renderer serves both the user-facing status line and the
server's `failureReason`, so the two cannot disagree. All three
callers are updated, not only the rename one.

P2 — non-recursive deletes inspected descendants (§9.2). `recursive`
is now a parameter of the shared query and descendant matching is
reserved for recursive deletes. The old doc comment argued at length
for the wrong behaviour and is replaced by the counterexample that
falsifies it: a modified buffer at `tree/gone.rs` whose file is
already gone blocked a non-recursive delete of the now-EMPTY `tree/`,
an op that would have succeeded and that removes none of that
buffer's contents. This narrows the Q#RD6 query #171 adopts.

Criterion 3's stated bite: fixed by fixing the SETUP, not the doc.
The first commit's test comment carried a correction saying the
framing's wording was wrong. It was wrong only against that setup —
and §9.2's narrowing would then have left the setup with no bite at
all, since a non-recursive delete no longer inspects a descendant.
So the buffer is now bound to the EXACT deleted path: a file is
opened, then replaced on disk by a non-empty directory, and
`remove_dir` fails with ENOTEMPTY deterministically under any uid.
Both of the framing's stated pre-images now bite, so the framing
needed no amendment there. The correction is recorded in §9.1 rather
than only in a test comment, which is where the review asked for it.

WHY THE SHIPPED SUITE PASSED WHILE FINDINGS 1 AND 4 WERE LIVE — two
coverage facts for the next lane. Every delete criterion drove the
PRIMITIVE directly, so nothing in the suite ever built a multi-op plan
and the preflight's plan-time behaviour had no test at all; the only
batch test, `m4_15`, happens to delete a path no earlier op touches.
And every recursive-delete criterion (7, 8, 9) passes `recursive =
true`, while every non-recursive one binds its buffer to the exact
target, so no test in the suite ever combined a non-recursive delete
with a descendant buffer — the exact cell finding 4 lives in.

Sweep, per the review's request. Seven sites decide something is
"affected"; the table is in framing §9.7. Three were the defects
above. Two are unchanged by design and named so they are not mistaken
for oversights: phase-4 reconciliation compares paths RAW via
`BufferRegistry::find_by_path`, which Q#RD10 pins as "exactly today's
behaviour" and which correcting would widen reconciliation — the one
thing Q#RD5 forbids; and `delete_verdict` stats the raw path while
comparing normalized ones, a latent inconsistency whose every branch
fails safe and which matches the primitive's own `remove_file`. Two
are consistent: the `_delete_verdict` binding defaults `recursive` and
`ignore_if_not_exists` the same way the primitive does, and the
deferral set is enumerated (create: 1 path; rename: 2; delete: 1;
edit: excluded, with the argument written down) rather than reasoned
about. Nothing else in the lane decides an affected set.

Bites. Every row was RUN, with the positive control `scripts/bite`
gained in #192 (merged into this lane), and every ref-based row below
reports `OK (assertion)` rather than `OK (COMPILE)`. `1873be6` is this
lane's own first commit: findings 1, 3 and 4 were introduced by it, so
`main` cannot falsify their pins.

  rd11a builtin/runtime/lsp.lua @ main      OK (assertion)
  rd11b builtin/runtime/lsp.lua @ main      OK (assertion)
  rd11c builtin/runtime/lsp.lua @ main      OK (assertion)
  rd11d builtin/runtime/lsp.lua @ main      OK (assertion)
  rd12a builtin/runtime/lsp.lua @ main      OK (assertion)
  rd12b builtin/runtime/lsp.lua @ main      OK (assertion)
  rd13  builtin/runtime/lsp.lua @ main      OK (assertion)
  rd15  builtin/runtime/lsp.lua @ main      OK (assertion)
  rd18  src/lua_bindings/mod.rs  @ 1873be6  OK (assertion)
  rd19a builtin/runtime/lsp.lua @ 1873be6   OK (assertion)
  rd19b builtin/runtime/lsp.lua @ 1873be6   OK (assertion)
  rd19c builtin/runtime/lsp.lua @ 1873be6   OK (assertion)
  rd20  builtin/runtime/lsp.lua @ 1873be6   OK (assertion)

Two rows need their weakness stated rather than hidden.

rd11 is VACUOUS against `main`'s `lsp.lua` and the script says so — a
preflight-less applier passes it, which is expected, because rd11 is
the direction that asserts the guard does NOT over-refuse (the same
shape as criteria 2, 7, 9 and 14). It bites two other ways, both run:
`OK (assertion)` against `main`'s `src/lua_bindings/mod.rs`, where the
primitive's absent-plus-ignore branch destroys the buffer; and against
a hand mutation dropping `ignore_if_not_exists` from the preflight
call, which is the pre-image the framing actually names for it.

rd3's two pre-images are designs never committed, so no ref carries
them and `scripts/bite` cannot be used. Hand-mutated instead:
reconciliation moved ahead of the filesystem mutation makes rd3 fail
on exactly its stated assertion (and rd4 with it). On this setup that
mutation and "validation that removes rather than inspects" are the
same mutation, because the buffer is bound to the exact deleted path —
stated because the first shipped setup could see neither.

The eight rows against `main`'s `lsp.lua` all fail by TIMEOUT rather
than by a value assertion, and that is the pre-image behaviour, not a
flaky harness: on `main` the primitive's raise escapes the applier,
escapes `handle_server_requests`, is swallowed by the
`pcall(handle_server_requests)` at the bottom of the file, and the
server is never answered at all. The sink is therefore never written.
That unanswered request is the defect criterion 13 exists to pin.

Gates: fmt; clippy -D warnings; --lib 1863; --lib --features crdt
2048; m4_acceptance 146 (was 132); lsp_dispatch_seams_acceptance 15;
dired_acceptance 25 and autosave_acceptance 29 (the framing's watch
items); PMACS_REQUIRE_GPU=1 -p pmacs-gpu 202; git diff --check clean.

No protocol change.
2026-07-29 11:45:43 -04:00
Levi Neuwirth 99d026fa22 docs(active-work): rewrite the Stage 1 lane after review round 1
The lane itself carried two instances of the defect the review found in
the tests, so it is rewritten rather than appended to.

- Its "two framing criteria were wrong and the tests say so" bullet was
  a restated contract in the ledger, which is the same shape as the one
  in the test comments. Replaced by a statement of the boundary --- #188
  owns the acceptance contract, this lane adopts it and files findings
  there --- and by what each finding actually was.
- Its bite list said "five criteria are falsified by revert" over six
  enumerated items, and counted the disambiguated-panel criterion among
  them. That criterion fails on `main` only at its fixture premise, so
  it is a mutation bite. Both errors corrected, and the list is now
  split by falsification method rather than by a single count.

The base is measured in the lane with the command output pasted, and
labelled a reading rather than a constant: `main` moved twice while this
lane was open, so a SHA quoted forward from an earlier message would
already be wrong.

Adds the sweep result (7 sites, 4 named by the review, 3 found by the
sweep), the re-run bite results under `scripts/bite`'s new positive
control, and the one deliberately-recorded VACUOUS result --- criterion
7's listview half, which passes under the criterion's own mutation
because `window.switch_buffer` rebuilds the `TextView`. That is measured,
not inferred: the same mutation reports VACUOUS against the listview half
alone and BITES against the dired half.

Also records that the dired 200 ms perf test is load-sensitive rather
than regressed, with the pre-image comparison: 0.09 s either way over
five runs each, so the whole-buffer conversion costs nothing measurable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-29 11:40:35 -04:00
Levi Neuwirth 55c3061003 docs(framing): generated-buffer immutability, revision 6
Answers review round 5 on cab3404 -- five P1, two P2. All seven confirmed
against the tree. Withdraws two of revision 5's decisions and corrects one
assertion it made about containment.

Numbering: the review asked for "revision 5", but the reviewed commit
already carried revision 5 (238fd04, closing round 4), so this is revision
6. Recorded in the document rather than silently renumbered.

Base measured at the moment of writing, not carried from a briefing:
githubsucks/main is 64883eb, merged here at 76cfaac. Every count and every
pre-image is stated as a reading at 76cfaac rather than a constant, because
the base has moved four times while this document has been open. Verified
rather than assumed that 300cbc4..64883eb touches only docs/ and
scripts/bite -- no product code -- which is what lets the source citations
survive the move.

P1-1, criterion 5 impossible. begin_edit and apply_edit each call
ensure_writable as their first statement, so a locked rope returns ReadOnly
and no intercept runs. The named-error requirement moves behind a Rust-side
lift, where the intercept genuinely is the guard; criterion 5 splits into
(a) refused-and-unchanged and (b) refused-by-the-intercept-under-a-lift,
and (b) is where the original bite survives. Consequence revisions 1-5 all
missed: after adoption the intercept is unreachable on the ordinary path
including in the shipped precedent -- terminal.lua's has been dead since
#178.

P1-2, Diverged classifier. Revision 5 got three of seven cases wrong, not
one: export_updates_since runs after every successful CRDT mutation for all
three op shapes, and revision 5 mapped all three to Rejected, whose cleanup
restores a fresh buffer to writable while CRDT and rope disagree -- the
exact harm round 4's P1-3 withdrew the four-variant fallback to prevent.
Replaced by a crdt_mutated flag carried out of the routing function, total
by construction rather than an enumeration a future failure point escapes.

P1-3, containment. Revision 5's "strongest available containment" was
wrong; neither export path reads read_only and neither can, since read_only
is about inbound mutation. New crdt_quarantined flag with three consumers
-- both export sites and queue_daemon_origin_crdt_op, the third not named
by the review. Repair stays deferred and quarantine's cost is stated.

P1-4, stale views. The review named one broadcast site; the sweep found
four, and two it did not name are load-bearing here: buffer.rs:1250 is the
arm every NoOp takes, including Q#GB5's ensure_slot lock, and :1033 is how
a locked buffer's write reaches a replica. All four adopt
continue-and-retain-first-error. Criterion 15a gains a RecordingView after
FailingView, without which it cannot see the gap.

P1-5, selection anchor. The only one of the three coordinates that panics.
Clamp-or-clear in both clamp sites, for every affected window -- and the
rule is not invented: src/terminal/view.rs:715-721 already solved the
identical problem for TerminalSelection, collapsed_by_clamp included. Also
found: a stale anchor reaches the presence broadcast, not only the local
slice.

P2-6, pre-images. Four named, six wrong. Relabelled with a table stating
what the base actually does per criterion, re-derived at 76cfaac and
labelled as a reading at that commit.

P2-7, unfold before refusal. A read-only generated_preflight runs exits 1-4
before the unfold; apply_generated_edit still re-checks all four because
the borrow is released between and Buffer stays the only authority.

Sweep F, recovery mechanisms audited for unenumerated failure points: 10
examined, 4 broken (all four are the review's), and within them three exits
the review did not name.

Sweep G, pre-images re-derived from what the base does: 41 criteria
audited, 6 mislabelled. Arithmetic shown, since a count asserted without
one is what the sweep exists to catch.

Citation audit: PR #187 moved four source files, and 15 stale citation
instances across 9 distinct citations were found and corrected. The tell
was internal -- revision 6's new text cited editor_core.rs:1836 and :1865
for the same two functions older text still cited as :1814 and :1843. A
citation is a reading too, and this document had not been re-reading them.

Records the #188/#191 acceptance-contract boundary verbatim, and the two
divergences it has already had to settle.

Status stays PROPOSED. No runtime code, no protocol change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-29 11:31:51 -04:00
Levi Neuwirth 8a4ea6d522 Merge remote-tracking branch 'githubsucks/main' into generated-buffer-immutability-stage1 2026-07-29 11:27:25 -04:00
Levi Neuwirth 76cfaac780 Merge remote-tracking branch 'githubsucks/main' into generated-buffer-immutability
# Conflicts:
#	docs/active-work.md
2026-07-29 11:25:25 -04:00
Levi Neuwirth 30750310fc merge: integrate main @ 64883eb (PR #192, the bite positive control)
Keeps both ledger lanes: this one and the `scripts/bite` lane the
merge brought in. No product code is touched by the merge.
2026-07-29 11:24:52 -04:00
Levi Neuwirth 9d83fe06e1 wip: revision 6 in progress 2026-07-29 11:24:35 -04:00
Levi Neuwirth 5ae4b784e8 merge: integrate main @ 64883eb, and absorb the landed bite lane
Clean auto-merge of #192. Carries the absorption that merge left owed:
#192's own lane still sat in the ledger describing an open PR, the
fourth time today a PR has merged carrying its own pre-merge lane text.

Removal is legal here without further work because #192 put its durable
facts into `docs/agent-handoff.md` §5 in the same commit --- the
positive control, the two-assertion rule, the NO CONTROL and
assertion/COMPILE/MIXED labels, and the correction to the false
`git checkout --` restore claim. Rule 4's precondition is already
satisfied, so the lane goes rather than being relabelled.

This PR is the natural carrier: it is docs-only, it touches no file any
open PR touches except this one, and it was already integrating.
2026-07-29 11:23:22 -04:00
Levi Neuwirth 8d93fc6978 review round 2: MIXED must not exit 0, and its assumed trigger is wrong
P2 --- the MIXED arm said "OK" and exited 0 while its likeliest cause
is vacuity. Reaching it means cargo failed, at least one `test result:`
line exists, and none says FAILED --- i.e. the named tests may have run
CLEAN against the old tree, the vacuous outcome, with an unrelated
failure supplying the exit status. The message admitted as much while
the exit code contradicted it, so a scripted caller, or a framing doc
quoting "bite exited 0", would record a certified bite. It now exits 4
and is labelled INCONCLUSIVE rather than OK. The exit ladder in the
header gains the code.

While exercising that arm I found the review's stated trigger --- and
my own comment repeating it --- is WRONG, which is worth more than the
fix. `--test A --test B`, where B's swapped file will not build, does
NOT produce MIXED: cargo builds every named target before running any,
so B's build failure stops A from running too and the swapped run
emits no summary at all. Verified by running exactly that shape
against an ancient `tests/m4_acceptance.rs`; it lands in COMPILE, as
it should. What genuinely reaches MIXED is a run that prints summaries
and *then* fails --- doc-tests failing to compile after the lib tests
pass, or a harness dying after its summary line.

So the arm stays as defence but is documented as defensive, and the
ledger lists it as the one path NOT reproduced rather than implying
the validation was complete. Manufacturing a doc-test failure to reach
it would test the arm at the cost of a contrived fixture; saying which
paths were executed and which were not is the more useful record.

Two wording fixes from the same round. The COMPILE message said the
file "does not build against the current tree", but the no-summary
condition also covers a harness that built and then died before
printing one; it now says "produced no test summary (build failure, or
a harness that died before printing one)" so the label never overstates
the observation. And the colour comment no longer overclaims: exporting
CARGO_TERM_COLOR=never defeats an ambient variable, but an explicit
`--color always` in the passed-through args still beats the
environment. Self-inflicted and fail-closed, and now said so.
2026-07-29 11:05:55 -04:00
Levi Neuwirth 1485f7610b review round 1: classify from libtest, and drop a dangling doc citation
P1 --- the classifier could mislabel a genuine bite, and this repo
manufactures the trigger. It grepped the swapped run's output for
`^error\[E...\]` / `^error: could not compile`, but a failing test can
print those lines itself: `tests/compile_mode_acceptance.rs:572` has a
fixture that emits `error[E0308]: mismatched types` at column 0, and
any suite that dumps buffer or PTY text on failure does the same. A
clean assertion failure --- the strong evidence --- would then be
stamped `OK (COMPILE)`, "weaker evidence", while the new handoff text
tells the reader to trust the label. The verdict was never wrong; the
label was.

Classification now reads libtest's own summary instead:
`test result: FAILED` present means the harness ran and tests failed;
no `test result:` line at all means nothing ran. A third `MIXED` arm
covers "some target ran clean but cargo still failed", which the
two-way split silently folded into the wrong side. This also removes
two latent portability bugs for free: `\|` alternation in a BRE is a
GNU extension, so on BSD grep every failure would have labelled as
assertion; and cargo's coloured `error:` output would have evaded the
grep entirely. libtest's summary lines stay uncoloured when piped, and
`CARGO_TERM_COLOR=never` is now exported so `--color always` in the
passed-through args cannot break the parse either --- that would
otherwise have zeroed `count_passed` and produced a spurious
NO CONTROL.

Verified rather than reasoned: a test made to print BOTH
`error[E0308]: mismatched types` and ``error: could not compile
`probe` `` at column 0, then failed by swap, is still classified
`OK (assertion)`.

P2 --- the ledger cited a file that is not in the tree. The lane
scoped itself by `TEST_IMPROVEMENT.md` §8, which is untracked and
machine-local: not on `main`, not in this PR's tree. That is a
dangling reference for any other machine, which is the failure this
ledger exists to prevent. The entry is now self-contained and says so;
if that audit is to scope later lanes it needs committing in its own
PR first.

P3 --- a comment records that both runs are captured rather than
streamed, and why, so nobody "fixes" it back to streaming and breaks
the parser. Double blank line before the parked lane collapsed.

Also, on review's prompting: the #165 wiped-round incident is now
UNEXPLAINED, and the handoff says so rather than leaving a gap the
next reader will fill by re-deriving the false mechanism. Work was
lost; not by the cause this file blamed. SIGKILL bypassing the trap
and a stash collision in the same round are both candidates, and the
note declines to pick one.
2026-07-29 10:55:42 -04:00
Levi Neuwirth 29d02ae1b0 tooling: give scripts/bite a positive control, and correct the handoff
Lane 6 of the test-improvement arc, pulled ahead of its own priority
list because every later lane's evidence is bite-shaped. A verifier
that cannot fail is worth no more than the claims it certifies.

The defect. The script ran only the SWAPPED tree, so "the tests failed
against the old file" was the only thing it ever checked. It could not
distinguish a load-bearing fix from a broken test: a typo, an unrelated
compile break, or a filter matching nothing all make the swapped run
fail, and each printed `bite: OK`.

The control now asserts two things, because passing alone is not
enough --- `cargo test` with a filter matching zero tests exits 0, and
a compile error in the old tree would then still read as OK:

  * the named tests PASS against the working tree, and
  * at least one test actually RAN,

exiting 3 as NO CONTROL otherwise. The swapped run additionally
distinguishes `OK (assertion)` from `OK (COMPILE)`, since an old file
that will not build against the current tree is much weaker evidence:
the tests may never have run at all.

Validated on all three paths rather than the happy one alone. A
zero-match filter reports NO CONTROL. A genuine bite --- PR #190's
`rd1`, which is known to fail against `main` --- reports `control OK
--- 1 test(s) pass` then `OK (assertion)`. A test deliberately broken
so it fails on BOTH trees reports NO CONTROL at exit 3; the previous
script printed `bite: OK` for exactly that case.

Handoff correction, verified rather than inherited. §5 claimed the
script "restores by `git checkout --`, which reverts the file to HEAD",
destroying uncommitted work, and cited a wiped review round during
#165. The mechanism description is false: the script copies the file to
a mktemp path before swapping and restores from that copy under an
`EXIT INT TERM` trap, touching no git state beyond a read-only
`git show`. The commit-before-gating rule is kept --- it stands on
gate-results-describe-the-pushed-tree --- but the false mechanism is
removed, because a reader who believes bite eats uncommitted work will
reach for `git stash` to protect themselves, which is the repo-global
trap the script exists to avoid.

No product code. No tests changed.
2026-07-29 10:12:38 -04:00
Levi Neuwirth e64bebc9c1 docs(active-work): the generated-buffer immutability Stage 1 lane
An open PR is exactly the volatile work this file records, so it gets a
lane the moment it is opened — and the lane rides on its own branch, not
on `main`, because with several PRs open a lane written there
re-conflicts on every merge.

Records the branch and its base (`githubsucks/main` @ `300cbc4`), the
code checkpoint the verification numbers describe, what Stage 1 ships,
what Stage 2 still owes, the two framing criteria that turned out to be
unimplementable as written and what replaced them, the bite result for
every criterion, and the recovery commands.

The canonical-base line above is deliberately left at `7586905`: this
lane names its own base, which is the case that paragraph already covers
("lanes below that name an older base have not been re-based"), and
editing the shared snapshot line while other PRs are open is the
contention this file warns about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-29 09:59:00 -04:00
Levi Neuwirth 1873be6141 feat(lsp): refuse a resource-op delete that would destroy unsaved work
Implements the framing merged as #186. On `main` today,
`pmacs.buffer.apply_resource_op`'s delete arm removes a file and then
removes any buffer bound to it, with no dirty check at any link in the
chain — so a server-driven delete destroys unsaved edits, and the
`ignore_if_not_exists` arm destroys them having done no filesystem work
at all.

Layer 1 — the primitive. The delete arm becomes four ordered phases:
stat/no-op decision, enumerate and validate, mutate the filesystem,
reconcile the registry. Validation inspects and removes nothing, so a
filesystem failure leaves every buffer intact automatically rather than
by compensation, and `on_removed` still observes the path already gone
because reconciliation stays last.

`delete_verdict` is the single shared query. It scans *every*
path-bound buffer rather than the first match, because `find_by_path`
is first-match-only and `pmacs.buffer.from_file` makes duplicates
reachable — a clean first match could otherwise hide a modified second.
It normalizes both sides before comparing and uses component-aware
`starts_with`, so `/tree` does not match `/tree-sibling`. It stats with
`symlink_metadata`, not `canonicalize`, which reports a dangling
symlink as absent and would disagree with the primitive on exactly the
input `ignore_if_not_exists` turns on.

Layer 2 — the applier and the server-request boundary.
`apply_workspace_edit` gains a plan-time delete precondition check
driven by the same Rust helper, so the two layers cannot drift. It is a
filter, not a transaction, and the code says so: `documentChanges` are
sequential, so an earlier edit can dirty a buffer a later op deletes.
The applier is now total — every failure becomes `nil, message`, and
the origin buffer is restored on the failure path as well as the
success path. At the boundary, parse *and* apply are wrapped:
`_parse_workspace_edit` sits one line above the applier and is
fallible, so a parse failure previously escaped, was swallowed by
`pcall(handle_server_requests)`, and left the server unanswered — the
defect being fixed, one line out of scope. Failures now also append one
labelled record to `*errors*`.

Scope, stated plainly rather than implied by what is present:

  * Acceptance criteria 1-10, 14 and 16 land here — 11 tests driving
    the primitive directly. Criteria 11, 11a-11d, 12, 13 and 15 do
    NOT: they exercise Layer 2 through a real server pump and need
    `pmacs_fake_lsp` modes that do not exist yet. Criterion 13
    explicitly rejects a direct-call test as insufficient, so the
    Layer 2 code currently has no production-path pin. That is a real
    gap and the reason this is not the whole lane.

  * The framing's §8 branch plan said the implementation would land on
    #186 itself. #186 merged as framing-only, so it gets its own
    branch and PR. No decision changes.

  * Criterion 3's stated bite in the framing is wrong. It claims to
    fail against buffer-first ordering; it does not, because the
    deleted path is a directory no buffer is bound to, so the
    reordering never fires on that input. It does fail against
    validation that removes rather than inspects. Checked by mutation
    rather than trusted, and the test comment carries the correction.

Bite: criteria 1, 5, 6, 8 and 10 fail against `githubsucks/main` under
`scripts/bite`. Criteria 3 and 4 pin phase ordering against designs
never committed, so `main` cannot falsify them; both were verified by
hand mutation instead. Criteria 2, 7, 9 and 14 assert preserved or
deliberately-unchanged behaviour and pass against `main` by design —
2 is criterion 1's opposite direction, 9 pins today's imperfect
orphaning so widening cannot happen silently.

Gates: fmt; clippy -D warnings; --lib 1863; --lib --features crdt
2048; m4_acceptance 132; lsp_dispatch_seams_acceptance 15;
dired_acceptance 25 and autosave_acceptance 29 (the framing's watch
items); PMACS_REQUIRE_GPU=1 -p pmacs-gpu 202; git diff --check clean.

No protocol change.
2026-07-29 09:47:35 -04:00
Levi Neuwirth cab3404286 merge: integrate main @ 300cbc4 (PR #187) into the immutability lane
Same file, same anchor, third time: `docs/active-work.md`. #187 removed
the #186 and #171 lanes — legally, having first put their facts in
`docs/agent-handoff.md` §1 — and rewrote both the ledger intro and the
bottom-panel lane. This branch still carried all three of those blocks
from the previous integration, so the two sides disagree about most of
the file's middle.

Resolved the same way as that integration, by construction rather than
by editing markers: main's file taken whole, with this branch's #188
lane block inserted between the folding lane and the parked lane, where
it sat before. Verified against both parents — zero lines removed
relative to main, so nothing #187 landed was lost, and the #188 block
byte-identical to this branch's.

Two claims elsewhere in the file were falsified by this merge rather
than conflicting with it, which is the failure mode a marker-by-marker
resolution misses:

  * #187's intro said "one open PR has no lane here: #188". True when
    written, false the moment this merge lands. Rewritten to state the
    reason the arrangement is deliberate, which is what the sentence
    was actually for.
  * this lane claimed integration "to `7586905`". It is now `300cbc4`,
    by way of #189, then #186 and #171, then #187 — three conflicts in
    this one file, all at the same anchor. Recorded rather than
    silently corrected, because the count is the evidence for keeping
    lanes on their own branches.

No framing content changed. No runtime code.
2026-07-29 09:05:35 -04:00
Levi Neuwirth 7c1563c2c2 merge: integrate main @ 6c9e765, and absorb three landed lanes
Merges `main` (PRs #186 and #171) into the 2B-2 branch — a clean
auto-merge, no conflict — and carries the ledger absorption those two
merges left owed.

`main` currently describes three lanes wrongly. The #186 lane still
says "OPEN, PROPOSED, DO NOT MERGE" and the #171 lane still says
"AT REVISION 9, AWAITING APPROVAL", because each PR merged carrying its
own pre-merge lane text; #187's lane says "OPEN" for the same reason
this commit exists. Rule 4 forbids relabelling a landed lane and allows
removing it only once its durable facts reach `docs/agent-handoff.md`,
so no earlier commit could legally fix any of them. This one does both
halves together.

Handoff §1 gains three arc bullets and an advanced anchor (`7586905` ->
`6c9e765`):

  * bottom-panel Stage 2B-2, recording what the review round taught
    rather than what the feature does — a durable transition
    implemented as a per-frame effect is a bug shape and four of five
    findings were that one shape; a producer should be checked against
    the other frontend kind's equivalent; a panel may legitimately be
    wider than a PTY; and mutation testing cannot reach behaviour never
    modelled, which is why 16/16 passed while five real defects stood;
  * dired Stage 2 (#171) as an approved framing with no implementation,
    carrying the order-independence decision and the reason a token was
    rejected rather than built, the already-destructive refused kill,
    path-derived buffer names, and the uncorrelated URI-store writers;
  * the resource-op delete guard (#186) likewise, carrying the
    four-phase ordering, why `find_buffer_for_path` is the wrong
    lookup, that pmacs advertises no `workspace.workspaceEdit`
    capability at all, and that `pmacs.fs.remove` is guarded by
    neither lane.

The ledger then drops both framing lanes (335 lines) and rewrites the
bottom-panel lane to the remaining 2B-3 plan rather than deleting it,
since 2B-3 and Stage 3 are still ahead. Two stale claims elsewhere in
the file are corrected as a consequence: the intro's "#171's lane is
below", and the recovery block's instruction not to start 2B-3 until
#187 lands.

#188 deliberately gets no lane here. Its lane lives on its own branch
and arrives with it — writing one here for work that lands elsewhere is
what makes this file re-conflict on every merge.

No runtime code. No protocol change.
2026-07-28 22:33:12 -04:00
Levi Neuwirth 842a7fb10f merge: integrate main @ 6c9e765 (PRs #186, #171) into the immutability lane
One conflict, in `docs/active-work.md`. Both sides inserted a new lane
block at the same point after the folding lane — this branch added the
#188 lane, main added the #186 lane — and main additionally replaced the
dired Stage 2 lane with revision 9's expanded version. Two independent
insertions at one anchor plus a rewrite of the block immediately below
them is not something the three-way merge can align.

Resolved by construction rather than by editing markers: main's file
taken whole, with this branch's #188 lane block inserted ahead of the
#186 lane. Lane order after the folding lane is now #188, #186, #171.

Verified against both parents instead of by inspection:

  * the resolved file removes zero lines relative to main, so it is a
    pure insertion and nothing landed by #186 or #171 was lost;
  * the #188 block is byte-identical to this branch's;
  * the dired lane is byte-identical to main's revision 9 text.

Ledger staleness carried over untouched, and it is now three entries
deep: the #186 and #171 lanes both still describe themselves as open
and unmerged, and this branch's own lane will join them when it lands.
Removing a landed lane is legal only once its facts reach
`docs/agent-handoff.md`, so none of the three is relabelled here. That
update wants its own carrier.

No framing content changed. No runtime code.
2026-07-28 22:23:16 -04:00
Levi Neuwirth 61202d50c1 Record PR 187 and its review state
Advance the canonical landed-base anchors through the docs-only main
updates, give PR 187 a complete volatile lane, and stop calling Stage
2B-2 the next unstarted slice in the durable handoff.

Record both review rounds, the round-2 code checkpoint, verification,
and exact cross-machine recovery.
2026-07-28 21:59:43 -04:00
Levi Neuwirth 238fd043bf docs: revise generated-buffer immutability framing
Close review round four by preserving applied edits through the
borrow-free fan-out, making the transaction criteria discriminating,
removing the unproven unlock capability, requiring the CRDT divergence
fault seam, and aligning Stage 2 with owner-local identity routing.

Update the active-work lane to carry revision 5 and its cross-lane
facts.
2026-07-28 21:41:19 -04:00
Levi Neuwirth e1359279d0 merge: integrate main @ 0f4e9e0 (PR #186) into the dired Stage 2 lane
One conflict, in `docs/active-work.md`, and it is adjacency rather than
disagreement. PR #186 inserted its lane block directly above the dired
Stage 2 lane heading; revision 9 rewrote that heading from "OPEN, STALE,
DO NOT MERGE AS-IS" to "AT REVISION 9, AWAITING APPROVAL". Git saw a
rewrite abutting an insertion and could not tell they were independent.

Resolved as both, in order: #186's lane block kept intact, followed by
revision 9's heading. Verified in both directions rather than by
inspection — the resolved file removes zero lines relative to this
branch's tip (8f42826), so nothing of revision 9 was dropped, and
differs from main only in the dired lane, so nothing of #186's was.

Note for whoever refreshes the ledger next: the #186 lane block this
merge preserves still reads "OPEN, PROPOSED, DO NOT MERGE". That was
true when main recorded it and is not true now — #186 merged as
0f4e9e0. Removing a landed lane is legal only once its facts reach
`docs/agent-handoff.md`, so it is left alone here rather than
relabelled, per the ledger's own rule.

No framing content changed. No runtime code.
2026-07-28 21:32:45 -04:00
Levi Neuwirth 8f42826355 docs: dired Stage 2 framing rev 9
Resolve review round 8 by making the withdrawn resource ordering
contract singular, assigning every revision-8 acceptance item to 2a,
and specifying one exact-pair LSP tombstone lifecycle.

Replace name/path equivalence with explicit buffer-name provenance,
pin both uncorrelated state creators and tombstone reclamation, correct
the independent-order test's bite, and refresh the active-work lane.
2026-07-28 21:09:11 -04:00
Levi Neuwirth d98d0b3994 docs(framing): generated-buffer immutability, revision 4
Answers review round 3 on 8e032d7 -- three P1, two P2. All five confirmed
against the tree; none re-litigated. Withdraws three of revision 3's own
decisions.

Why revision 3's exactness argument did not extend to CRDT: it proved that
`revision` bumps between the undo push and the on_edit broadcast, which
makes the predicate exact for the v0.1 stack -- and then treated that as a
proof about cleanup in general, when CRDT mutation happens upstream of
`revision` entirely (apply_to_crdt_then_normalize_bytes runs before the
rope edit). The argument was sound about the mechanism it examined and
silent about the one it did not, and stating it as a proof is what stopped
it being checked against the second.

P1-1, the cleanup predicate. Wrong in three directions, not two. Section
3.4 is rewritten around an explicit five-variant GeneratedOutcome reported
by the apply. Direction A is fixed by restating the invariant as a property
of the buffer -- a generated-locked buffer carries no history -- so NoOp
clears; that phrasing needs no reference to `revision` and is therefore
mode-independent, which is the actual repair. Direction B gets a Diverged
variant that clears nothing and surfaces, because clearing would destroy
the last local record of the pre-edit rope; the divergence itself is
pre-existing, reaches apply_edit today, and is deferred as its own lane
rather than pretended to be fixed here. Third direction, found while fixing
the other two and not in the review: the unconditional relock locked a
fresh buffer that was never successfully written -- a mid-codepoint
generated insert on a writable *scratch* returned Err and left it
read-only. Rejected now restores the entry lock state.

P1-2, vacuous criteria 15-16. Confirmed, and the contradiction was internal
-- section 3.4 argued pre-validation makes an invalid range cost nothing,
then the criteria used an invalid range to test post-apply cleanup, in
adjacent sections. Both rewritten around a valid write that fails at the
on_edit broadcast, staged with a Rust-side FailingView (view::View is pub,
attach_view is pub). Criterion 16 splits, because section 3.4 now answers
relock-on-failure and no-lock-on-refusal differently. New 16b and 16c cover
the two P1-1 directions; 16c carries an explicit caveat that its staging
recipe is not verified here, with a four-variant fallback rather than a
criterion that passes by never reaching its path. Rule adopted: every
criterion names the exit it drives the implementation to, and that exit
must be inside the mechanism under test.

P1-3, provenance. generated_lock withdrawn. The defect is not the rule's
details but that a derived fact must be maintained by every mutation of
what it derives from, and set_read_only is pub with callers this document
does not control. Replaced by identity_protected -- a property of what the
buffer is, set once by TerminalSession::open, never written by
set_read_only. The lift/upgrade/restore cycle at
terminal_copy_mode_acceptance.rs:578-584 is transparent to it, so Q#GB12
and Stage 2 criterion 4 need no change, which is the test that the rule is
right. Section 10 now names `--test terminal_copy_mode_acceptance --features
crdt` as its own gate step: acc16e is cfg-gated, so a default-feature run
never compiles it.

P2-4, the registration dependency. Confirmed: revision 3 replaced an
existing guarded optional dependency with an unguarded call, and the
configuration the guard defends is reachable -- LuaHost is pub and nine
test files already build one. Q#GB18 becomes symmetric guarded optionality:
each module answers for its own buffers and consults the other through the
guard shape already in the tree. That also removes the teardown obligation
revision 3's registry introduced. New criterion 21 pins the minimal
harness.

P2-5, the fold criterion. Confirmed. Split into 13a [main] for the
behaviour change and 13b [mutation] for the status string, whose pre-image
cannot be main because on main the call succeeds and sets no status.

Sweep D, rules spanning v0.1/CRDT or fresh-lock/existing-lock: 13 examined,
3 broken (the two the review named plus the relock one), 1 that looked at
risk and verified clean -- undo_crdt_mode does return NothingToUndo
(src/buffer.rs:1374-1376), so Stage 2 criterion 4's assertion is correct in
both modes.

Sweep E, every criterion added in revisions 3 and 4 audited for whether the
state it asserts is reachable by the operation it performs: 19 audited, 3
defects, all fixed here. Also records that S2-17 is the one criterion for
which the pre-validation exit IS the mechanism under test, so it stays as
written.

All counts re-measured at 7586905 with the command output pasted into the
revision-4 block; all unchanged.

Status stays PROPOSED. No runtime code, no protocol change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-28 21:01:31 -04:00
Levi Neuwirth c7ad4f491f docs(framing): revision 5 of the resource-op delete guard
Review round 4 accepted the core design and the two surfaced choices,
then found two remaining contract defects and one ledger-ownership
defect. Close all three without starting runtime implementation.

Narrow the last normative "always answers" consumer to Q#RD7's honest
"always attempts while the response channel remains live" contract.
Strengthen the audit procedure to search normalized prose and term
stems so Markdown emphasis cannot hide a consuming site from a literal
phrase sweep.

Replace Q#RD12's incomplete no-op/clear/conflict table with a total
no-op/clear/refuse verdict exposed through one named internal binding.
The shared Rust helper now has an explicit mapping for missing with and
without ignore, non-NotFound stat errors, modified buffers, and
editing-in-progress buffers. Specify how both the primitive and Lua
preflight consume the same result. Add acceptance 11c for known
NotFound before earlier batch mutation and 11d for fail-closed stat
uncertainty.

Stop rewriting #171's lane from #186. Restore the sibling block exactly
to main, retain only the stable cross-lane ownership split, and record
that #171 owns its volatile state. Sequence #186 before #171 because
the urgent data-loss guard is the dependency #171 adopts, not to make a
textual conflict cheaper.

Correct #186's own ahead count to include this revision commit and add
the previously omitted lsp_dispatch_seams_acceptance file to the lane's
touch list.

Still PROPOSED. No runtime code. Implementation begins only after
explicit user approval.
2026-07-28 20:55:01 -04:00
Levi Neuwirth 46b3c8db1a docs(framing): revision 4 of the resource-op delete guard
Review round 3 accepted the core --- pre-filesystem refusal, four-phase
ordering, the #171 split, Q#RD10 --- and raised four P1s and a P2. All
accepted.

The external-claim audit added in revision 3 certified this document as
clean while Q#RD3, the normative decision, still asserted the very LSP
claim the audit's own row 4 marked WITHDRAWN. It did so because it
checked each claim where it was DEFINED and never at the sites that
CONSUMED it, so a withdrawal recorded in one section coexisted with the
same claim doing load-bearing work three sections away. That is a defect
in the audit procedure rather than a missed line, and since the table is
now an artifact readers trust, section 1.15 records both the failure and
the corrected procedure: grep the whole document per row and check every
hit.

P1, lane state. Re-measured: `git rev-list --left-right --count
fd7ae37...7586905` returns `13  2`, so #171 is two commits behind, not
zero, and those two are exactly PR #189's COHERENCE change. Revision 3's
"0 behind" was taken against `ad41cf1` and reported in present tense
after main had moved. Both ledger lane entries are corrected in place
--- #186's revision and base, and #171's superseded `ab42a79` /
153-behind / merge-base-`c8ec8f3` ground truth --- rather than having a
correction layered above stale text, which is what made the ledger
self-contradicting. Every count now appears as pasted command output.

P1, withdrawn claim surviving. Q#RD3 called partial application
`FailureHandlingKind.Abort`, "the strategy the spec itself assigns to
any edit containing resource changes", and section 1.11 called `Abort`
the default "by omission". The spec establishes no default for a client
advertising no strategy. Both sites now say only that verified pmacs
behaviour resembles abort-style application, resting on the reproduction
in section 1.6.

P1, acceptance 15. `WorkspaceEditResponse::from_lsp_value` returns
`Self`, its doc says a shapeless result yields an empty response, and
the binding's only `?` is `lua_to_json` over a value that arrived
through `json_to_lua` --- so no server payload can make the parse fail
and the criterion could not fail. Decision, new Q#RD11: keep the wrap,
drive the test with an explicit throwing stub, label it defensive.
Q#RD7's promise narrows to "always attempts a response while the
response channel remains live", since `send_response` is itself under an
ignored `pcall`.

P1, absent-plus-ignore. `pmacs.fs.stat` dispatches async and
`canonicalize` resolves symlinks and returns nil for a dangling one, so
it disagrees with the primitive's `symlink_metadata` on exactly the
input this query turns on. New Q#RD12 specifies a structured Rust-backed
verdict --- no-op / clear / conflict --- evaluated with the same
`symlink_metadata` call, with an error contract that fails toward
refusal. New criteria 11a and 11b supply the missing opposite direction.

P2. Criterion 14's "fails in both directions" was false: with both
duplicates clean the setup cannot distinguish first-match validation
from full validation. The claim is fixed rather than the setup, because
criterion 6 already pins validation breadth. Section 8's touch table and
section 7's gate list are reconciled.

Sweep for corrections applied at one site while a dependent site kept
the old claim: count 4 --- the two `Abort` sites above, and the stale
#171 count in both the revision history and section 1.12. Nine other
withdrawn or revised claims were checked at every consuming site and
found clean.

Still PROPOSED. No runtime code. Implementation begins only after
explicit user approval.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-28 20:31:36 -04:00
Levi Neuwirth 7ecea94693 docs: dired Stage 2 framing rev 8 — round 7, five contract defects
All seven citations re-verified against the tree; all seven hold, one is
understated. The split with #186 is untouched.

Revision 7's process read every mechanism it CITED but never read the
mechanisms it RELIED ON: it verified that tick exists and returns
settled ids without reading that the drain is a bare try_recv with no
execution token, and it verified that kill_buffer exists without reading
the order in which it mutates before it can refuse. Reading a symbol to
confirm it is there is not the same as reading it to confirm it does
what the surrounding argument needs.

F1 — reply order is not execution order. AsyncRuntime::tick
(src/async_runtime.rs:1003) drains the reply bus with no execution
token, so a worker can finish, be descheduled before sending, and have a
later reply arrive first. Asked the cheaper question first as directed:
reconciliation does NOT need to be order-dependent. Independent
mutations commute; interdependent ones cannot arise from any production
path (dired serializes, apply_resource_op is synchronous, the fs
primitives have zero production callers); and fs.lua:155-165 already
instructs callers needing ordering to serialize. So the guarantee is
withdrawn rather than engineered. A token under a mutation lock was
rejected: it serializes every fs mutation to close a hazard with no
production reachability and a documented caller-side remedy. No static
ordering rule is offered because none works — worked through in §6.

F2 — preflight, and the review understates it. kill_buffer clears
round_trip_buffers, closes side windows and redirects ordinary windows
before BufferRegistry::remove can refuse. Rev 7 said to treat the
refusal as "keep the buffer", which reads as though skipping phase 2
restored something; it does not. reconcile_delete now preflights
editing_in_progress — sound because phase 1 is pure EditorCore with no
Lua handle, so nothing can re-enter between check and removal.
Acceptance 53b is three separately-asserted properties, each with its
own named bite.

F3 — path-equivalence, not string equality. Names come from
path.display() as given while only file_path is normalized, so a
relative open leaves a short name a string rule mistakes for
user-chosen. Tested both directions; the custom-name arm is what stops
the fix becoming a name-clobberer.

F4 — bounded tombstone, and the census corrected this revision's own
first answer. A first pass checked only handle_notification and
concluded publishDiagnostics was the only uncorrelated writer. Wrong
lens boundary: mark_document_stale takes no LspServerId and creates URI
keys across three stores for every server. The full census also found
that diag_store has zero correlated writers and that
DiagnosticStore.by_uri is keyed by URI alone with no server component —
so the store most needing protection is the one a route purge cannot
help, and a (sid, uri) tombstone would not match it. Recorded as the
round's own defect class occurring inside the round.

F5 — three outcomes, because pmacs.minibuffer.read already has three.
Cancel does not fall through to on_no: for C, declining means copy the
non-colliding entries while cancelling means abandon, and conflating
them would make C-g perform a partial copy.

Sweep for the same defect class: three candidates, two cleared by
reading (run_all_must_succeed really does collect and not abort;
commit_to_refuses_an_await_and_restores really exists), one real and not
raised by review — the framing never said which HookKind the new hooks
register with, and short-circuit would let one subscriber silently stop
every later one reconciling. Both are now all-must-succeed.

P2: pmacs.lsp.forget_uri named with an error contract and pinned; the
ledger note rewritten — it claimed single-file scope while that same
revision edited the ledger — with both line counts measured at this tree
rather than quoted.

Status unchanged: PROPOSED, needs explicit user approval. No runtime
code. Do not merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-28 20:30:29 -04:00
Levi Neuwirth 8e032d75d8 docs(framing): generated-buffer immutability, revision 3
Answers review round 2 on #188 @ 516bd35 -- three P1, two P2 -- integrated
to main @ 7586905 (#189, COHERENCE.md only; clean merge).

P1-1, the route cannot write to a locked buffer. Confirmed, and the
review's first repair option cannot work: reordering begin_edit's two
checks leaves ensure_writable running one line later, so a locked buffer
is still refused and only the reported error changes. Any admitting path
must not reach ensure_writable at all. Q#GB3 is reversed -- generated
writes get their own run_buffer_edit arm and never touch begin_edit -- and
new section 3.4 specifies the transaction: one &mut Buffer method, eight
named exits with the post-state of each, relock and editing_in_progress
clear unconditional, bounds pre-validated so an invalid range costs no
history, and history cleared iff the revision advanced (exact, because
revision bumps after the undo push and before the on_edit broadcast).
Proved along the way that no Lua runs inside a generated write:
LuaInterceptView implements only intercept_edit and inherits View::on_edit's
no-op default, so the only re-entrancy direction that needs a gate is
outward.

P1-2, disambiguation. New section 2.10 is the census: 19 units across 14
grep lines, five classes, arithmetic shown, each classified by reading its
enclosing function. Two sites break; between them six downstream consumers,
not three. The fourth listview consumer is listview.open's own
never-capture-a-panel guard, which fails inverted and silently -- it
captures the panel as its own q target, the exact loop its comment says it
prevents. compile's slots table is NOT affected (keyed by a module
constant, slot_for_buffer is id-based). New Q#GB18 routes both by owned
BufferId, and notes listview registers no on_removed so its new list must
compact on scan like dired's live_handles.

P1-3, unlock_generated. Withdrawn as written; both halves of the finding
hold. Sweep C found the mirror-image hole the review did not: the SHIPPED
set_generated_contents lifts a lock it did not install, so it can already
overwrite a live terminal identity buffer. Provenance (new Q#GB15,
generated_lock beside read_only) therefore earns its cost in both
directions rather than existing to make the unlock safe. Q#GB7 moves to
Stage 2, its claim narrows from "brick escape" to "the closure of the
capability { generated = true } adds", and the asymmetry with the standing
no-Lua-set_read_only policy is addressed directly: a provenance-bounded
unlock adds no reachable state that { generated = true } did not.

P2-4, criterion 6. Confirmed, and the framing cited the wrong precedent:
terminal_copy_mode_acceptance has zero dispatch_idle_for references. The
model is dired_acceptance.rs:969. dispatch_idle_for has six ways to return
false and the review named one, so the criterion now carries three halves
-- document-window premise, the gate, and acc16's positive control, which
rules out the other five clauses in one assertion.

P2-5, the path-backed refusal. Now exercised on all four surfaces with
guard misplacement as the explicit bite.

Sweep C (obligation 3) also found that read_only is one boolean serving
three policies: the generated lock, terminal identity, and -- as a reader
-- fold.rs:313's "is this a document buffer" test, pinned by
folding_acceptance.rs:570. Locking five families silently disables
pmacs.fold.fold on all five, which contradicts Q#GB3's own stated rationale
for preserving the unfold seam. New Q#GB16 names it and recommends
accepting it with a criterion rather than accepting it silently.

Two further corrections the review did not name. COHERENCE.md section 14's
listview list was corrected by #189 while revision 2 was open, so section
1.5 goes from owed to landed -- a merged correction is removed, not
restated. And *help* has two independent writers, src/help.rs:354 (Rust,
apply_edit, mark_clean) and default.lua:1239 (Lua, no mark_clean), each
with its own find-by-name and its own copy of the name constant, so the
inventory's headline is five mechanisms, not four.

Section 2.9's is_modified sweep is corrected a second time: seven
production consumers, two load-bearing. Revision 1 said two, revision 2
said four. The conclusion is unchanged but was reached twice from an
incomplete count.

New section 9b records the cross-lane boundaries verbatim and confirms
Q#DR25 against #171 revision 7 at fd7ae37. Revisions 1 and 2 never
mentioned Q#DR25 at all despite owning it; revision 3 claims it, and
carries #171's trap that dired_acceptance.rs:969's read-only status
assertion passes both before and after adoption.

Twelve acceptance criteria added or rewritten, each stating what it asserts
about content produced and the mutation that falsifies it. Stage 2 gains
criteria for the transaction's error paths specifically -- a leaked
editing_in_progress asserted through the NEXT edit, not the failing one,
because the failing call reports the same error either way.

Why revision 2 shipped these: it audited criteria against pre-images
(sweep A) and capabilities against who may use them (sweep B), but never
read the guards its own mechanisms had to pass through. begin_edit,
document_bytes and terminal/session.rs:305 were all cited elsewhere in the
tree and none was opened. The process fact for revision 4: for every seam
the design routes through, read the callee, not the caller's description of
it.

Status stays PROPOSED. No runtime code, no protocol change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-28 20:26:18 -04:00
Levi Neuwirth aea5097890 docs(framing): revision 3 of the resource-op delete guard
Review round 2 confirmed everything central from round 1 as fixed and
raised four P1s. All four accepted; both requested sweeps run. Rebased
onto `7586905`.

P1-1, the ownership boundary was stale. Revision 2 described PR #171 as
"OPEN, STALE, 153 commits behind, under re-scout" claiming the rename
side only. Re-checked directly: #171 is at revision 7, `fd7ae37`,
merge-base `ad41cf1`, zero commits behind, and its revision 6 had
assigned rename AND delete to Stage 2a with the opposite policy --- file
deleted, modified buffer kept alive and orphaned, that orphaning named
as accepted residue. Two lanes, opposite answers, same event. The
settled split is now carried verbatim in the framing and the ledger, and
#171 revision 7 has adopted it from its side.

P1-2, the LSP failure-handling claim was wrong. The spec does not assign
`Abort` to resource-op-bearing edits. Recovery is described by the
client's advertised `workspace.workspaceEdit.failureHandling`; `Abort`
is one of four strategies, `Transactional` covers all operations, `Undo`
attempts rollback, and only `TextOnlyTransactional` degrades to abort
when resource changes are present. pmacs advertises none, so the spec
assigns pmacs no strategy at all. Section 1.7 and B2 now rest on
verified repository behaviour --- the reproduced partial batch --- not
on borrowed protocol authority.

P1-3, Q#RD7 had no implementable seam. Three gaps confirmed by reading:
`_parse_workspace_edit` is called one line above `apply_workspace_edit`
and is fallible, so the proposed wrap left "always answers" untrue;
`append_to_errors_buffer` is private and a Lua preflight rejection never
reaches Rust, so the promised logging was not implementable from where
it was promised; and acceptance 13 tested the response but not the
trace. Q#RD7 is rewritten around one seam at the server-request
boundary, and of the two options offered this revision picks wrapping
parse-plus-apply rather than narrowing the claim.

P1-4, clean duplicate reconciliation is now Q#RD10: validate every
match, reconcile today's first exact-path match only. Widening would
enlarge the parked lifecycle defect Q#RD5 exists to contain; the
surviving clean duplicate is named as residue handed to #171.

External-claim sweep. Every non-repo claim is now listed in a new
section 1.15 with its evidence. One was a paraphrase standing in for a
quote: "in Emacs `kill-buffer` on a modified file-visiting buffer
prompts". It is true, but the gate is `INTERACTIVE`, defined as
`(NILP (Vexecuting_kbd_macro) && !noninteractive)` --- keyboard present,
not `call-interactively` --- so eglot's programmatic kill does prompt in
a normal session and does not in batch or during a keyboard macro. The
revision 2 sentence was right for a reason it never established and
false in two environments it never considered.

Cross-lane sweep. Q#RD8 said mode (d) needs its own lane; it has one now
--- #171's `reconcile_delete` composes both removal phases. Q#RD6 claims
the shared walk query explicitly so the duplicate resolves in one
direction. And `pmacs.fs.remove`, guarded by neither lane and verified
to have zero production callers, is named as explicitly out of scope
with its owner rather than left to read as covered.

Still PROPOSED. No runtime code. Implementation begins only after
explicit user approval.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-28 19:01:19 -04:00
Levi Neuwirth e1e9b44154 docs(framing): revision 2 of the resource-op delete guard
Review round 1 approved the refusal strategy in principle and rejected
revision 1 as written. Six blocking points, all accepted, plus three
further overclaims found by the requested sweep. Q#RD1 and Q#RD5 are
settled yes; Q#RD9 is settled no and is withdrawn.

Q#RD2 conflated inspection with removal. Revision 1 removed the buffer
before the filesystem call, which fires arbitrary Lua `on_removed`
callbacks while the file still exists and accepts losing the buffer if
the deletion then fails. The sequence is now `stat/no-op -> enumerate
and validate -> mutate filesystem -> reconcile`. Validation inspects
`is_modified` and `editing_in_progress` without removing anything, so a
failed deletion leaves buffers intact automatically and `on_removed`
still observes the path already gone.

Q#RD3 overclaimed whole-batch atomicity. `documentChanges` are
sequential, so an earlier edit can dirty a clean buffer and an earlier
rename can move a modified buffer into a later delete's subtree, after
the snapshot. LSP 3.18 assigns `FailureHandlingKind.Abort` to any edit
containing resource changes --- "all operations executed before the
failing operation stay executed" --- so the protocol itself declines to
promise what revision 1 claimed. The preflight is now described as an
early conflict check, with robustness coming from per-op `pcall`, an
always-sent server response, and best-effort origin restore.

The lookup cannot be `EditorCore::find_buffer_for_path`: it normalizes
but delegates to the first-match-only `find_by_path`, and
`pmacs.buffer.from_file` creates path-bound buffers with no dedup, so a
clean first match can hide a modified second. Q#RD6 now requires a full
scan with component-aware `Path::starts_with`.

Recursive deletion now inspects the tree but reconciles only the exact
path, so the parked lifecycle defect stays exact-path rather than
becoming tree-wide. Q#RD4 holds at both layers, so the preflight cannot
reject an absent path the primitive treats as a no-op.

The prompt argument was overclaimed and that was my error.
`pmacs.lsp.send_response` takes `request_id` as an ordinary value, so a
`workspace/applyEdit` can be answered on a later tick, and a callback
continuation would reuse the existing minibuffer shadow rather than add
a seventh dispatcher rung. Prompting is expensive and separately
scoped, not impossible; the section now claims only what the evidence
carries.

Sweep found three more of the same defect class --- an absence or a
guarantee asserted rather than established:

  * a durable error surface does exist (`append_to_errors_buffer` ->
    `*errors*`), so Q#RD7 now records the refusal there as well;
  * no caller reliably surfaces a raise, because the async path routes
    uncaught coroutine errors through the undefined `pmacs.error`;
  * pmacs advertises no `workspace.workspaceEdit` capability at all ---
    no `documentChanges`, no `resourceOperations`, no
    `failureHandling`.

Adds seven acceptance pins with their bite obligations, adds the
`docs/active-work.md` lane the ledger requires for every open PR, and
drops the two-PR plan: #186 is revised in place and becomes the
implementation PR.

Still PROPOSED. No runtime code. Implementation begins only after
explicit user approval.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-28 18:49:24 -04:00
Levi Neuwirth aae113ba76 docs(framing): guard the resource-op delete arm against data loss
`pmacs.buffer.apply_resource_op`'s delete arm removes the file from
disk and then unconditionally drops any buffer bound to that path.
There is no dirty check at any link in the chain --- not in the arm,
not in `remove_buffer_and_fire`, and not in `BufferRegistry::remove`,
whose only guard is `editing_in_progress`. A buffer with unsaved edits
is destroyed and the file that would have held them is already gone.
Reachable today through any language server's `WorkspaceEdit`.

Reproduced four ways against `ad41cf1`, by throwaway tests run in this
worktree and removed before commit:

  a. the reported bug --- op returns `Ok(())`, file gone, buffer gone;
  b. `ignore_if_not_exists = true` does zero filesystem work and still
     destroys the buffer (the `create` arm's early return was never
     applied to delete);
  c. `recursive = true` reconciles nothing, so a whole tree leaves
     orphaned buffers --- the most destructive arm does the least
     reconciliation, and it bypasses any exact-path guard;
  d. removal is not `kill_buffer`: windows are left bound to a removed
     `BufferId` and the registry can be driven to empty.

Recommends refusing before touching disk, at two layers: the Rust
primitive reconciles the registry first so the guard is expressible at
all, and the applier's existing URI preflight gains a second
precondition so the whole batch aborts with its documented
`nil, message` contract rather than half-applying.

Prompting is rejected on evidence: prompts are callback-continuations
resumed by a later keystroke, so a synchronous Rust binding cannot
issue one; the alternative is a seventh dispatcher shadow; and the
server-initiated path must answer `workspace/applyEdit` synchronously
with no user turn available. Backing up is rejected because removing
the buffer purges its autosave recovery file. Emacs prior art (eglot's
`do-delete`) kills the buffer before deleting the file and confirms
server-initiated edits as a whole-batch decision taken before any
mutation --- the same shape, in the phase pmacs already has.

Framing only. No runtime code. PROPOSED --- needs explicit user
approval before implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-28 18:49:24 -04:00
Levi Neuwirth fd7ae37071 docs: dired Stage 2 framing rev 7 — cross-lane reconciliation with #186
PR #186 frames a guard for apply_resource_op's delete arm that refuses
before touching disk when any affected buffer is modified. Rev 6 said the
opposite for the same call site: the file is deleted, the modified buffer
survives orphaned, and §11 named that orphaning as accepted residue. Two
lanes, two answers.

Rev 7 adopts the settled split, recorded verbatim in the framing and in
the ledger lane so the two cannot diverge again: #186 owns the urgent
pre-filesystem refusal for synchronous apply_resource_op; #171 later owns
full post-delete lifecycle reconciliation, including the async race where
a buffer becomes modified after dired dispatch. Not re-litigated.

Census of orphaning claims: 20 matched lines resolving to 13 distinct
passages, classified by reading each enclosing passage rather than the
matched line. 4 ownership claims (this lane closes the LSP data-loss
defect) reassigned to #186; 4 policy claims (LSP intentionally deletes
and orphans) deleted; 3 ground-truth statements kept, because main really
does destroy unsaved work on that arm, but attributed to #186 as fixer;
2 async-race claims kept and narrowed.

Q#DR18 is rewritten, not withdrawn. The reconcile_delete seam survives
and is explicitly not cancelled. Three changes: the synchronous path
refuses before disk (#186 Q#RD1, adopted not re-decided); the policy
becomes symmetric rather than asymmetric; and the walk rule is adopted
from #186's Q#RD6 rather than reinvented — scan every path-bound buffer,
normalize once, component-aware Path::starts_with — which is
character-for-character what rev 6 wrote independently. Whichever lane
lands first owns the query, per #186's own boundary wording.

DeleteReconcile.kept_modified stays and is not dead. The synchronous
caller can no longer produce it; the asynchronous one still can.

Residue kept, and narrowed to one path: dired never goes through
apply_resource_op — it calls pmacs.fs.remove, which dispatches a worker
that an in-applier guard cannot see. So the LSP path has no residue and
dired's is its own. Rev 6 called it "one deferral, two paths"; that was
backwards. Also newly named: pmacs.fs.remove itself is guarded by
neither lane, since both guards sit one layer above it. Latent — zero
production callers.

The full post-delete lifecycle stays here, and #186 hands it over
explicitly rather than by omission: its Q#RD8 parks the window and
last-buffer defects and independently reaches rev 6's R4 finding that
kill_buffer and remove_buffer_and_fire clean disjoint sets; its Q#RD5
keeps reconciliation exact-path precisely so as not to promote those
defects tree-wide. That parked work is this lane's Q#DR27.

LSP failure handling: this document never made the claim — grep returns
10 lines, all about hook fan-out or dired's batch semantics, none about
WorkspaceEdit recovery. Verified the spec anyway and recorded it, since
the two framings will sit side by side: only textOnlyTransactional
degrades to abort for resource ops, transactional covers them, and LSP
3.18 states no default for a client advertising no failureHandling.
Rev 6's one adjacent claim — "refusing mid-edit leaves a half-applied
refactor" — was the load-bearing support for the deleted policy and goes
with it.

Noted without re-litigating: #186's Q#RD5 cites the ledger's "OPEN,
STALE, 153 commits behind, under re-scout" assessment of #171 in support
of taking the delete side now. That citation is stale — the re-scout is
done and this lane is integrated to ad41cf1 — but the conclusion stands
on urgency alone, so nothing about the split changes.

Status unchanged: PROPOSED, needs explicit user approval. Do not
implement, do not merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-28 18:46:09 -04:00
Levi Neuwirth 516bd3579a docs(framing): generated-buffer immutability, revision 2
PROPOSED. Framing only; still no runtime code.

Answers five review findings on PR #188 at 9101bf5 (three P1, two P2),
plus the two sweeps the review asked for by class.

P1-1: Stage 2 criteria 3, 4 and 5 all passed on the pre-image. Ordinary
edits are already refused by the existing intercept, and Buffer::undo
checks read_only before it inspects history, so "undo returns false"
passes against an implementation that locks and never clears. Rewritten
to use a bypass write or Rust-side is_read_only for the lock, and to lift
the lock inside a Rust test before asserting NothingToUndo.

P1-2: ownership by handle is now a prerequisite of the stage that locks
each writer. Measured: a foreign *references* is clobbered and left
permanently un-editable, a pmacs.compile.run that raises on validation
still leaves a foreign *compilation* un-editable, and M-x buffer.undo is
currently the only recovery -- which this arc removes.

P1-3: revision 1 was wrong that is_modified drives only the mode line.
autosave.rs:363 and desktop.rs:302 read it. The rule chosen: a generated
write refuses a buffer that has a file_path.

P2-4: view_top is a line index, cursor is a byte position. The clamp is
now per-coordinate and ungated.

P2-5: re-measured at ad41cf1, 276 dark, with the command shown.

Sweep A found Stage 1 criterion 7's stated bite was wrong, and that eight
criteria pass on main by design without saying so; every criterion now
carries a pre-image class. Sweep B found the two halves of the protection
are asymmetric -- the intercept is removable, the rope lock is not --
which falsifies revision 1's reason for deferring the unlock, so Q#GB7 is
upgraded to Stage 1 work.

Adds the PR #188 lane to docs/active-work.md, which the ledger now
requires for every open PR.
2026-07-28 18:32:05 -04:00
Levi Neuwirth bc16716b86 docs: dired Stage 2 framing rev 6 — review round 5 (4 P1, 2 P2)
Revision 5 was reviewed and not approved. Round 5 has one theme, not
six: rev 5 changed the slice split and the ownership of a decision, and
the prose did not follow. Four of the six findings are that same defect
in different places. All six verified against the tree before acting;
two are understated by the review.

R1 (P1) — rev 5 was superseded by its own ledger entry and by a PR body
older still. active-work.md:507 records Q#DR25 as moved out of this lane
(dired's paint is one instance of a CLASS bug — the same
erroring-intercept-over-writable-rope idiom is in listview, compile and
search/grep, and no Lua caller anywhere sets read_only because there is
no Lua set_read_only), while the framing still called it mandatory and
assigned it to 2b, and the PR body still described revision 1's
two-slice plan. All three reconciled: Q#DR25 is withdrawn, §3.1 becomes
a handoff to the generated-buffer-immutability lane, and the PR body is
rewritten. Also corrected in the other direction: rev 5's claim that
Q#DR25 "closes dired's quarter" of the §14 gap was itself wrong — dired
was never a quarter of anything.

R2 (P1) — acceptance allocation contradicted the code split, and the
review undercounts by one. Items 23-24 test apply_resource_op and the
drain harvest (2a substrate) but sat under a 2b header; item 33 needs a
dired.lua subscriber while 2a is defined as containing no dired code.
23-24 moved to 2a; 33 moved to 2b, because 2a's whole review rationale
is that its diff contains no dired code. The cost is stated: between 2a
and 2b a directory rename leaves dired handles stale, which is the
status quo and not a regression. The undercount: that leaves 2a shipping
resource.renamed with no acceptance, so item 50 pins the hook contract.

R3 (P1) — TickOutcome could not carry deletions. Replaced the two ad-hoc
vectors with one ordered Vec<ResourceOp>, and PendingJob carries a
single Option<ResourceOp>. Ordered because a directory rename and a
delete beneath it can settle in one tick; one enum rather than two
Options because two admit a both-Some state that cannot occur, the
argument ResolvedTarget's own doc makes at editor_core.rs:100-102.

R4 (P1) — reconcile_delete stopped short, and the substrate is worse
than the review says. Right that removal is two phases; what it does not
say is that NO existing Rust path composes them. pmacs.buffer.kill
(mod.rs:5476-5491) does, and its doc says it is late-bound precisely to
redirect windows — but apply_resource_op uses remove_buffer_and_fire,
which is phase 2 without phase 1, and BufferRegistry::remove touches
only buffers and order. So an LSP-authored delete leaves a displaying
window pointing at a removed id: a third defect on that arm. §6 now
specifies both phases, the last-buffer and ConcurrentEdit refusals, and
items 51-53.

R5 (P2) — the "every consumer" claim was false for Lean. The deferral
was right and the summary was wrong; fixed the summary. Stage 2 supplies
the hook, Lean's state stays stale until its owner adopts it.

R6 (P2) — pre-three-slice file inventory. minibuffer.lua joins
src/editor.rs's include_str! sequence, so 2b touches a Rust file; "2b is
dired.lua plus one killring binding" was false. + and C retagged 2c.

The sweep the review asked for caught four more stale labels it had not
cited: §4's "C (copy, 2b)", §7's "(in 2b) a recursive delete", §8's op
sections, and §8's d/x line — which was wrong in BOTH halves, saying 2a
where it meant 2b and 2b where it meant 2c, a pure two-slice-era
leftover. Historical round sections keep their original labels, flagged
rather than retconned.

The ledger lane rides this PR and is updated to match.

Status unchanged: PROPOSED, needs explicit user approval. Do not merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-28 17:59:54 -04:00
Levi Neuwirth 9101bf5b78 docs(framing): generated-buffer immutability, revision 1
PROPOSED. Framing only; no runtime code.

Buffer::undo gates on ensure_writable() and never consults the intercept
chain, so the add_intercept "read-only" idiom leaves the rope writable
and the owner's own bypass_intercept paint on the undo stack. Four writer
mechanisms across five buffer families are affected; every one was
reproduced by execution at ad41cf1, not inferred.

The document carries the measured reproductions, the full classified
census with its arithmetic shown, the primitive decision with numbered
alternatives, per-criterion bite obligations, and the staging proposal.

Recommended primitive: Buffer::apply_generated_edit(op), exposed as a
{ generated = true } option on the existing Lua mutators, with
set_generated_contents reimplemented as its whole-buffer wrapper. It is
the only candidate in which the buffer is never observably unlocked.
2026-07-28 17:57:57 -04:00
Levi Neuwirth 259bfdb5ca docs(active-work): refresh this lane to match revision 5
The lane described the pre-re-scout state: head ab42a79, 1,570 lines,
"re-scout under way", and a five-owner rename census. All four are now
wrong. It rides this PR rather than a standalone refresh, because a
separate ledger PR with several others open re-conflicts on every merge.

What the lane now records:

- Head e7f811b, 2,304 lines, integrated to main @ ad41cf1, status
  PROPOSED and never formally approved — four revisions and three review
  rounds are not approval and must not be read as one.
- Seven of rev 4's own claims about pmacs were wrong. The load-bearing
  one is kept in full because it is a trap anyone can repeat:
  drain_external_cancelled (:1561) and drain_cancelled_externals (:1596)
  are two different functions 35 lines apart, and rev 4 named one while
  citing the other's line. Following the citation gives the per-tick
  token sweep instead of the unconditional server-scoped drain, so the
  drain half becomes a silent no-op and the awaiting coroutine hangs.
- The path-owner census is six, not five. The sixth is lean.lua's
  M.file_progress, a URI-keyed Lua table in no Rust store, which is the
  first evidence outside dired that the hook rather than the Rust method
  is what scales.
- The journey ratchet is split across journey_acceptance.rs and
  gpu_invocation_acceptance.rs; both are gates, and a scout checking only
  the first concludes #183 added nothing.
- open_directory now commits under pmacs.window.commit_to, whose scope
  refuses an await, which constrains the proposed batch contract.

Scope moved OUT of this lane: Q#DR25's adoption of
set_generated_contents turned out to address a class bug shared with
listview, compile and search/grep, so it is owned by the
generated-buffer immutability lane now. This PR needs a rev 6 deferring
Q#DR25 there once that framing is approved.

Also records two live bugs this lane confirmed but does not fix, both
owned elsewhere: apply_resource_op's delete arm destroying unsaved work,
and View still lacking rename_resource.
2026-07-28 17:37:39 -04:00
Levi Neuwirth e7f811be3b docs: integrate main @ ad41cf1 and record what the ledger now needs
PR #185 merged during the re-scout, so `docs/active-work.md`,
`docs/agent-handoff.md` and `COHERENCE.md` are no longer held by an open
PR. #185 changed docs only — no source — so nothing in rev 5's census
moves, and the two facts rev 5 cites from the durable records survive
intact: the handoff §4 inventory still names `builtin/runtime/dired.lua:371`
as a non-adopter of `set_generated_contents`, and COHERENCE §14 still
names dired and still classifies it as the cheap half.

This branch continues to touch only `docs/dired-stage2-framing.md`. A
standalone docs-refresh PR is exactly the ledger contention treadmill
the ops lessons warn about, and the ledger's own protocol puts a lane
refresh with the work rather than ahead of it.

What §16 now records instead: the landed `docs/active-work.md` lane
still has this branch at head `ab42a79`, calls the document 1,570 lines,
says the re-scout is under way, and states the rename census as FIVE
path owners. Rev 5 makes all four stale — and the census is SIX (§5,
W5). Same lesson in the other direction: a census is a reading, not a
constant. The refresh should lift §5's table rather than re-derive it,
and should note that Q#DR25 added scope the lane does not mention.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-28 17:24:02 -04:00
Levi Neuwirth fa14d39a0f Merge remote-tracking branch 'githubsucks/main' into dired-stage2-framing 2026-07-28 17:23:05 -04:00
Levi Neuwirth a69cd0b170 docs: integrate main @ 0442d78 and confirm the rooturi deferral
`main` moved mid-re-scout: #174 (fix-m4-sink-races) merged. It touches
`tests/m4_acceptance.rs` only and changes no source, so nothing in the
rev 5 census moves — but it intersects this document at one point, and
in the direction that strengthens it.

#174 fixed the CONFIG sink by waiting for a complete JSONL record
(`ends_with('\n')` instead of `contains("probe")`) and deliberately left
the ROOTURI predicate alone, adding a comment at
`m4_acceptance.rs:5486-5496` giving the same three reasons §11's
deferral gives: it is the same weak-predicate class, it has never been
observed failing, and waiting for the expected value would trade a
precise regression diff for a vague five-second timeout. Closing it
needs a record terminator in the fake server first.

So that deferral is no longer a claim of this framing; it is a claim of
the tree. Item updated with the current line (`:5499`, shifted by
#174's own comment) and the in-tree backing.

The header now records both anchors: the census was read at `6bee09d`,
and it holds unchanged at `0442d78`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-28 17:21:43 -04:00
Levi Neuwirth 8ddc8a7a5f docs: dired Stage 2 framing rev 5 — re-scout against main @ 6bee09d
Rev 4 was scouted at c8ec8f3, which is dired Stage 1's own merge commit.
Canonical main has moved 153 commits since. This is that re-scout. Every
claim below was read on the tree at 6bee09d, not inferred.

Four dependencies arrived that rev 4 could not have known about:

- #178 landed `Buffer::set_generated_contents`, and dired has NOT
  adopted it — the handoff §4 and COHERENCE §14 both name
  `dired.lua:371` in the non-adopter inventory. Dired pairs an erroring
  intercept with a bypass_intercept write over a still-writable rope, so
  `M-x buffer.undo` empties a listing today, with no keybinding needed.
  Stage 2 writes that buffer on every mark. New decision Q#DR25 adopts
  the primitive at the head of 2b, with acceptance items 48-49.
- #182 (Journey Stage 1a) demoted dired to a replaceable slot
  (`pmacs.path.directory_handler`, deliberately not a hook subscriber)
  and rewrote `open_directory` around `pmacs.window.commit_to`, whose
  scope REFUSES an await. That constrains §9's serialize-and-await batch.
- `tests/journey_acceptance.rs` is a declared ratchet (24 tests, "stages
  add rows, none removes them"), seven rows assert on dired, and #183
  put the GPU row in `gpu_invocation_acceptance.rs` instead — so the
  ratchet is split across two files and both are gates now.
- #179/#181's typed-edit chain: dired participates in neither it nor
  `buffer.after-edit`, and `set_generated_contents` fires no hook, so
  Q#DR25 does not drag dired onto the chain. The chain's lessons still
  bind §5's two NEW hooks.

Seven of rev 4's own claims about pmacs were wrong:

- W1 (load-bearing): §5 named `drain_external_cancelled` and cited
  `lsp.rs:1596`. Those are two different functions.
  `drain_external_cancelled` (:1561) is the unconditional server-scoped
  drain and is the right precedent; `drain_cancelled_externals` (:1596)
  is a per-tick token/timeout sweep. A rename flips no token, so
  following the line number yields a `forget_uri` whose drain half is a
  silent no-op and the awaiting coroutine hangs forever — the exact
  failure step 2 exists to prevent.
- W2: there is no `fn restart`. The teardown is `start_generation`
  (:1307-1345), and there is a second precedent rev 4 never named,
  `LspManager::forget` (:3015-3042). Neither clears the 14 result
  stores, so rev 4's "surprising" note now holds twice.
- W3: `ResponseRoute` has 15 variants, 14 URI-bearing; there are 16
  insert sites, 15 URI-bearing. Rev 4's fifteen was a correct count of
  URI-bearing inserts and an incorrect count of sites. The purge
  predicate must retain `WorkspaceSymbol { query }` explicitly.
- W4: `rec.uri` is read at 57 lines in lsp.lua, not "~20".
- W5: the path-owner census has grown to SIX. `lean.lua`'s
  `M.file_progress` is a URI-keyed Lua module table in no Rust store, so
  `forget_uri` cannot reach it — independent evidence that the hook, not
  the Rust method, is the mechanism that scales.
- W6: the ledger note named PR #169, which merged. #185 holds the
  durable records now.
- W7: the C1 seam is at mod.rs:7104-7115, and mod.rs defines three
  `_tick` bindings in different classes.

The 2a/2b/2c/Stage-3 cut was re-examined and holds unchanged; §16 adds
the ownership warning that 2a overlaps editor_core.rs, lsp.lua and the
URI-keyed LSP state with Journey Stage 1b and must not run concurrently
with it. §0.5 now cites COHERENCE §20 Priority 1 and §14 by number, and
records that #182 put dired on journey step 3 — a claim rev 4 could not
make.

Status is stated honestly: PROPOSED, never formally approved, and
needing explicit framing approval before any implementation branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-28 17:18:25 -04:00
Levi Neuwirth 6fc7fee044 docs: absorb #174 and advance the anchors to 0442d78
#174 merged while this PR was open, so the lane it had just been given
is already a merged lane. Rule 4 removes those rather than relabelling
them — but only once the durable facts are in the handoff, so do both
here in that order.

docs/agent-handoff.md
- §5 gains the lesson: a wait predicate WEAKER than the assertion it
  guards is a race on whichever platform loses it, and "wait until the
  file mentions X, then assert Y" races whenever Y is stricter than X.
- Two things that generalize past the fix ride with it: a race you
  cannot reproduce can still be bitten at one remove (an unsatisfiable
  predicate proves the wait is load-bearing; the old predicate still
  passing proves a local run cannot tell them apart), and the obvious
  fix is sometimes worse — the sibling m4_26 is deliberately left alone
  because closing it the easy way trades a precise assertion diff for a
  vague five-second hang.
- Header and §1 anchor advance to 0442d78, noting #174 is test-only and
  #184 remains the substantive change at that head.

docs/active-work.md
- Remove the #174 lane; the header paragraph now says where it went.
- Canonical base and recovery floor advance to 0442d78. The floor
  explicitly rejects 6bee09d as well as 7fd646d, per its own rule that a
  check accepting an older commit than the declared base passes on a
  tree the file does not describe.
- Bottom-panel lane: 2B-2 is IN FLIGHT, not merely "next" — branch,
  worktree, and the fact it is branched fresh rather than stacked. Its
  boundaries are restated because they are easy to overrun: production
  Hello stays v20, panel_capable stays false, and 2B-3 may not simply
  bump the unsolicited Hello.
- dired lane: the re-scout is under way on the existing branch, so #171
  keeps its three-round history and the product is a rev 5.
- Both lanes note that main advancing to 0442d78 changes nothing for
  them, since #174 is test-only.

The bottom-panel framing doc keeps its 6bee09d ground-truth line: that
is a scouting statement about what was actually read, and re-anchoring
it to a commit nobody re-scouted against would be a false claim.
2026-07-28 17:04:17 -04:00
Levi Neuwirth 55122ba912 Merge remote-tracking branch 'githubsucks/main' into dired-stage2-framing 2026-07-28 16:46:35 -04:00
Levi Neuwirth a159200756 docs(active-work): give the two open PRs the lanes they never had
Neither #174 nor #171 had any entry in this ledger. An open PR is
exactly the volatile work the file exists to record, so its absence is a
defect rather than a tidy omission — #171 drifted 153 commits while
invisible here, and its still-green old CI run describes a tree nobody
has looked at since.

#174 — M4 config-sink race, revived and regated. Records the predicate
fix, the gate numbers on the integrated head 302c21c, and two things
verified rather than inherited: the "one writeln! is the only writer"
argument re-checked against a fake_lsp that DID change upstream, and the
wait bitten two ways (unsatisfiable predicate fails on the pump deadline;
the old predicate still passes locally, confirming a green local run
cannot tell them apart).

#171 — dired Stage 2 framing, stale, do not merge as-is. Measured 4
ahead / 153 behind with merge base c8ec8f3, which is itself #165's merge
— so dired Stage 1 and find-file are its base, not new arrivals. Names
the three landed changes that move ground under it: #178's
generated-buffer write invariant (dired is a non-adopting writer),
#182's demotion of dired to a replaceable directory-handler slot, and
#179/#181's typed-edit consumer chain. Restates the five-path-owner
rename transaction the framing still has to answer.

Also records that three review rounds in a commit history is not the
same as approval, and that dired 2a's file overlap with Journey 1b needs
assigning before either runs.
2026-07-28 16:42:00 -04:00
Levi Neuwirth e05ac819b5 docs: sync the durable records to landed main @ 6bee09d
PR #184 (bottom-panel Stage 2B-1) merged, but the four documents that
new work is supposed to start from still described it as open. Advance
every canonical anchor and close the one obligation that was blocking a
lane removal.

docs/agent-handoff.md
- §1's head-of-main anchor moves 7fd646d -> 6bee09d.
- State schema support and production advertisement as two separate
  facts: SUPPORTED is now v6..=v21, the server-first Hello still says
  v20, and #184 landed only the first.
- New §1 bullet for Stage 2B-1 carrying its durable substrate: the
  server-first incompatibility argument, the shared wire_grid boundary
  and why per-axis caps are a parameter, authoritative Absent, and the
  panel_epoch / geometry_epoch split.
- New §1 bullet for the PTY terminate diagnostic (#176), plus two §5
  ops lessons: a drain that ticks is not an observation, and how to
  prove a child exited without waitid or unsafe.

docs/active-work.md
- Canonical base and the recovery floor both advance to 6bee09d.
- Remove the #176 lane. It was retained only because rule 4 requires
  its facts to reach the handoff first; that move is in this commit, so
  the lane goes.
- Rewrite the bottom-panel lane as 2B-1 MERGED with 2B-2 next, branching
  fresh from landed main rather than stacking.
- Annotate the documentation lane with measured staleness (1 ahead, 320
  behind) without deleting it, since nothing has decided its fate.

docs/bottom-panel-stage2-framing.md
- Status header records 2A and 2B-1 as merged and 2B-2 as next. No
  revision bump: the header is landed state, and no design decision in
  the document changed.

COHERENCE.md
- §16 names #184 as the PR that landed the reserved v21 family.

No runtime code, no protocol change, no gate suite implicated.
2026-07-28 16:28:21 -04:00
Levi Neuwirth 5539b6e8c6 Record the fixture-specific PR 184 probe fix
Capture the follow-up review finding, the evidence-driven completion
contract, the exact corrected CAT duration, and the proportional green
gate matrix at 9c79ce1.
2026-07-28 15:29:55 -04:00
Levi Neuwirth 80b761bb03 Record the regated PR 184 review head
Capture the exact review-fix and GPU probe checkpoints, the full green
gate evidence, and the classified sandbox-only socket failure in the
cross-machine active-work ledger.
2026-07-28 14:29:04 -04:00
Levi Neuwirth ab7c207904 Keep the v21 panel wire dark for v20 clients
Reserve the additive v21 panel schema without advertising it in the
server-first production handshake. Pin a real shipped-v20 client attach,
make the two aggregate-budget ratchets exactly one byte over, and update
the framing, coherence audit, handoff, and volatile lane record.
2026-07-28 14:08:17 -04:00
Levi Neuwirth f82d91ed3a docs(active-work): record bottom-panel 2B-1 PR
Record PR #184 as open for review and keep the explicit no-merge hold in
the portable lane state.
2026-07-28 12:51:42 -04:00
Levi Neuwirth 17867ace0e docs(active-work): record integrated 2B-1 gates
Record the complete post-#183 gate matrix for bottom-panel Stage 2B-1
at c8895a8 and mark the lane ready to open for review.

Retain the required-GPU first-pass classification: one unrelated math
render assertion failed, passed immediately in isolated single-threaded
execution, and the mandatory complete 202-test rerun passed.
2026-07-28 12:44:27 -04:00
Levi Neuwirth c8895a85d3 Merge canonical main into bottom-panel Stage 2B-1
Integrate main through the Journey/GPU directory-target ratchet (#183).
The public managed-GPU probe additions compose with the v21 wire layer.

Resolve the volatile ledgers to the landed state: advance the canonical
anchor to 7fd646d, remove the completed ratchet lane, preserve the
approved Stage 2 revision-5 recovery facts, and mark the integrated
2B-1 tree as awaiting its from-start full gate rerun.
2026-07-28 11:38:19 -04:00
Levi Neuwirth 22c1b14b18 docs(active-work): record review-round cleanup gates
Record the second-pass daemon-cleanup finding, its correction, and the
green verification matrix on the updated PR branch.
2026-07-28 11:03:36 -04:00
Levi Neuwirth 22dc46c184 docs(active-work): record public-path gates
Record the green full matrix for the public pmacs --gpu directory
acceptance revision at 34b8f28.
2026-07-28 10:33:26 -04:00
Levi Neuwirth 34b8f28cf2 test(journey): ratchet the public GPU directory path
Drive pmacs --gpu . through the root broker and real managed GPU
connector, keep the session alive through the asynchronous dired
replacement, and assert its canonical listing before daemon reuse.

Expose snapshot count and materialized text through the private
display-less acceptance probe so the public path is observable.
2026-07-28 10:27:22 -04:00
Levi Neuwirth 3be4285f7c docs(active-work): record review-round gates
Close PR 183 review round 1 in the volatile ledger and record the
post-correction full gate matrix at ec4191f.
2026-07-28 10:01:18 -04:00
Levi Neuwirth ec4191fd8e test(journey): pin post-quiescence GPU dired surface
Consume the directory session's later replacement snapshot and assert
the canonical dired header plus a known listing entry before checking
daemon reuse.

Correct the bottom-panel revision-5 recovery branch and advance the
durable handoff to the Journey Stage 1a main anchor.
2026-07-28 09:48:48 -04:00
Levi Neuwirth c2b855e252 docs(active-work): record directory ratchet PR
Attach the gated Journey/GPU side-quest lane to PR #183 and make its
intentional open, unmerged review state explicit.
2026-07-27 23:19:05 -04:00
Levi Neuwirth a5107ca32d docs(active-work): record directory ratchet gates
Capture the complete green gate matrix and retain the diagnosed setup
and transient full-sweep failures so the lane remains recoverable and
the evidence is not flattened into an unexplained rerun.
2026-07-27 23:15:10 -04:00
Levi Neuwirth 486ce16748 test(journey): ratchet directory GPU bootstrap success
Replace the stale directory-negative in the GPU initial-target suite
with an explicit snapshot-first readiness path, while retaining all
genuinely malformed and unloadable failure cases. Record the portable
side-quest and bottom-panel dependency state.
2026-07-27 22:53:36 -04:00
Levi Neuwirth b9123c2f6d test(protocol): advance touched-suite ratchets to v21
Make the statusline and Vterm Stage 3 acceptance suites track the
bottom-panel v21 bump, including the real daemon and headless GPU probe.
Record the full gate result and the unrelated stale directory-target
assertion reproduced on canonical main.
2026-07-27 22:39:15 -04:00
Levi Neuwirth d88d60eba6 Merge canonical main into bottom-panel Stage 2B-1
Integrate the Journey Stage 1a merge without rewriting the already
reviewed protocol branch. Record the approved three-way Stage 2B split,
advance the canonical recovery anchor, remove the landed Journey lane,
and put 2B-1 into its full-gating state.
2026-07-27 22:16:11 -04:00
Levi Neuwirth 7a3a55de40 docs(active-work): remove the landed Lean 4 lane
The ledger preamble already says Lean 4's merged lane was removed, and
the durable Stage 4b facts already live in the handoff. Remove the stale
section that still called Stage 4b in review so PR #182's post-merge
state is internally consistent.
2026-07-27 21:47:01 -04:00
Levi Neuwirth 56301eda02 docs(panel): Stage 2 framing rev 5 — the three-way slice of 2B
Rev 4 §9 scoped Stage 2B as a single PR: v21 protocol, daemon panel
projection, GPU band, and the negotiated `panel_capable` flip.
Implementation showed that to be roughly four thousand lines across
three crates with three different failure modes. Rev 5 splits it into
2B-1 (the wire layer), 2B-2 (the daemon projection and epoch machine),
and 2B-3 (the GPU band and the flip), on the rule that a slice ends
where the next thing to build has a different authority.

No decision changes. What changes is the allocation:

- §7.2 becomes three subsections, and criteria that span a boundary are
  named in every slice they touch with their half stated, rather than
  assigned wholesale to one. Parent 39 is the clearest case: its
  shared-validation and transport-budget halves are wire properties
  provable in 2B-1, while "the previous valid frame is retained" and "a
  duplicate does no work" need the epoch machine and are 2B-2. A2B-1
  splits the same way — grid exhaustion in 2B-2, the frontend latch in
  2B-3.
- §9 lists four serial PRs instead of two, each cut from `main`, and
  states that every slice runs the full gate set rather than the subset
  its own crate suggests.
- §6 records which slice pays the coherence debt. The journey claim
  belongs to 2B-3 alone: with `panel_capable = false`, a GPU user still
  gets the Stage 1 non-side fallback after 2A, 2B-1 and 2B-2 have all
  landed. Three quarters of this stage is preparation.

Two things recorded because they are easy to inherit silently:

- This revision is retroactive for slice 1. `bottom-panel-stage2b`
  already carried the v21 protocol layer, written before the revision
  existed, which inverts framing -> approval -> branch -> implement. The
  slicing was sound; taking it in code rather than in the document is
  how a stage's scope drifts without anyone deciding that it should.
- 2B-1 and 2B-2 ship dark. The bump advertises a capability whose only
  distinguishing feature is unreachable until 2B-3, so the arc must not
  stall between them. Safe for compatibility — appended variants,
  extended ladder, a v20 peer still negotiates 20 — but a stall should
  be visible as a decision, not inherited as a default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
2026-07-26 20:02:19 -04:00
Levi Neuwirth d0817314f9 merge: fold the terminal landed-docs PR (#180) into this branch
Two open documentation PRs editing the same three ledger files
re-conflict on every merge to `main` — the treadmill #169 and #176 each
paid three resolution rounds for. #180 was docs-only and already
CONFLICTING against `main`; folding it here retires one of the two
writers instead of resolving the same file twice.

Also integrates `githubsucks/main` @ `42025e4` (Lean 4 Stage 4b #181).

Conflicts resolved, and three claims corrected rather than picked:

* **The head-of-`main` anchor.** Both sides named `74301d1`, which is no
  longer true — `#181` has landed. The anchor now reads `42025e4` and
  leads with it, in `docs/agent-handoff.md` §1 and the active-work
  canonical-base line, and **the recovery floor moved with it**: the
  `git log` check requires `42025e4` or newer, per that section's own
  rule that a floor accepting an older commit than the declared base
  passes on a tree the file does not describe.
* **The Lean 4 bullet contradicted the anchor I had just written.** Its
  header still said "stages 1, 2, 3a, 3b LANDED" and its Stage 4b
  sub-bullet still said "implemented and in review", while the anchor
  above it now records #181 as merged. Both corrected: all six stages
  landed, #181 named.
* **The COHERENCE §2 grade.** #180's side still carried "broken at step
  3" / "Broken at entry" in both the ground truth and the §0 scorecard;
  this branch's corrections win, since they are what the code change in
  this PR makes true.

The removal note at the top of `docs/active-work.md` is reattributed:
the terminal config + copy mode lanes were #180's removal, and it now
says so and says why the fold happened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 18:36:55 -04:00
Levi Neuwirth 8e31ca4646 Merge remote-tracking branch 'githubsucks/main' into journey-stage1a-directory-open 2026-07-26 18:33:44 -04:00
Levi Neuwirth 4cf1b63198 docs(active-work): the journey suite is 24 pins, not 23
A count is a reading, not a constant. Recorded as of rev 8 with an
explicit instruction to re-read rather than quote the line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 18:33:26 -04:00
Levi Neuwirth 7741cf806a fix(journey): honor the captured window, not the selected one
Review round 1 of PR #182. One implementation gap and two stale claims.

**The scope pins the frontend; it does not pin the window.** Framing
§4.4 specified `display{ window = dest:window() }`, but dired's commit
still ended in `pmacs.window.switch_buffer`, which targets whatever
window the scoped frontend has selected. A split or panel that took
focus while `read_dir` was pending therefore received the listing, and
`prev` was captured from it too — with every preflight check passing,
because the captured window was still live and still held its captured
buffer. Both sites now read the captured window: `display` routes to it
with `select = true` (the later `seat_cursor` acts on the active
window), and the `prev` read asks it directly.

N4c pins both halves. The suite's existing routing pins all varied
*frontend* identity; none varied the selected window within one
frontend, which is exactly why 23 green pins missed this. Bite: dired's
`display` back to `switch_buffer` fails N4c alone; `prev` read from the
ambient window fails N4c alone.

Two stale documentation claims, both of which this PR was supposed to
have already fixed:

* **The §0 scorecard still graded §2 "Broken at entry"** while §2's own
  ground truth had been rewritten. The scorecard is a second copy of the
  same claim and §25's protocol covers both. §19's row and ground truth
  were stale the same way — this PR creates the first cross-subsystem
  suite, which §19 says should exist and grades as missing — and are
  corrected too.
* **P4 still read "leaves exactly one buffer"**, the exact claim rev 6
  corrected as false everywhere else in the framing. Restated to what it
  actually pins: the file is in the *active window*. The test was
  already written correctly; only the framing lied.

Framing rev 8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 18:20:02 -04:00
Levi Neuwirth d9d1c0b423 test(journey): pin the destination commit, and land the doc updates
Completes Journey Stage 1a: the `commit_to` acceptance suite (framing
§6 N4, N6, N6b, N6c, P1, P2, P3) plus the documentation updates
COHERENCE §25 requires the PR to carry.

Bite-testing the new pins found a real gap. Deleting the
`ScopedFrontend` arm from `acting_frontend` left N4 green, because
`ScopedFrontend::enter` also swaps `core.active_frontend` and the
ambient fallback then answers correctly on its own. The arm is
load-bearing in exactly one case — a commit reached from inside an
interactive command, where the origin sits between the override and the
ambient value — and nothing pinned it. N4b is added, driven through
`dispatch_key` because that is the only thing that establishes an
interactive origin, and the mutation now bites it.

Two smaller corrections found the same way:

* `commit_to`'s forged-destination message was unreachable. Typed as
  `AnyUserData`, mlua rejected a table during argument conversion, so a
  caller got "error converting Lua table to userdata" — true, but naming
  neither the rule nor the remedy. The parameter is now `mlua::Value`
  and the pointed message fires.
* P1 and P2 also fail on full revert, since `commit_to` does not exist
  on the pre-image, so §6.0's "legitimately green on the pre-image" does
  not describe them. They stay in the P list because their
  discriminating falsifier is the named mutation — a revert-only check
  cannot distinguish "validates" from "validates in time" — and each pin
  now says so at its own site rather than being silently mislabelled.

Bite results, each run against the whole suite:

  scope stops swapping `core.active_frontend`  -> N6a, P3 fail; nothing else
  preflight moved after the callback           -> P1, P2 fail; nothing else
  drop the `ScopedFrontend` arm                -> N4b fails; nothing else

Docs: COHERENCE §2 grade + step-3 verdict row, §20 Priority 1 and the
arc list; the GPU initial-target framing's Q#GT6 and acceptance 10,
whose directory case this stage deliberately supersedes; handoff §1;
the active-work ledger; framing rev 7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:25:22 -04:00
Levi Neuwirth d1bff6ac30 fix(lean4): count fan-outs where a claim cannot skip the count
Round 11 put the nesting count in the expander, which is optional. A
consumer at a lower priority can CLAIM and stop the chain before the
expander runs, while that fan-out's deferred-expansion subscriber still
runs — so the nested pass went uncounted, looked like the outermost
one, expanded early, and outer pairing resumed with a record the
replace had invalidated. `\alp(` gave `α(` again.

The count now comes from a no-op consumer registered at the minimum
priority, which runs first in every chain invocation that reaches any
consumer at all. Its guarantee is exactly the ordering contract the
chain already rests on, and it degrades safely: the only thing that can
skip it is a claim ahead of it, which skips the expander too, so
nothing is queued in that fan-out either.

The other plausible home does not work and the comment now says why: a
subscriber registered beside `run_deferred` is too late, because the
whole nested fan-out completes inside the OUTER chain's subscriber,
before either of them runs.

Acceptance 45o pins the short-circuit path — a consumer at 25 that
claims when the record is nil, so the nested pass never reaches the
expander. 45n passes against this bug, which is why both exist.
Counting in the expander fails 45o and nothing else.

Framing rev 12 also names the shape rounds 10–12 share: each fix was
correct about the failure it was shown and wrong about the boundary of
the mechanism it leaned on — the chain's copy semantics, then its
re-entrancy, then its short-circuit. A queue that outlives the thing
that filled it has to name that thing, not approximate it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
2026-07-26 17:15:24 -04:00
Levi Neuwirth 0d7ec7e3a6 fix(lean4): tie the deferred expansion to the fan-out that queued it
`buffer.after-edit` fan-outs NEST — the typed-edit contract supports a
consumer calling `pmacs.hook.run`, and typed_edit.lua's header says so
in its second paragraph. A nested run re-enters every subscriber,
including the deferred expansion's, while the OUTER chain is still
walking its consumer list and pairing has not yet seen the terminator.

So a consumer registered at priority 75 — between the expander at 50
and pairing at 100 — that runs one nested fan-out made `\alp(` yield
`α(` again: the nested pass consumed the queued expansion and edited,
and outer pairing then resumed holding a record the replace had
invalidated. That is round 10's failure reached through the chain's
documented re-entrancy seam rather than through claiming, which is why
deferring alone did not close it.

Deferring work past a fan-out means owning WHICH fan-out it belongs to.
The chain's subscriber and this module's each run exactly once per
fan-out, in that order, so counting invocations of the first and
matching them off in the second identifies the nesting level. Only the
outermost pass expands; a nested one leaves the expansion queued. No
new seam in typed_edit.lua, which is merged Stage 4a substrate.

Both halves bite: removing the level check and never counting
invocations each fail the new acceptance 45n.

Also fixes a test comment that still described the span design round 10
discarded — it claimed the expansion replaces the span "INCLUDING the
terminator". The behaviour asserted was right; the explanation was
stale. Framing rev 11.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
2026-07-26 17:00:58 -04:00
Levi Neuwirth f3103a6953 fix(lean4): defer the expansion past the chain, and guard its point
Three defects, all about what happens AROUND the expansion rather than
about resolving an abbreviation.

A pair character that TERMINATES an abbreviation never reached
auto-pairing: `\alp(` gave `α(`. Q#LN22 already said the terminator is
not claimed and the implementation claimed it whenever an expansion
succeeded. Merely declining is not enough either — the chain hands each
consumer a copy of the record made before any consumer ran, so
expanding inside the chain invalidates the copy pairing is holding and
the closer is silently lost. Verified by mutation rather than assumed:
expand-then-decline reproduces `α(` exactly.

The expansion therefore runs on its OWN `buffer.after-edit` subscriber,
registered after typed_edit.lua's and before lsp.lua's. A claim stops
the chain but not a separate subscriber, which is the point: pairing
claims the terminator it reacts to. The replaced span now covers only
the leader and the typed text, so pairing's closer lands outside it and
survives. One undo restores the same text either way, because the
terminator was always its own insert.

That second subscriber is a new instance of Q#AP7 — lsp.lua flushes
didChange synchronously on the signature-trigger path, and `(` is a
trigger — so acceptance 45m pins it with the sighelp fake server: no
didChange may ever carry the unexpanded text.

The relevance check is now three-part, as pairing's has been since
#110: buffer, window, AND `ed.cursor() == rec.post_cursor`. A redefined
self-insert can insert the completing character and then move the
point, and expanding over a span the user has left teleports them back
into it.

Cursor placement after the replace is context-guarded, as
`repair_cursor` is. A buffer intercept may switch buffers while
`buf:replace` runs; the unguarded `goto_byte` then translated the Lean
buffer's pre-edit point through the Lean buffer's edit and applied it
to whatever was ambient.

Q#LN22, criterion 38's span wording, and the ledger are corrected to
describe the deferred design rather than the one that shipped — the
rationale's source, not only the sites quoting it. Acceptance 45j/45k/
45l/45m added; framing rev 10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
2026-07-26 16:47:54 -04:00
Levi Neuwirth d2ab3761c7 docs: correct the non-adopter attribution and two stale anchors
Round 6. Documentation only; no code, no protocol change.

1. **The four-writer table mapped the wrong buffers to `ensure_slot`.**
   Verified at the call sites: `ensure_slot` has exactly two callers,
   `*compilation*` (`compile.lua:1090`) and `*shell-command*` (`:1125`).
   `*search-results*` is an **independent panel** in
   `builtin/commands/default.lua` with its own intercept (`:869`),
   round-trip mark and writes; `compile.lua` declares its name only to
   answer a predicate (`:216`), which is what made it look like a third
   slot. Round 5 fixed an undercount and introduced a misattribution in
   the same paragraph — the count was right, the mechanism was not.

   The table is now keyed by **writer**, not by buffer, so the mapping
   cannot silently drift again: four mechanisms, five buffers. It carries
   an explicit "do not read `ensure_slot` as covering the search panel"
   note, because that is the specific wrong inference.

   Corrected identically in `COHERENCE.md` §14 and the framing's
   deferred-lane text, which both carried the error.

   The scope claim is narrowed with it. "Every generated buffer outside
   copy mode" was too wide: `*workers*`, `*help*` and `*buffer-list*` are
   generated but do not use this idiom, and the REPL package's intercept
   (`packages/repl/init.lua:187`) is an op-filtering editing policy
   rather than a read-only panel. The claim is now "every remaining
   intercept-protected writer", and the two excluded groups are named so
   the next reader does not have to re-derive the boundary.

2. **The recovery floor contradicted itself.** The canonical-base line
   said the check accepts `a27f646` or anything newer while the check
   below required `74301d1`. The floor genuinely advanced; the prose now
   says so outright — a tree at `a27f646` no longer passes — and states
   why the floor must move with the base rather than trailing it.

3. **Two anchors survived the integration.** Lean 4 Stage 4a said it was
   part of "the `fe8b8ba` anchor above" when the anchor had become
   `74301d1`; it now refers to the anchor rather than restating a commit,
   which is what let it go stale. And #168's closed entry called its own
   `fe8b8ba` figure "the live figure" — it is a reading taken at
   `1b6a084`, kept as history, and now says so and points at the
   coverage lane as the single authority with an explicit "do not quote
   this one forward".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
2026-07-26 16:45:12 -04:00
Levi Neuwirth 25b07be97b fix(journey): correct Q#JR3, report the post-dispatch buffer, unvacuate two pins
Four review findings, all confirmed against the tree.

Q#JR3 was false. `replace_active_buffer` does not drop the startup
scratch buffer -- its body is one `switch_active_buffer` call, which
reassigns the window's buffer_id and removes nothing. The claim came
from that function's own doc comment, wrong for as long as it has
existed, and rev 5 propagated it into the framing and into new
documentation this branch added. Both comments are corrected here,
because this PR was adding further false references to a claim P4
depends on. Actually removing the stale scratch is buffer-lifetime work
and stays out.

The daemon bootstrap could report the wrong buffer. The directory arm
captured the destination id, ran the resolver chain synchronously, then
returned the captured id -- so a handler that opened something
synchronously through commit_to had already replaced the window's
buffer, and the reply paired one buffer's snapshot with another's
identity. It also returned early, skipping the post-hook revalidation
the framing said stayed active. The arm now re-reads the destination
after dispatch and rehomes through `non_side_target` as the file arm
does. Pinned by a test whose handler claims synchronously.

N11 tested neither RET nor self-insert: it called display_file and
buf:insert directly, so it stayed green with dired's RET binding, its
entry dispatch, and the editor's self-insert path all broken. Both
gestures now go through dispatch_key.

P7 is removed rather than weakened. Q#JR12 has nothing to pin --
`had_file = file.is_some()` and a directory is Some like any other, so
no directory-specific branch exists to break. The old test never armed
restore and hard-coded had_file, so it could not fail against any
implementation.

Also adds the daemon bootstrap pins (N2, N5) and fixes an insertion that
had orphaned a `#[cfg(feature = "crdt")]` from the test it guarded --
which would have made one new test dark and one existing test escape its
gate.

Framing: docs/journey-stage1a-framing.md rev 6.
2026-07-26 16:39:59 -04:00
Levi Neuwirth dd9f380533 docs(active-work): record Journey Stage 1a branch and ordering
Rev 5 is approved and the branch is cut, so the ledger's "no branch,
commit, or PR exists yet" line no longer describes reality. Records the
recovery command, notes that PR #177 has merged and therefore unblocks
implementation, and carries the standing obligation that dired Stage 2
re-scouts around commit_to before its branch is cut.
2026-07-26 16:39:59 -04:00
Levi Neuwirth 600b3d0c7d docs: framing for Journey Stage 1a (directory open on one path)
Serves COHERENCE.md §2 (the golden product journey), §19 (coherence
acceptance tests), and §20 Priority 1, which grades the journey broken
at step 3 because `pmacs .` exits 1.

Stage 1a ships four things: the directory argument routed into dired's
buffer on both the local and daemon/GPU startup paths; EditorState::open
adopting resolve_target_buffer so the two path-open implementations
become one; a scoped-destination commit primitive so an async open lands
where it was requested or nowhere; and the first cross-subsystem journey
acceptance suite.

Framing only -- no implementation. Rev 5 after four review rounds.
2026-07-26 16:39:27 -04:00
Levi Neuwirth d14fe4c4b9 Merge githubsucks/main (#169, #176) into docs-terminal-copy-mode-landed
Two doc conflicts, both in favour of `main`, and both are this PR's own
findings arriving from the other side:

- **The dired lane.** #169 did exactly what round 5 finding 5 said it
  would: absorbed dired Stage 1 into handoff §1 and removed the ledger
  lane per rule 4. This branch carried a placeholder saying #169 owned
  that text; the placeholder is dropped and #169's version taken whole.
  This is the rule working — one PR, one authority per paragraph.
- **The canonical base.** Both sides edited it. Resolved to `74301d1`,
  now naming #169 and #176, keeping `main`'s added guidance that a
  recovery threshold must move with the base it declares canonical (a
  check that accepts an older commit passes on a tree the file does not
  describe). The threshold moved with it.

Consequences of the merge, folded in:

- **The census is re-measured on the merged tree**: 3,176 vs 3,449 —
  still **273 dark**, 185 of them in the library. #176's six new tests
  are not `crdt`-gated, so both totals moved and the gap did not. The
  per-target table is unchanged.
- **The `crdt` Clippy failure is re-measured too, and the previous count
  was wrong** — it was read off a different checkout. At `74301d1` it is
  seven errors before the build aborts: four in `src/daemon.rs`, three in
  `tests/vterm_stage3_acceptance.rs`. Recorded as a **lower bound**,
  because Clippy abandons the remaining targets once one fails, and an
  older tree showed a further error in
  `tests/auto_indent_crdt_acceptance.rs` this run never reached.
- **The PTY-terminate lane (#176) is marked MERGED and retained**, with a
  note at its head saying why it survives rule 4: no landed-doc PR owns
  moving its facts to the handoff, so removing it now would delete the
  record rather than move it. The ledger preamble's stale-on-purpose
  paragraph is rewritten around that one lane; the Lean 4 and
  GPU-terminal-input headers it used to disclaim no longer exist.
- The flake note's "`src/process.rs` last changed by the Darwin PTY
  signal-name fix" is dropped — #176 changed it. The load-bearing half
  (#178 did not touch that file at all) is what remains.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
2026-07-26 16:37:41 -04:00
Levi Neuwirth f8ca722d66 merge: integrate main (74301d1) into Stage 4b
Reconciles the handoff and ledger against a main that advanced past
this branch's base: the header, `main` anchor, and canonical-base
description take main's richer versions restamped to 74301d1, main's
new PTY-terminate lane is kept alongside the Lean lane, and main's
Stage 4a/rev-8 lane history is dropped in favour of the Stage 4b lane
that supersedes it — per this ledger's own rule to remove entries when
their PR merges.

Also fixes the coherence census's second count, which still said eight
settings three paragraphs below the nine it now lists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
2026-07-26 16:37:19 -04:00
Levi Neuwirth e4c9d65231 docs: fix five review-round-5 findings in the landed-doc refresh
Documentation only; no code, no protocol change. All five findings
reproduced before fixing.

1. The CRDT-dark census was stale. Re-measured at `fe8b8ba` under CI's
   exact flags versus the same flags plus `crdt`: 3,170 vs 3,443 —
   **273 dark, 185 in the library**, not the 264/177 #168 measured at
   `1b6a084`. The per-target table is regenerated (it gains a
   `terminal_copy_mode_acceptance` row, acc16e's, from this very arc),
   the rows are stated to sum to the total, and the lane now says the
   number moves with every merge and must be re-measured rather than
   quoted. #168's figure is kept as a dated historical reading with a
   pointer to the live one.

2. The generated-buffer non-adopter inventory was short by half. It is
   **four writers, not two**: listview panels (`listview.lua:60-61`),
   `*compilation*` **and** `*search-results*` — both through
   `compile.lua`'s shared `ensure_slot`, so naming only the first
   undercounts a mechanism rather than a buffer — and dired
   (`dired.lua:371`). All four pair an erroring intercept with
   `bypass_intercept` writes over a still-writable rope, and all four are
   emptiable by `M-x buffer.undo`. Corrected in the handoff §4 (as a
   table, with each writer's shape), `COHERENCE.md` §14, and the
   framing's deferred-lane text. The adoption estimate gains a
   consequence: the two `compile.lua` slots append and need a streaming
   variant; listview and dired are whole-buffer replaces and are the
   cheap half.

3. The ledger's canonical base still named `a27f646` while the same file
   recorded #168 and #178. Now `fe8b8ba`, with the recovery check's
   accept-or-newer floor moved with it — a stale floor is what lets a
   wrong base pass verification.

4. The completed terminal lane is **removed**, not marked complete. Rule
   4 of this file's own update protocol says a lane goes when it merges,
   and its opening contract says the file records only what has not
   landed. Its durable facts moved first: a new arc bullet in the handoff
   §1 (the snapshot materializes, so the dispatch-shadow count stays at
   six; `prune` reacts to removal rather than causing it; ownership means
   the handle table, never found-by-name; profiles are a raw Lua table
   and why the escape cache lives on `TerminalSession`; what criterion
   17 must assert when it can finally be written; the `cat -v` echo probe
   and count-don't-match rule), with the `set_generated_contents`
   invariant already in §4. A compact entry remains under "Closed since
   the last snapshot". The gate-run flake the lane carried moved to the
   CI `crdt`-coverage lane, which owns its discrimination — verbatim,
   including its explicit refusal to claim a root cause.

5. The refreshed handoff was internally stale: it anchors on a `main`
   that contains #179 and #165 while still calling both "in review".
   Both now read MERGED. Dired's durable facts are deliberately **not**
   absorbed here — that is open PR #169's job, and writing it from two
   PRs would put two authorities on one text — so the dired lane stays,
   with a note saying why it survives rule 4 and who removes it.

Also recorded while verifying finding 4's new home: the `crdt` Clippy
failure on `main` is re-verified with exact sites (four in `src/daemon.rs`,
one in `tests/auto_indent_crdt_acceptance.rs`), because any CI job that
compiles the `crdt` targets is red on arrival until they are fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
2026-07-26 16:28:04 -04:00
Levi Neuwirth a53965474d feat(lean4): the Unicode input method (Arc 8 Stage 4b)
Typing `\alpha` in a Lean 4 buffer gives `α`; `\<>` gives `⟨⟩` with the
point between them. The abbreviation table is vendored from
vscode-lean4 and the expander is a typed-edit consumer registered on
the Stage 4a chain at priority 50, ahead of auto-pairing.

The ordering is load-bearing. 64 abbreviation keys contain a character
in the `lean4` pair set, so with pairing first, typing `\[` would
insert `[]` and corrupt the pending key to `\[]` before the second `[`
arrives — `\[[]]` becomes unreachable. The consumer therefore claims
every keystroke that EXTENDS a pending abbreviation, not only one that
completes an expansion; claiming only completions would hand each
intermediate `[` to pairing by a different route.

The vendored table is an ORDERED SEQUENCE, not a map. Upstream breaks
equal-length ties by source declaration order — 101 prefixes depend on
it, and `\f` resolves through `f<` rather than `f>` — which a
`pairs`-iterated Lua table cannot express. `scripts/regen-lean-abbrev`
takes a vscode-lean4 commit, emits the file with its provenance header,
and aborts on a duplicate key, invalid UTF-8, or a round-trip mismatch.

Undo is cross-peer-degraded on CRDT frontends and that is accepted and
named, not papered over (Q#LN21): `\alpha` arrives as six source-peer
optimistic inserts while the expansion is one daemon-peer replace.
`set_round_trip_input` would fix it and also makes `dispatch_idle`
report false, so RET would stop inserting a newline.

Round 9 corrects three approved acceptance criteria that the real table
contradicts, found by simulating the state machine over all 1,855
entries and re-reading upstream at the pinned commit rather than
re-reading the prose. `\to` is not eager — `top`, `to0` and `toa`
extend it. `\zzzz` expands to `ζzzz ` because `ze`, `zeta` and
`zsqrtd` exist; only `$ % , ; @ W` open no key at all. And `\alpha`'s
undo does not restore `\alpha ` because `alpha` IS eager, so the
terminator is a separate edit. Criteria 38, 41 and 42 now state both
paths, and the false halves are asserted too: they read as correct
until the table is consulted.

Three implementation traps worth the record. The generator's own
round-trip check was broken twice and failed closed both times:
`str.splitlines()` splits on U+2028, which 53 symbols contain, and
escaping through `chr(byte)` produced a latin-1-shaped string that the
UTF-8 write re-encoded. The first check compared in-memory strings and
agreed with itself; it now stages the file, re-reads the bytes from
disk, and renames into place only on a match. And the expansion SHRINKS
the buffer, so the point must be placed explicitly — pairing's
no-cursor-motion rule holds only for an insert AT the cursor, and
without this every self-insert after the first expansion is silently
rejected and the editor looks dead.

25 acceptance tests plus one `--lib` test for the optimistic CRDT
producer (45f), which is where the gate list's `--features crdt` run
reaches it; a crdt-gated integration test would be dark in CI and in
the gates both. Fifteen mutations bite, each failing its target. Three
of these tests were vacuous when first written and biting is what
found them: the abandonment test asserted text a surviving record
would also produce, the re-arm test used an example that never reaches
the re-arm branch, and both switch tests ran through
`find_or_open`'s fresh-load path rather than `buffer.after-switch`.

No protocol change (Q#LN14). Also reconciles the handoff and ledger
for Stage 4a (#179) and adds `lean.abbrev` to COHERENCE.md's
config-registry adoption census, now nine settings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
2026-07-26 16:24:06 -04:00
Levi Neuwirth 74301d1670
Merge pull request #169 from levineuwirth/docs-dired-stage1-landed
docs: record dired Stage 1 (#165) as landed
2026-07-26 20:23:39 +00:00
Levi Neuwirth c5c9f3f13a Merge branch 'main' into pty-terminate-eperm
Clean integration of #178 (terminal copy mode); no conflicts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
2026-07-26 16:09:48 -04:00
Levi Neuwirth 4182270fd3 Merge branch 'main' into docs-dired-stage1-landed
Re-integrated after #178 (terminal copy mode) landed. One conflict, in
COHERENCE.md's journey table: row 7 keeps this branch's text (dired #165
is merged, so "PR #165" is wrong), row 8 takes main's, which adds copy
mode and the missing close/kill command to the terminal step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
2026-07-26 15:57:13 -04:00
Levi Neuwirth 44f5e44b9f docs: record #168 and #178 as landed, and the generated-buffer invariant
Documentation only; no code, no protocol change.

Handoff §1 anchors on `main` @ `fe8b8ba` and leads with terminal copy
mode (#178) and its landed-doc pair (#168). The active-work terminal lane
flips to BOTH STAGES MERGED, and the #166 lane records #168 landing with
what it measured: 264 tests dark workspace-wide for want of `crdt` in CI,
and the vterm audit's honest count of acceptances that drive a real
daemon (2, not 3).

The substantive addition is a new §4 substrate invariant for
`Buffer::set_generated_contents`, because it is now the one authorized
write for every generated buffer and three of its properties are
non-obvious enough that four review rounds were spent finding them:

- an intercept is not read-only — `Buffer::undo` reaches the rope through
  `ensure_writable` without consulting the intercept chain, and rebinding
  the undo chords does not help because `M-x buffer.undo` needs no
  keymap;
- a bare `set_read_only` would be worse than nothing, since it also
  refuses the owner's refresh — the pairing is the primitive, which is
  why no Lua `set_read_only` exists;
- a rope write is only half of an edit: the returned `Edit` must be
  fanned out, or a displaying window paints the new rope with stale line
  offsets and replica mirrors never import the write.

Also recorded: history clearing must clear whichever history the buffer
has (the v0.1 stacks are bypassed in CRDT mode), that `*compilation*` and
listview have NOT adopted the primitive and remain emptiable by
`M-x buffer.undo`, and that this does not replace `set_round_trip_input`
— the two guards cover different copies, and a daemon refusal arrives
after the frontend has already painted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
2026-07-26 15:48:03 -04:00
Levi Neuwirth f3e0b98ced Merge branch 'main' into docs-dired-stage1-landed
Re-integrated after #168 landed the ledger refresh. Five conflicts, none
resolved by taking a side wholesale:

- Both head-of-main anchors take main's a27f646 wording (written by
  #168); the ledger keeps this branch's caveat about lanes naming an
  older base.
- The dired lane section is pruned, which is this PR's purpose, but main
  had meanwhile pruned the GPU terminal input lane as landed — so the
  union drops both stale lanes rather than resurrecting either.
- "Closed since the last snapshot" keeps BOTH new entries: dired Stage 1
  #165 from this branch and GPU terminal input #166 from main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
2026-07-26 15:41:58 -04:00
Levi Neuwirth 8943b4c277 Merge branch 'main' into pty-terminate-eperm
Same one-line ledger conflict as the previous integration, now against
the refreshed main (#168): keep this lane's PTY terminate section, take
main's newer Lean 4 lane heading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
2026-07-26 15:00:49 -04:00
Levi Neuwirth 1b44c69a53 Merge remote-tracking branch 'githubsucks/main' into terminal-copy-mode
# Conflicts:
#	docs/active-work.md
2026-07-26 14:59:09 -04:00
Levi Neuwirth a58bb110bc docs(terminal): qualify process flake attribution
Separate the observed suite behavior from the suspected drain_until
mechanism, and leave round-2 failures unattributed without test names.
2026-07-26 14:54:37 -04:00
Levi Neuwirth 42f601a7ec Merge branch 'main' into gpu-terminal-input-docs, and refresh the ledger
The handoff's "last updated" paragraph conflicted; resolved by writing
the current statement rather than taking either side, and the same pass
clears the staleness the ledger had accumulated while five PRs contended
for it:

- Both head-of-main anchors (handoff §1 and the ledger's canonical base)
  now name a27f646, and list terminal config #173, the bottom-panel
  Stage 2 framing #175, Stage 2A #177 and Lean 4 Stage 4a #179 — plus
  the CRDT undo repro #157 and the inline-math landed-doc refresh #172,
  which are merged but had dropped out of both lists.
- The bottom-panel lane records Stage 2A as MERGED (#177 @ 0a3fcd1)
  instead of IN REVIEW, and states what 2B is: approved, unstarted,
  branching from main rather than stacking, with §7.2's criteria and
  §5.3's three-boundary trap named.
- The Lean 4 lane records Stage 4a as MERGED (#179 @ a27f646); its
  "framing-only, no code, awaiting approval" bullet was false.

The dired lane is left alone deliberately: #169 is the PR that records
dired Stage 1 as landed, and owns that heading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
2026-07-26 14:43:43 -04:00
Levi Neuwirth b123c0b9fe Merge branch 'main' into docs-dired-stage1-landed
Three conflicts, resolved by taking the newer statement on each side
rather than either side wholesale:

- COHERENCE.md journey row 7 keeps this branch's text (dired #165 is
  merged, not a PR); row 8 takes main's, which records terminal config
  #173.
- The ledger's canonical-base line takes main's d400f30 anchor and keeps
  this branch's caveat about lanes that name an older base.
- agent-handoff's two anchors take main's d400f30 wording, with the CRDT
  undo repro #157 and the inline-math landed-doc refresh #172 added
  back — both are merged and main's summary had dropped them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
2026-07-26 14:40:13 -04:00
Levi Neuwirth 23c966cc31 docs(terminal): restate criterion 17's bite for the fixed substrate
Review round 4, P2. A fix can invalidate a test that was never written.

Criterion 17 still specified the pre-round-2 world: remove
`set_round_trip_input` and the optimistic op "passes `ensure_writable()`
and mutates BOTH sides, silently, with no divergence to notice". That
was true while no Lua binding set `read_only`. Since
`set_generated_contents` does, the daemon refuses the op — so only the
frontend's own mirror mutates, and the copies diverge.

The gap matters precisely because 17 is unpinned. A real-GPU test
written to the old spec would hunt for a daemon-side edit that can no
longer occur and pass for the wrong reason, quietly readmitting the
round-2 regression through a test not yet built. The specification is
the artifact under review here, not the code.

Restated around unauthorized MIRROR mutation plus daemon refusal —
divergence — in all four places carrying the obsolete claim: the
criterion itself, the Q#TC6a heading, the acceptance-16 doc comment, and
the bite roster. The heading's "ONLY thing" now says what it is the only
thing FOR: the replica's own mirror. `docs/active-work.md` also still
described acceptance 16b as asserting `is_read_only()` is false, which
round 2 flipped.

Why round-trip input stays load-bearing rather than redundant, now
stated wherever the daemon guard is mentioned: a refusal arrives after
the frontend has already applied optimistically and painted. It buys
divergence instead of silent agreement; it does not prevent the mutation
the user is looking at.

Also recorded, after capturing it properly this time: the gate-run flake
in `cargo test --lib --features crdt` is
`process::tests::setsid_escapee_is_not_reaped_and_teardown_reclaims_readers`
(`active_reader_probe` -> None, "live runtime probe"), ~1 run in 5.
Pre-existing and unrelated — this branch does not touch
`src/process.rs`, the test passes 10/10 standalone and 2017/2017 at
`--test-threads=1`, and it is another instance of the known `drain_until`
trap: draining for `Started` also ticks, and a tick reaps the leader.
That also explains the unattributed "2 failed" run noted in round 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
2026-07-26 14:38:22 -04:00
Levi Neuwirth 9e6c166c57 Merge branch 'main' into pty-terminate-eperm
Resolves the ledger conflict: keep this lane's PTY terminate section and
take main's newer Lean 4 lane heading verbatim. No content of either
lane is rewritten here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
2026-07-26 14:32:15 -04:00
Levi Neuwirth a27f6467ea
Merge pull request #179 from levineuwirth/lean4-stage4a-typed-edit-chain
Lean 4 Stage 4a: the typed-edit consumer chain
2026-07-26 18:00:48 +00:00
Levi Neuwirth ea9b8c379e docs(lean4): correct Q#LN10's throw-containment rationale
Q#LN10 still said a throwing consumer "fails the fan-out for everyone."
It does not: `run_all_must_succeed` (src/hook.rs:332) collects the error
and continues to the hook's remaining subscribers, so `lsp.lua` still
flushes didChange. The throw stops every LATER consumer in the chain,
which is a narrower consequence and still worth containing — the
failure is silent exactly where the abandoned consumers registered.

The module comment, criterion 46d, the test, and the ledger were all
corrected in the previous commit; Q#LN10 is the decision they descend
from, so leaving it stale would have made the disproven claim the
authoritative one. Also records the protected-rendering rule there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
2026-07-26 13:43:30 -04:00
Levi Neuwirth a0fb01f24c fix(buffer): fan out generated writes, and clear the history that exists
Review round 3, on the round-2 primitive itself. One lesson covers all
three findings: a rope write is only half of an edit, and "discard
history" means whichever history the buffer actually has.

P1 — the binding swallowed the edit. `set_generated_contents` returned
`()`, so nothing reached `notify_buffer_edit_to_windows`. Two
consequences, both reproduced by the reviewer. In the default build a
window showing the buffer kept a `TextView` line index describing the
PREVIOUS contents, and the next paint indexed the new rope with stale
ranges — `assertion failed: end <= self.len()` in `src/rope.rs`. In the
CRDT build `pending_crdt_ops` stayed empty, so replica mirrors never
imported the owner's write and their optimistic edits were generated
against content already replaced. The `delete`+`insert` pair this
replaced had done that fan-out for free.

Now applies ONE whole-buffer `Replace`, returns its `Edit`, and notifies
from the binding. The doc comment states the obligation, because the
next owner to adopt the primitive inherits it.

P2 — "discard history" was false in CRDT mode. The v0.1 stacks are
bypassed entirely there; the history lives in loro's `UndoManager`.
`read_only` stops the replay but not the retention, which is the memory
cost the contract claims to eliminate. `UndoManager` exposes no clear,
but needs none: it records only what happens after it is constructed,
the same property `CrdtState::from_bytes` already uses to keep the seed
insert out of undo. `CrdtState::clear_undo_history` rebinds a fresh
manager to the same doc.

P2 — the docs described the pre-fix architecture. Q#TC6a said no Lua
binding sets `read_only` and round-trip input is the only guard; the
acceptance text still said `is_read_only() == false` while 16b had been
flipped to true; `terminal.lua`'s comment repeated the obsolete claim.
The architecture is layered and now says so: rope-level read-only
protects the daemon copy, round-trip input protects the replica's
optimistic mirror, and neither substitutes for the other. Q#TC6a keeps
its analysis under a superseded-in-part box rather than being silently
rewritten — its conclusion survives, two of its premises do not.

New pins. acc16d paints the window after a SHRINKING generated write:
stale offsets then point past the buffer end, so the failure is the
reported crash rather than merely stale pixels. acc16e asserts the
refresh is queued for mirrors, through the real copy-mode path;
`crdt`-gated and therefore dark in CI, which is why 16d drives the
binding rather than the terminal. Plus a CRDT unit test that ten renders
leave the `UndoManager` with nothing recorded.

Bites: dropping the notify panics acc16d at `rope.rs:145` and fails
acc16e with `queued: []`; dropping the `UndoManager` rebind fails the
new unit test on `can_undo`.

Still open, and recorded in COHERENCE.md §14: the fan-out obligation
makes `*compilation*`/listview adoption more than a one-line swap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
2026-07-26 13:43:22 -04:00
Levi Neuwirth aef4e98c26 fix(typed-edit): close round-8 review on the consumer chain
Five defects in the chain itself, plus the stale handoff state.

Each consumer now gets its own shallow copy of the typed-edit record.
Handing everyone the same table let a DECLINING consumer rewrite
provenance for the ones behind it, and pairing decides what to close
from `rec.char` — so a forged `char` turned a typed `x` into `x)`.
Every field is a scalar or an opaque id, so a shallow copy is complete.

The fan-out iterates a snapshot of the consumer list. It was iterating
the same array `add_consumer` mutates: a consumer that registered a
lower-priority one shifted itself forward under `ipairs` and ran twice,
and re-registering made that unbounded. Registrations and removals made
during a fan-out now take effect on the next one, stated as a contract
and pinned in both directions.

`tostring` on the caught error moved inside the containment. A Lua
error may be any value, including a table whose `__tostring` throws —
rendering it outside the `pcall` reintroduced exactly the escape the
containment exists to prevent.

Priorities are validated as finite integers in i32 range, matching
`pmacs.completion.register`. NaN is a number and every ordered
comparison with it is false, so a NaN consumer landed wherever the
insertion scan gave up and silently voided the lowest-first ordering
that Q#LN22 depends on.

`add_consumer` returns a handle and `remove_consumer` unregisters it,
reporting whether it was live. Without teardown the chain inherited the
`pmacs.hook.add` callback leak COHERENCE.md §13 already records, and
spread it to every consumer.

Also corrects the rationale the containment was documented with, in the
module, the test, and the framing: an uncontained throw does NOT take
the fan-out's other subscribers down. `run_all_must_succeed`
(src/hook.rs:332) collects errors and continues, so lsp.lua still
flushes didChange. The containment is still required — the throw skips
every later consumer in the chain — but the reason is narrower than
rev 7 claimed.

Criteria 46f (record isolation), 46g (snapshot iteration), and 46h
(lifecycle and priority validation) added; 46d's rationale corrected.
Four new tests, all bite-verified by mutation, each failing only its
target: shared record table (1), live-array iteration (1), unprotected
tostring (1), bare number check (1), no-op removal (2). The suite also
runs green under `--features lua54`.

docs/agent-handoff.md said Stage 4a was awaiting approval while this
branch had it implemented and in review.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
2026-07-26 13:39:33 -04:00
Levi Neuwirth 0a3fcd1942
Merge pull request #177 from levineuwirth/bottom-panel-stage2a
feat(panel): bottom-panel Stage 2A — classified census routing + painter extraction
2026-07-26 17:28:49 +00:00
Levi Neuwirth 842417200a fix(panel): close Stage 2A review round 3 (2 P1)
**P1-1 — layout invalidation could suppress the authoritative clear.**
Real bug. Both render paths resolved the document identity AFTER the
evaluator ran callbacks, but BOTH outcome arms carry PHASE-1 contexts.
A provider that closes the primary document split changes
`primary_document_window` mid-evaluation, so the filter compared
phase-1 contexts against a replacement identity, matched nothing, and
emitted no clear — leaving stale statusline text on the wire forever.

The identity is now captured BEFORE `evaluate_statusline` runs and
threaded through both paths (the terminal path via `terminal_chrome`).

Pinning it took three attempts, and the two failures are the useful
part:

- `pmacs.window.close()` takes no argument — it closes the ACTIVE
  window. The first version passed a window id that was silently
  ignored, so it closed the panel instead of the document.
- The Lua window API acts on the ACTIVE FRONTEND, so driving it against
  a synthetic semantic view changed nothing at all.
- Closing the only document window is structurally REFUSED (Q#BP6
  forbids a lone side window as a resting state), so the fixture needs
  TWO document windows for the close to be legal.

The test now asserts its own precondition — that the callback really
changed the identity — before asserting the clear, and reproduces the
reported symptom (no `StatuslineSegments` at all) when the fix is
reverted.

**P1-2 — #21 was pinned at the helper, not the producer.** Confirmed:
reverting only the call site inside
`publish_buffer_snapshot_to_replicas` left both the helper test and the
existing socket-pair test green. The helper assertions are removed (with
a note saying why) and replaced by
`snapshot_publication_follows_the_document_under_a_focused_panel`, which
drives the real producer over socket pairs and asserts BOTH directions:
the document buffer's snapshot is delivered while a panel holds focus,
and a panel-only buffer's is not.

Biting that test exposed a defect in the test itself: the delivery read
had no timeout, so a regression made it HANG rather than fail. A hanging
test is strictly worse than a red one — every read now has a timeout.

Gates: fmt clean; workspace clippy clean; 1,832 default + 2,015 CRDT
library; Stage 2A 17; Stage 1 46; statusline 8; m11_5 2; GPU initial
target 14; terminal config 12; folding Stage 2 48; vterm 1/2 10 / 6;
M4 121; required GPU 202; `git diff --check` clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 13:18:05 -04:00
Levi Neuwirth 8c5b39ef32 fix(buffer): make generated buffers survive undo
Review round 2, P1. Undo could empty the "read-only" snapshot.

`render_snapshot` wrote with bypass_intercept, which leaves ordinary
undo history behind, and `Buffer::undo` reaches the rope through
`ensure_writable` without ever consulting the intercept chain. So a
single `C-/` — or `M-x buffer.undo`, which needs no keymap at all —
replaced a freshly rendered snapshot with an empty buffer.
`set_round_trip_input` does not help: it routes the key into the daemon
command path, which is exactly where undo runs.

Rebinding the undo chords buffer-locally would not have closed this,
and `compile.lua` already says so in a comment: "command/menu undo
stays dispatchable". `*compilation*` and listview panels therefore
carry the same latent defect today.

Adds `Buffer::set_generated_contents` (Lua:
`pmacs.buffer.set_generated_contents`): lift `read_only`, replace the
contents skipping intercepts, discard the resulting history, re-assert
`read_only`. This ships the framing's deferred immutability lane as ONE
primitive rather than exposing the setter — a bare `set_read_only`
would let a caller lock a buffer it can no longer refresh, which is
precisely why that lane was deferred. Discarding history is
load-bearing twice: it removes what undo would replay, and it stops a
periodically refreshed buffer accumulating rope clones that `read_only`
guarantees nothing can ever pop.

New acceptance 16c drives the real M-x path
(`command.invoke_interactive`), the chord, and redo, and asserts the
owner's own refresh still works — the operation plain `read_only` would
have broken. Acceptance 16b flips from asserting `is_read_only()` is
false to true, because the property it documented is the one that was
wrong. Three `buffer.rs` unit tests cover the primitive directly,
including that ten refreshes leave an empty undo stack.

Bite: restoring the delete+insert render reproduces the report exactly
— `left: Some("")` against the full snapshot — failing 16c and 16b.

Still open, and now named in the framing, COHERENCE.md §14 and the
ledger: `*compilation*` and listview have not adopted the primitive and
remain emptiable by `M-x buffer.undo`; a streaming-friendly variant is
needed for the append case. In CRDT mode `read_only` is what refuses
undo, since loro's UndoManager exposes no clear through `CrdtState`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
2026-07-26 13:12:08 -04:00
Levi Neuwirth c7072b49e9 docs: record the Stage 4a lane and its bite table
Verification describes the pushed tree, per the standing rule. Includes
the bite that was worthless as first written: moving only typed_edit.lua
past lsp.lua broke the runtime load instead of testing flush ordering.
A bite that kills everything has not isolated anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
2026-07-26 13:05:41 -04:00
Levi Neuwirth 174e36fce3 docs(lean4): reconcile peer edits with pending ownership
Advance the Stage 4 framing to revision 8. Keep pending abbreviation
state frontend-owned while conservatively invalidating it after any
intervening shared-buffer edit, make the revision token explicit, and
rewrite acceptance 45i around that contract.

Correct the active-work multi-codepoint count and the stale coherence
revision label.
2026-07-26 10:49:43 -04:00
Levi Neuwirth b9fbb42dc0 Merge remote-tracking branch 'githubsucks/main' into terminal-copy-mode 2026-07-26 10:45:32 -04:00
Levi Neuwirth 2eb6218ccd fix(terminal): close review round 1 on copy mode
Four findings, all real, and they rhyme in pairs. Two implementation
defects and two vacuous pins, all four tracing to one root: a name is
not an identity, and a context-free readout is not a state observation.

A foreign buffer carrying the snapshot's name was adopted and then
overwritten. `pmacs.buffer.create` accepts any caller-chosen name and
snapshot writes use bypass_intercept, so found-by-name adoption
clobbered user data — the reviewer reproduced "do not clobber" becoming
23 newlines. Now follows dired's F7 rule: ownership means "in copy
mode's own handle table", never "found by name", and a taken name yields
a `<2>` variant.

Snapshot identity was keyed by terminal NAME.
`TerminalManager::open` uniquifies only the derived name — an explicit
`name = ...` is inserted verbatim — so two valid terminals can share
one, and a name-keyed table handed them a single snapshot: the second
invocation retargeted it, `q` returned to the wrong terminal, and
killing either removed the shared buffer. Identity is now the terminal
buffer, compared in an array, because BufferIdLua implements `__eq` but
each wrapper is a distinct table key: comparison works, hashing does
not. The kill-with-terminal callback now closes over its own record
rather than looking the name up again.

The refresh pins were vacuous. Acceptance 19 compared a quiet
terminal's snapshot against itself and 18 counted buffers, so both
passed with render_snapshot replaced by a no-op. The child is
`exec cat`, so the tests now type a marker into the focused terminal,
require it ABSENT from the existing snapshot, and only then refresh —
via `g` and via re-invocation respectively.

The tail-follow pin could not observe view state.
`TerminalManager::snapshot(buffer_id)` is context-free and always
returns the live screen, so it reported "at the tail" even for a view
forced to the oldest retained row. Now read through
`snapshot_for_view`'s at_bottom and its projected cells.

Adds acceptance 18a (a foreign same-named buffer is never adopted or
clobbered) and 18b (two same-named terminals get two independent
snapshots, each `q` returns to its own source, and killing one leaves
the other's snapshot alive).

Four new bites, all discriminating: restoring adopt-by-name fails 18a
AND 18b; restoring name-keyed identity fails 18b; making
render_snapshot a no-op fails BOTH 18 and 19, which is the vacuity
demonstrated rather than argued; and forcing the view off the tail
fails 20.

Criterion 17 stays a named follow-up, per review agreement, until the
real GPU probe is non-skipping and CI-executed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
2026-07-26 10:45:32 -04:00
Levi Neuwirth c4fad0731c docs(lean4): rev 7 — round 6 review, five P1s, and reconcile the ledgers
The 4a/4b split held; five P1s against rev 6's own content, all real,
all reproduced. Four share a root: rev 6 verified its external facts and
under-verified its internal ones.

1. Stage 4a's declared footprint excluded the tests its own acceptance
   required. 46a-46e cannot live in tests/auto_pair_acceptance.rs,
   which criterion 46 requires byte-identical. Footprint now names
   tests/typed_edit_chain_acceptance.rs and gates on it.

2. Pending abbreviation state had the wrong owner. pmacs is
   multi-frontend: EditorCore.views is per-FrontendId with its own
   active window, take_typed_edit is already frontend-keyed, and
   buffer.after-switch fires with no arguments — so a buffer-keyed
   clear-on-switch lets any frontend discard another's pending
   abbreviation. Now keyed (frontend, buffer) with a window check,
   frontend-scoped clearing, a frontend.detached purge, and acceptance
   45i, which the buffer-keyed design passes every other criterion
   without.

3. The shortest-match rule was missing its tie-break: upstream keeps
   declaration order among equal-length shortest keys, and 101 prefixes
   have equal-shortest candidates resolving to different symbols (f
   picks f< over f>). A pairs-iterated Lua map cannot express this, so
   the vendored artifact is now an ordered sequence and resolution sorts
   by (#key, source rank). Rev 6 missed this because it declared the
   package ships no README after a 404 on the package root, with the
   directory listing showing src/README.md already in hand — a 404 on a
   guessed path is not evidence of absence, and the README states the
   rule in one sentence.

4. The generator's rejection rule rejected the current table: \ is a key
   and " begins eleven, while acceptance 45d requires \ to work.
   Replaced with canonical lossless escaping; aborts only on duplicate
   keys, invalid UTF-8, and a failed self-round-trip. 45g no longer
   claims to diff against abbreviations.json, which is not shipped.

5. Durable and volatile state were not reconciled. agent-handoff.md
   anchored main at d152120 with neither #167 nor #170 and no Lean arc
   bullet at all; active-work.md kept 407 lines of merged Stage 1/2/3a/3b
   history against its own instruction to prune merged entries, under a
   stale snapshot date. Durable facts moved to the handoff; the ledger
   keeps only the unlanded Stage 4 lane.

Also corrected: 119 multi-codepoint symbols (26 with $CURSOR), not 93;
three backslash values, not two; Q#LN22 now states the terminating-\
reprocess rule acceptance 45d depended on; acceptance 38 says the
terminator is retained, so undo restores "\alpha " with its space;
coherence cites golden-journey step 5, not step 4; and the
config-registry prior art points at Q#LN22.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
2026-07-26 10:39:01 -04:00
Levi Neuwirth ccdf352258 fix(panel): close Stage 2A review round 2 (2 P1, 1 P2)
**P1-1 — the `Invalidated` arm published the panel context on the
document wire.** Real bug, and the live half of the routing defect: the
semantic peer has ONE statusline slot, so emitting an
authoritative-empty payload for every context replaced the document's
with the panel's. Now filtered by document-window identity exactly like
the `Ready` arm; a panel's own clear belongs to `PanelFrame` in 2B.

Pinned by `invalidated_statusline_clears_only_the_document_not_the_panel`,
which reproduces the reported shape — two targets instead of one — when
the filter is removed.

Honest note on the `Ready` arm: its identity selector is **defensive**,
not independently falsifiable today, because the document context is
captured first so "first context for my frontend" happens to pick it.
Rather than leave that as a silent dependency,
`the_semantic_fan_out_captures_the_document_first` pins the order and
says why it matters.

**P1-2 — round-1 finding 3 was not closed; four of my pins were
vacuous.** All four confirmed and fixed:

- The statusline consumer test discarded `render_frame`'s output. It now
  observes the WIRE payload from a v18 peer with a registered provider,
  and asserts non-emptiness so it cannot pass by emitting nothing.
- The terminal test compared two NON-terminal buffers, so both routings
  answered `false`. The document window now holds a REAL terminal, so
  the routes disagree; reverting `semantic_terminal_key` fails it.
- The decorations test used different buffers and an empty selection —
  again the same answer either way. The panel now displays the declared
  buffer with a non-empty selection while the document has none.
- #1/#3/#21 had no discriminating pin at all. Their only production
  caller is `dispatcher_loop`, which no test can drive, so this extracts
  three named seams the loop calls — `document_buffer_to_follow`,
  `document_cursor_byte`, `peer_displays_buffer_as_document` — and pins
  each.

Also newly pinned: #2 the lazy CRDT upgrade (the census's sharpest
case), #7 `Viewport` aligning WITHOUT taking focus, and #9 a focused
terminal panel not suppressing the document viewport.

**Every one of the nine pins was falsified by revert.** Two needed a
second attempt after the first bite came back green.

**P2-3 — stale docs.** `StatuslineEvaluationTarget::Semantic`'s
documentation described evaluating only the focused window; it now
describes the document-plus-side fan-out, the capture order, the
identity-selection requirement, and that `active` reports actual focus.
The ledger's Stage 2A entry is corrected to five commits, 2,014 CRDT
tests, and 16 acceptance tests.

Two clippy findings the refactor introduced were fixed:
`document_buffer_to_follow` is `crdt`-gated to match its only caller,
and the `CursorByte` guard collapses into one `if`.

Gates: fmt clean; workspace clippy clean; 1,832 default + 2,014 CRDT
library; Stage 2A 16; Stage 1 46; statusline 8; m11_5 2; GPU initial
target 14; terminal config 12; folding Stage 2 48; vterm 1/2 10 / 6;
M4 121; required GPU 202; `git diff --check` clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:38:21 -04:00
Levi Neuwirth 1b1e599070 feat(terminal): copy mode over retained scrollback
Stage 2 of docs/terminal-config-and-copy-mode-framing.md (rev 4,
approved). `M-x terminal.copy-mode`, or `C-t` in a terminal buffer —
physically `C-c C-t`, since every unescaped key goes to the child —
materializes the retained scrollback into an ordinary read-only,
path-less buffer, with `g` to re-snapshot and `q` to return.

No protocol change.

Materializing is the whole design. isearch, motion, selection and the
kill ring work with no new substrate because the snapshot is a rope, so
SearchStore and the existing match painting apply unchanged. And "keys
must not reach the child" dissolves structurally rather than being
guarded: the transport arm keys on is_terminal(buffer_id), and a
snapshot is not a terminal, so the arm never fires. The
dispatch-shadow count stays at six and describe-key keeps telling the
truth — asserted directly, since that is the observable difference
between the buffer-local idiom and a shadow.

One serializer, not two (Q#TC7). `copy_retained` builds a whole-range
selection and hands it to `copy_selection_bytes`; a second walk would
re-derive soft-wrap joining, wide-glyph continuation, cluster bytes and
per-row trailing-blank trimming, and the two would drift. Four unit
pins in view.rs assert exact bytes against the same projection fixtures
that pin the serializer itself.

Q#TC6a is implemented as two calls, and the second is the load-bearing
one: an intercept guards dispatch only, and no Lua binding sets
Buffer::read_only, so set_round_trip_input is what keeps a replica
frontend from applying optimistically and emitting an op that would
pass ensure_writable and mutate both sides. Acceptance 16 pins that
UNGATED, because CI never compiles the crdt feature.

Eight of nine criteria. Criterion 17's semantic-frontend end-to-end pin
is deliberately absent: the optimistic apply lives only in
pmacs-gpu/src/main.rs and the headless SemanticClient has no optimistic
path, so a faithful test needs the real GPU binary — the a37
foundation, which CI never compiles, silently returns ok when the
binary is unbuilt, and is load-sensitive. Both halves of the mechanism
are pinned ungated instead (16, and 16b for the hazard); the wire-level
half stays an explicit obligation of the CI crdt-coverage lane.

Substrate fact found while wiring lifecycle: TerminalManager::prune
REACTS to a buffer already gone from the registry rather than removing
one, so a child exiting leaves both the terminal and its snapshot
alive. That is why on_removed is a sound teardown hook, and why a
finished command's output stays readable.

Five bites, five different wrong implementations, each failing exactly
one test: removing set_round_trip_input fails acceptance 16 in the
DEFAULT configuration; a naive independent serializer fails all four
unit pins, with the diffs naming each drift mode; making re-invoke
create a fresh buffer fails 18; dropping the kill-with-terminal
teardown fails 18; removing the intercept fails 16b.

COHERENCE.md: §6 gains this as the worked example that a modal-looking
feature need not become a shadow; §11 records the scope="global"
deferral's second live case, making the argument for both registry
deferrals cumulative; §2 step 8 gains copy mode and keeps the
still-missing close command named.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
2026-07-26 10:10:27 -04:00
Levi Neuwirth 3b54c78494 docs(lean4): rev 6 — re-scout Stage 4 and split it into 4a and 4b
Stages 3a and 3b landed (#167, #170). Re-scouting Stage 4 against main
@ d400f30 produced six findings that change the plan and three that
confirm it. The pmacs-side facts were verified in a worktree at that
commit; the upstream facts by reading leanprover/vscode-lean4 @ 17d1d08.

The split: Stage 4's risk column read "refactors pair.lua's provenance
read" — every language's auto-pairing — for a stage the prose called the
Lean input method, which is exactly the rule §4 states and exactly what
round 4 found for Stage 3. Rev 5 had noticed the shape and answered it
with a commit boundary; a commit boundary is not a review boundary.
Stage 4a is now the typed-edit consumer chain (substrate, no Lean) and
4b the input method.

Rev 5's expansion semantics were wrong in three ways. Resolution is the
shortest key having the input as a prefix (\al yields ∀ from `all`, not
`alpha`); there is no terminator list at all ('+ ' is a key, so space
extends after \+; '\' is a key, so \\ yields \); and an unmatchable tail
is appended rather than dropped (\alp7 yields α7).

Three further findings. There is no cursor-motion hook, so acceptance 43
as written was not buildable and abandonment is lazy. dispatch_key is
only half of 4b's production path — \ and the letters are not excluded
from the optimistic classifier, and that producer is crdt-gated, so a
crdt-gated integration test is dark in CI and dark in the gate list. And
the whole expansion has cross-peer-degraded undo, a wider bite than
Q#LN6's three bracket pairs; set_round_trip_input would fix it and is
rejected with reasons.

New decisions Q#LN21 (undo degradation) and Q#LN22 (the state machine);
Q#LN10 and Q#LN11 rewritten; §2.11 records the upstream algorithm; §9.1
states the coherence impact for both stages. Acceptance keeps its
existing numbers and adds letter suffixes on both sides of the split.

Citation sweep per COHERENCE §25: five live citations moved in the 50
commits since rev 5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
2026-07-26 10:10:06 -04:00
Levi Neuwirth 6b2b0f9dd2 fix(panel): close Stage 2A review round 1 (4 P1, 2 P2)
Integrates canonical `main` @ `cf54270` and closes every finding.

**P1-1 — a stale document `Pointer` stole focus from the panel.** Real
bug. `align_primary_document_window`'s unknown-buffer arm returned
`Some(window)` despite aligning nothing, so #8's activation focused the
document *before* `dispatch_pointer` rejected the mismatched buffer. It
now returns `None`: alignment did not happen, so no caller may treat it
as a document gesture.

Pinned through `handle_dispatcher_event` — the real dispatcher seam —
because the defect lived in the PAIR of alignment and activation, not
in either alone. **The first version of that test was vacuous**: an
unregistered session is dropped at `daemon.rs:1962` (#148's
membership check) before the aligner runs, so it passed with the bug
restored. It now registers a real semantic session and fails with
exactly the reported symptom, focus moving `WindowId(2)` →
`WindowId(3)`.

**P1-2 — the approved A2A-2 fan-out was missing.** The semantic target
returned one context. It now captures the primary document PLUS the
visible side window, each provider invoked once, with a
derived-hidden side omitted (Q#BP2b — no mode line to paint, so no
callback should run for it). The acceptance asserts `windows.len() == 2`.

This exposed a second defect the finding did not name: the consumer
selected segments with `.find(|w| w.context.frontend_id == frontend_id)`
— the FIRST context for the frontend. With two contexts that silently
depended on capture order and could have shipped the panel's mode-line
text as the document status band. `emit_statusline_segments` now takes
the document `WindowId` and selects on window identity.

**P1-3 — the census suite tested the authority, not the consumers.**
Confirmed: reverting a producer to `active_window_for` left all ten
tests green. Added consumer-level pins that drive the real producers
through `SemanticRenderState::render_frame` with a panel focused, plus
the terminal-declaration guard. Bite-verified: reverting the
`LineNumbers` routing now fails
`consumer_line_numbers_follow_the_document_not_the_focused_panel`.

**P1-4 — main integrated.** The textual conflict was `docs/active-work.md`
(both lanes rewrote the same region; the terminal-config lane is kept
whole and the bottom-panel heading updated). `src/editor.rs` auto-merged,
and the full gate suite was rerun on the merge result.

**P2-5 — the painter test was vacuous.** A fixed-point check that
survived deleting `window.text_view.render`. It now asserts each of the
four extracted outputs actually appears: buffer TEXT, the line-number
GUTTER (with line numbers explicitly enabled, rather than dropping the
assertion), the window MODE LINE, and a returned caret. Bite-verified
by deleting the render call.

**P2-6 — the stale fold-projection claim is corrected.**
`src/window.rs`'s `fold_projection` doc no longer asserts that a
semantic session never enters `paint_frame`; it records that the panel
band breaks that premise and that the extracted painters take the map
as a parameter.

Gates on the merge result: fmt clean; workspace clippy clean; 1,832
default + 2,010 CRDT library tests; Stage 2A acceptance 13; Stage 1 46;
statusline 8; m11_5 2; GPU initial target 14; terminal config 12;
folding Stage 2 48; vterm 1/2 10 / 6; M4 121; required GPU 202;
`git diff --check` clean. `vterm_stage3_acceptance::a37` remains the
pre-existing flake measured on the base commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:02:01 -04:00
Levi Neuwirth f78a5beedc Merge branch 'lean4-stage3a-seams' into lean4-stage3b-server
# Conflicts:
#	docs/active-work.md
2026-07-26 09:36:18 -04:00
Levi Neuwirth 23486fd572 Merge main, and fold #173's a37 findings into the CI-coverage lane
Integrates githubsucks/main @ cf54270 (terminal config Stage 1, #173).
Both conflicts were additive appends to the same lists — the handoff
preamble and the ledger's "Closed since the last snapshot" — so both
sides are kept and the preamble now leads with #173.

Also records what gating #173 measured about a37, because it changes
this lane's proposed fix rather than merely annotating it:

a37 reports `ok` without running whenever `pmacs-gpu` is absent from
the target directory. A fresh worktree reports the Stage 3 suite 9/9 in
0.17 s having never executed the arc's only real-daemon/real-PTY/real-
wgpu path; a genuine run takes about four seconds. Only
PMACS_REQUIRE_GPU=1 promotes that skip to a failure, and the standing
gate list applies that flag to `cargo test -p pmacs-gpu`, a different
package. So fix-shape part 2 must state the flag as a requirement of
the gpu-render job: a crdt leg added to the plain `test` job would run
a37 vacuously and report green.

a37 is also load-sensitive — it passed at d152120 and failed at that
same commit twenty minutes later under machine contention — which makes
a red first CI run ambiguous by construction. The lane now says to
re-run on the merge base before believing a failure, and to prefer
serialized execution over retry-until-green.

The vterm audit's "only 3 of 9 Stage 3 tests drive a real daemon" is
corrected: without the frontend binary the honest number is 2.

Ledger: terminal config Stage 1 flipped from IN REVIEW to MERGED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
2026-07-26 09:29:42 -04:00
Levi Neuwirth 7243714b3f Merge remote-tracking branch 'githubsucks/main' into lean4-stage3a-seams
# Conflicts:
#	docs/active-work.md
2026-07-26 09:25:25 -04:00
Levi Neuwirth dec51960da docs: record round-six bite evidence
Capture the concrete counterfactual outcomes for all five review fixes
against 19f48d4.
2026-07-25 22:36:02 -04:00
Levi Neuwirth 3b7cc67197 docs: record the sweep-found test race in the lane
The parallel sweep failed one of the new tests for a real reason, not a
flake: drain_until ticks, and a tick can reap an immediately-exiting
child before the diagnostic runs. Recorded with the matched-load
measurement that shows the fix is load-bearing (0/15 fixed vs 1/10
unfixed under full saturation), and the final sweep numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZjWMjwPXhPbt9upku9mCk
2026-07-25 22:33:22 -04:00
Levi Neuwirth 786de69d38 fix(lsp): close round-six Lean fallback gaps
Make command-time attachment healing cancel an armed terminal restart
before replacing the server, while keeping request-only lookup pure and
restart-safe.

Track config-driven server ownership privately, bound every fallback
server per SID, scope no-swap retirement to the failed root, and route
the shipped Lean diagnostics command through the safe resolver while
waiting for initialization.

Add direct acceptance counterexamples for all five review findings and
record the sixth-round verification and vacuity lesson.
2026-07-25 22:32:48 -04:00
Levi Neuwirth 18d481b046 docs: record the PTY terminate diagnostic lane in the ledger
The ledger's own update protocol requires a lane for volatile work, and
PR #176 had none: branch, worktree, review state, and verification were
all missing.

Records why the lane ships a diagnostic rather than a fix -- three
rejected tolerance designs, the two facts that killed the original
argument (group=true is rejected for PTY mode so the reap ledger never
applies to that path, and the ledger comment asserts EPERM cannot happen
rather than ruling that it means dead), and that the CI evidence never
established the child had exited.

Also records the round-1 test fixes and the four verified bites, so a
reader can tell which assertions are load-bearing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZjWMjwPXhPbt9upku9mCk
2026-07-25 22:19:19 -04:00
Levi Neuwirth 00e096ef81 Merge remote-tracking branch 'githubsucks/main' into pty-terminate-eperm 2026-07-25 22:18:32 -04:00
Levi Neuwirth 4413ea93d0 docs: record the Stage 2A lane and what gating it found
Ledger entry for the in-flight Stage 2A branch, plus three findings the
gate run produced that are worth carrying regardless of this PR:

- The structural test comparing the two authorities directly did NOT
  catch the focus-class bite; only the consumer-level assertion did.
  Both kinds are needed, and the distinction generalizes.
- `vterm_stage3_acceptance::a37` is badly flaky on this machine —
  6/8 failures on the BASE commit against 7/8 on the branch in matched
  isolated samples, so it is pre-existing rather than a regression. It
  also returns `ok` without running unless `pmacs-gpu` is built.
- `m11_5_semantic_acceptance` reports 0 tests and
  `gpu_initial_target_acceptance` reports 1 without `--features crdt`.
  Both are semantic-census suites, so gating Stage 2A in the default
  config alone would exercise almost none of its relevant coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 22:14:37 -04:00
Levi Neuwirth 56eaf2f07a Merge remote-tracking branch 'githubsucks/main' into terminal-config
# Conflicts:
#	docs/active-work.md
2026-07-25 21:50:25 -04:00
Levi Neuwirth 8e8f281f0e fix(terminal): close review round 1 on Stage 1
Five findings, all real. The blocker and both majors are the same
mistake in three places: a claim asserted somewhere cheaper than where
it actually lives.

COHERENCE.md was stale in four places, not the three reported. Step 8
still read "no keybinding" and §11 still read "five settings", but §6's
dispatch table also still cited `is_terminal_escape_chord` — a symbol
this branch deletes. §25 requires that update to ride the PR, so a PR
changing audited ground truth has to re-grep the audit for its own
symbols, not only for its topic.

Acceptance 5 asserted a registry round-trip, which is a test of the
registry: it stayed green with the setting's only consumer deleted. It
now opens a real terminal whose child overflows the 24-row screen,
scrolls the view to its oldest retained row, and asserts LINE001 is
present at 10,000 and absent at 0.

Acceptance 8a waited for the session count to fall, which the rejected
editor-side cache map satisfies exactly — a map with no purge hook
leaks while sessions drain. Adds `TerminalManager::escape_caches()`, the
lifetime half of Q#TC4c's contract that `escape_parses` cannot cover.

`table.sort` over `pmacs.terminal.profiles` raised "attempt to compare
number with string" on the unknown-profile path whenever the user's
table held both a string and a numeric key, replacing the exact
diagnostic being asked for; `%q` raised likewise on a non-string
`profile` argument. Both are partial functions applied to user input on
a diagnostic path.

Also corrects the framing's status line, and a status message whose
embedded whitespace run had survived a rustfmt reflow.

Three new bites, each falsified by revert: deleting the scrollback
consumer fails acc5 and only acc5; restoring the raw-key sort
reproduces the comparison error verbatim; and implementing the rejected
map fails the new acc8a at left: 2, right: 1 while passing the old
session-count version.

Merges githubsucks/main @ ccf29e3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
2026-07-25 21:47:37 -04:00
Levi Neuwirth 19f48d46c0 fix(lsp,lean): bound the fallback's own failure; heal at point of use
Round 5 review: one P1, a frontend scope hole, and three P2s.

**1. A fallback that SPAWNS and then dies retried forever.** The
once-per-buffer guard bounds calls to `_attach_buffer`, not the server
those calls produce. `ensure_server` still never forwards `cfg.restart`,
so the fallback inherits `OnCrash`; an executable that exits before
`initialize` is respawned by the manager with no attempt ceiling —
silently, because `latched` has already disabled the primary's failure
poll. The fallback now gets its own one-shot die-before-initialize
watch, which retires it (ending the respawn loop) and reports.

The prior failing-fallback test used a NONEXISTENT executable, so it
only ever exercised synchronous ENOENT. To reach "spawned, then died"
the fixture has to actually spawn.

**2. Simultaneous frontends.** Both repair triggers read the ambient
`pmacs.window.buffer()`, and the daemon restores `active_frontend` to
the last-dispatched frontend before `tick_processes` — so a Lean buffer
active in ANOTHER frontend receives no `buffer.after-switch` here and
stays stale after its server is globally retired.

Fixed at the seam that is frontend-agnostic: **make consumption safe.**
`attached_for_active` now rebuilds rather than returning a record whose
server is dead, and `attachment_for_request` reports none (it must not
perturb LSP state, so it cannot rebuild). Whichever frontend runs a
command is the active one while it runs, so healing at the point of use
reaches every buffer no eager sweep can. This also closes the half where
a dead attachment was handed to a command and the request vanished.

**3. The retirement sweep stopped user-managed servers.** Selecting on
`language_id == "lean4"` also names servers the user spawned from
`init.lua`, which are not derived from `pmacs.lsp.config.lean4`. It now
keys on the `default-lean4` label `ensure_server` stamps — the
derivation discriminator.

**4. Repair ran even when no swap occurred.** `swap_to_fallback()`
returning false left `latched` true, so the next tick retried the
UNCHANGED configuration and reported it as a fallback failure. Split
into `probe.fallback_installed`: repair exists to apply a swap, so no
swap means nothing to apply.

**5. The once-per-buffer assertion counted table keys**, which cannot
distinguish "once per buffer" from "every tick for one buffer" —
cardinality stays 1 either way. Replaced with a numeric attempt counter;
the bite reports 174 attempts against the expected 1.

Five bites, each against 7c37bdc: no fallback watch -> attempt reaches
4; retire by language_id -> the user's server is stopped; gate repair on
`latched` -> a repair is attempted with no swap; drop the
once-per-buffer guard -> 174 vs 1; hand back a dead attachment -> a
command receives a `stopped` server.

Two more vacuity shapes recorded in the ledger (8 and 9): counting
distinct keys cannot bound repeated work, and a nonexistent executable
cannot reach any post-spawn failure.
2026-07-25 21:28:11 -04:00
Levi Neuwirth ab42a7991c docs: dired Stage 2 framing rev 4 — review round 3
Three design blockers, four cleanups, and the staging call taken. Round
3's theme: rev 3 named the right seams but sized two of them from a
partial inventory, and one promise was still stronger than its mechanism.
All three verified against c93f9ee.

H1 — the modified-buffer delete check races the syscall. Rev 3's
"immediately before each syscall" was wrong about where the boundary is:
pmacs.fs.remove DISPATCHES A WORKER, so the interval to remove_blocking's
remove_file is wide open, and acceptance 20 (edit before y) could never
have detected it. NARROWED to a TOCTOU-bounded pre-dispatch check, the
same honest framing G6 forced on R, rather than inventing a reservation
primitive inside a dired stage. The residue is stated precisely: the
buffer survives with its contents (that half IS robust — it runs at drain
time), the file does not. So the orphan deferral rev 3 scoped to the LSP
path now covers dired too, as one deferral rather than two. Acceptance 20
says outright that the interval has no test because it is not closed.

H2 — the LSP teardown inventory was a third of the real one. LspManager
holds FOURTEEN URI-bearing store families (lsp.rs:741-819), not five, plus
the `documents` text map didChange diffs against — a stale entry there is
a correctness problem, not a leak — plus pending_routes, whose
ResponseRoute variants CARRY THE URI at fifteen insert sites, so an
in-flight response repopulates the old key AFTER any clear. Rev 4 gives
the full table and one manager-level forget_uri(sid, uri) that purges
routes, drain-cancels the matching awaiters (the existing contract at
:799-803 already requires that wherever routes are purged), and clears all
fourteen plus documents — handling locations_store's kind key and
symbol_store's scope key specially. Modelled on the server-scoped
teardown at :1316-1331. Also records the surprise found on the way: that
teardown clears routes and documents but NOT the fourteen stores.

H3 — the diagnostic-view seam is now chosen, not either/or. Verified the
constraints: DiagnosticView.uri is private and immutable, View has no
downcast, and _attach_view takes active_window_mut() and ERRORS otherwise,
so it reaches one window and cannot drive a per-window loop from Lua; and
a remove-and-re-push loses composition order in an ordered
Vec<Box<dyn View>>. The seam: a View::rename_resource default-no-op hook,
joining overlay_identity and clone_for_split — the family #113 round 6
added for exactly this class — swept over core.windows.values_mut() the
way overlay disposal already is (mod.rs:2016-2019). In-place mutation, so
order is preserved by construction, the field stays private, and future
URI-bearing overlays opt in by overriding rather than growing a special
case. Acceptance 30 now needs TWO windows and an order assertion; a new
item 31 pins the store inventory and the in-flight repopulation.

Staging: TOOK THE FURTHER CUT as directed. Three PRs — 2a the
reconciliation transaction with no dired surface, 2b marks and operations,
2c the new fs primitives. 2a leads with the two defects it closes on main
today (an LSP-authored delete that destroys unsaved work; a workspace-edit
phantom buffer), neither of which needs dired to be worth fixing. Named
for the substrate per #161's precedent. §10 states the cost: three review
cycles, and 2a ships nothing visible.

Cleanups: item 35→40 (now 41), acceptance 27→30 and 28→32 (now 33), and
the §10 table's obsolete rename-only-Rust description, replaced by a
per-PR breakdown of what each actually carries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126d2sikA6jZpFin3rtLCSK
2026-07-25 21:22:51 -04:00
Levi Neuwirth dd581cd90c docs: frame the PTY terminate diagnostic (revision 4)
A docs-only PR (#172) failed Test (macos-latest / luajit) on
acc28_child_input_and_the_c_c_escape_work_unchanged_in_a_panel with
"kill: EPERM: Operation not permitted" raised out of terminate. A docs
diff cannot cause that, main was green at the PR's exact base, and three
other PRs passed the same job.

This framing reaches revision 4 after three review rounds, and what it
proposes is much smaller than what it started with. Revisions 1 to 3 each
proposed a tolerance rule -- treat some errno as success -- and each was
unsound in the same way: they concluded something about a process from
something that was not about that process. Revision 1 concluded from an
errno alone, which says only that a syscall failed. Revision 2 concluded
from the spawned leader while a PTY signal targets the tty's foreground
process group, which diverges from the leader exactly when job control is
in use. Revision 3 corrected EPERM but kept group-directed ESRCH, which
proves only that the selected foreground group vanished, not that the
leader exited.

So no tolerance rule lands. The disposition is preserved exactly: every
failing call still fails, with no state transition and no ledger arming.
What lands is that the failure explains itself, recording the target
source and value, the spawn-time pgid or leader pid, the errno, and the
leader's real try_wait state as five separate facts. Every candidate fix
is decidable from those together and none is decidable from the errno
alone.

Two claims are stated more narrowly than earlier revisions had them.
Consulting try_wait reaps an exited child and caches its status, so this
is not "strictly additive" -- it is "no disposition change", with an
event-count test pinning that poll_one still emits exactly one exit
event. And the test seam injects the kill attempt's result only, never
the observation, so the real ChildHandle::try_wait runs against the real
child; a stubbed observation would bypass the path under test.

Parked with their reasons: all tolerance rules, terminate becoming
idempotent for an already-reaped process (an independent fix answering a
different failure), and signal_target's read-then-kill of tcgetpgrp,
which is the most likely real fix site.

The lane closes when this lands rather than waiting for the flake to
recur; the next occurrence carries its own evidence under whoever's PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZjWMjwPXhPbt9upku9mCk
2026-07-25 21:12:51 -04:00
Levi Neuwirth 2b42204693 docs: integrate #175 and align the recovery threshold with the base
#175 (bottom-panel Stage 2 framing) landed after this branch's last head
and touches both shared docs, so the previous green run did not cover the
combination. Merged cleanly this time — no conflict.

Also fixes an inconsistency this PR introduced: the recovery check still
accepted `d152120` while the canonical-base line above declared a newer
commit. A threshold looser than the base it guards passes on a tree the
rest of the file does not describe, so the two now move together and the
text says why.
2026-07-25 21:10:16 -04:00
Levi Neuwirth 5b58e9a994 Merge remote-tracking branch 'githubsucks/main' into docs-dired-stage1-landed 2026-07-25 21:05:24 -04:00
Levi Neuwirth 4fbd47f025 docs: refresh the handoff's Stage 2 status (COHERENCE §25)
`docs/agent-handoff.md` §1 still said Stage 2 "needs its own
re-framing". It is framed, so that line would be false on `main` the
moment this branch merges.

It now records the approved shape — protocol v21, two serial slices
(2A census routing + painter extraction, then 2B wire/projection/band/
capability flip), parent acceptance 37-55 still authoritative — and
carries the census classification rule itself, since that is the fact
the ledger previously got wrong and the one a future reader is most
likely to re-derive incorrectly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 19:49:19 -04:00
Levi Neuwirth 39233aa25f Merge canonical main (#172, #157) into the docs lane
Both conflicts were competing rewrites of the same anchor lines: #172
refreshed the canonical base and the handoff header while this branch
did the same for #165. Resolved by taking main's list, which is the more
accurate of the two (it names Lean 4 Stage 2 #161 properly), refreshing
it to the current tip `ccf29e3`, and keeping this branch's note that
lanes naming an older base have not been re-based.

#172 also removed the inline-math lane, so the stale-header note drops
from three back to two and now says who owes the remaining updates.
2026-07-25 19:49:18 -04:00
Levi Neuwirth 7c37bdc514 fix(lean): repair every buffer and retire every server on fallback
Round 4 review: one P1, and it is the same defect for the FOURTH time.

`pmacs.lsp.config.lean4` is a single global entry, so swapping its
command invalidates **every** Lean buffer and **every** Lean server —
Q#LN15 gives one server per project root, so there can be several.
Rounds 1-3 each repaired one buffer and retired one server, and round 3
shipped "repair the armed target, strand the rest": status and config
said fallback while a second open Lean buffer stayed on the retired
command, and a second project root's server stayed live.

The shape that actually holds:

  * **Retire ALL `lean4` servers on latch**, not the one the probe
    happened to name. `probe.primary` identifies the server the VERDICT
    is about; it was never the set of servers the swap invalidates.
  * **Repair each buffer lazily and at most once**, when it becomes
    active — on `buffer.after-switch` and on the tick. `_attach_buffer`
    is an active-buffer-only seam, so a global swap cannot be applied to
    every open buffer at once; it has to be applied as they surface.
    lsp.lua's own `after-switch` re-pushes views but does not rebuild a
    stale attachment, so nothing else covered this.
  * The **once-per-buffer bound** is load-bearing: without it a fallback
    that also fails to spawn would retry every tick forever — the
    round-2 defect, which a naive global repair loop would reintroduce
    for every buffer instead of just one.
  * `shutting-down` is deliberately not treated as stale. It is still
    live by `server_is_live`'s reckoning, so attaching would early-return
    the stale record and burn that buffer's single attempt on a no-op.

P2: argument-inclusive attribution was implemented in round 3 but pinned
only by "contains the command name", so a mutation dropping every
argument passed. Now asserted against the exact `<command> <args>`
string.

Also fixed a vacuous assertion this refactor created: a test checked
`_probe.reattach_from == nil` for a field that no longer exists, which
reads as nil and passes for nothing. It now asserts a positive count of
recorded repair attempts.

Three bites, each against 73587b0: repair only the armed buffer -> the
second buffer stays on `lake`; retire only the named server -> one live
stale server remains; drop arguments from attribution -> the exact-string
assertion fails.

The ledger records a second durable lesson beside the vacuity one: **a
scope error repeats until the scope is named.** Four rounds of locally
correct fixes, none of which asked what the config swap invalidates.
When a change edits shared state, enumerate everything derived from it
before repairing anything.
2026-07-25 19:46:29 -04:00
Levi Neuwirth 9227860e08 docs: dired Stage 2 framing rev 3 — review round 2
Four blocking, two high, four cleanups. Round 2's real finding: rev 2
widened the rename fix into a resource transaction, and four of the
consumers it named were not actually reachable by it. All six
substantive claims verified against c8ec8f3.

G1 — acceptance 29 was unimplementable. apply_workspace_edit captures
origin as a STRING (active_buffer_path is pmacs.editor.file_path,
lsp.lua:471-473), so no transaction reaches it and the phantom survives.
The applier itself changes: capture the buffer handle, restore with
switch_buffer, and no path fallback — restoring nothing beats inventing
a file that does not exist.

G2 — the dired subscriber could not rename its own buffer. dired.lua's
module doc says there is no pmacs.buffer.set_name, which is exactly why
Stage 1 chose buffer-per-directory. Rev 3 adds the setter (Q#DR21):
Buffer::set_name already exists and already documents itself as for
"rename operations", §5 needs it anyway for the Buffer.name half, and the
alternative — kill/recreate plus window replacement — loses placement,
cursor, intercept, round-trip input, and mode.

G3 — rec.uri was not the last LSP owner. DiagnosticView captures its URI
at construction and its own field doc anticipates this ("M5 may add
re-rooting if a buffer is renamed", diag.rs:455-457); five more stores
are URI-keyed. §5 now carries the ordered contract: flush pending
didChange, didClose, drop all five stores, re-run ensure_server, didOpen,
re-root the view per window.

G4 — Q#DR18 had no seam and was racy across the prompt. apply_resource_op
kills via find_by_path: raw path, first match, no descendants, no
modified check — it destroys unsaved work today. Rev 3 defines one shared
reconcile_delete called by both paths, harvests remove in the drain like
rename (so fire-and-forget reconciles too), and rechecks modified state
immediately before each syscall, since another frontend can edit while
the prompt is open. The policy stays asymmetric on purpose: dired refuses
the entry, an LSP-authored delete still removes the file but no longer
destroys the buffer.

G5 — w had no surface and the wrong semantics. push_entry is local and
copy() requires a region. Adds pmacs.killring.push (Q#DR22) with copy()'s
own semantics including breaking the kill chain, and makes w SET-BASED:
the parent approved the binding and Emacs copies marked filenames, so
rev 2's point-only narrowing was an unapproved change of its own. R is
now the only point-based operation.

G6 — R's no-clobber was only a preflight. rename_blocking calls plain
std::fs::rename, which silently replaces. The claim is narrowed to a
TOCTOU-bounded preflight refusal, acceptance 12 reworded to promise only
that, and a no-replace primitive named as deferred.

G7 — lsp_multi_root added to the gates, the §13/§7 slips fixed, and the
"2a's only Rust is the rename rebind" line corrected: it is now a rename
and delete reconciliation, two hooks, two new public surfaces, an LSP
teardown contract, and an applier change. §10 says so, and names the
further cut if that is now too large for one PR.

Acceptance renumbered flat (46 items) and the bite obligations are now a
table of eleven item/mutation pairs, three of them round-2 additions
where rev 2's design would have passed a weaker test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126d2sikA6jZpFin3rtLCSK
2026-07-25 19:41:08 -04:00
Levi Neuwirth 49757e51a8 docs: bottom-panel Stage 2 framing (revision 4)
Closes review round 3 — 1 blocking, 1 high, 1 medium.

**R3-1 (blocker) — the call-site table contradicted the source.** The
three-boundary model was right; five rows of its classification were
not, and each was a real defect:

- `:6140` is `completion_dropdown_layout` — DOCUMENT completion
  placement, deriving the space below the anchor line. Classified
  status-owned, it would let completion overlap the panel.
- `:7195` and `:7212` are the `status_buffer` / `status_left_buffer`
  `TextBounds.top` — status text bounds, classified document-owned.
- `:7351` clips global minibuffer CANDIDATE glyphs to the dropdown's
  band anchor; classified document-owned, they would be clipped
  against a boundary the dropdown does not sit above.
- `:8561` (`edge_scroll_direction`, document edge scrolling) was
  missing entirely, leaving it tied to the old bottom.
- `:8077` is `code_caret_rect_in_clip` — caret clipping, not
  completion placement. Its class was right, its label wrong.

Every production site is now individually verified against the source
and tabulated with what it actually is. The census is stated as
arithmetic a reader can check: 29 matches = 20 production + 1
definition + 8 test sites.

Root cause recorded in the revision history: rev 3's table was built
from a `grep | head -20` over 29 matches, which is precisely why
`:8561` vanished. The minibuffer's status-owned status is now argued
from Q#BP14b rather than assumed — it is global, bufferless chrome
anchored to the status band, so all four of its sites stay with the
band.

**R3-2 (high) — clamps preserved.** The three equations permitted
negative coordinates on a surface shorter than its chrome, where
today's `text_area_bottom` clamps with `.max(0.0)`. All three now
clamp at zero, which keeps the "exact formula" exact exactly where it
matters most.

**R3-3 (medium) — attachment rejection classified SHARED.**
`validate_cells` also rejects `cell.attachment.is_some()`
(`terminal.rs:305`), whose error text reads "which terminals never
use" (`:190-191`) — phrased as a terminal-specific fact, which is why
rev 3's "exact split" missed it. Panels implement no attachment
rendering in Stage 2, so a `PanelFrame` carrying one describes a
surface the GPU would silently not draw; shared rejection fails closed
on the producer side instead. The message is reworded grid-neutral
when it moves, and giving panels attachment rendering later moves the
rejection back deliberately rather than by default.

A2B-4 now names the counts on both sides (twelve document-owned move,
eight status-owned do not) and carries the three symptom-bearing rows
that a plausible misclassification produces. §9 records that the GPU
three-boundary split belongs to 2B, not 2A — it is only observable
once a band can be installed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 19:36:58 -04:00
Levi Neuwirth e1db5bb392 Merge remote-tracking branch 'githubsucks/main' into terminal-config 2026-07-25 19:32:25 -04:00
Levi Neuwirth 73587b0e37 fix(lean): correlate the probe verdict with its own server and buffer
Round 3 review: two P1 asynchronous-correlation defects, with the
focused suite at 25/25 while both were live.

**1. A late version verdict retired nothing and claimed success.**
`probe.watching` is cleared the moment the server initializes — it is
failure-polling state. A slow `lake --version` landing after a
successful initialize therefore reached `fire_latch(nil)`, which retires
nothing: `_attach_buffer` found the still-live primary attachment,
early-returned it, and the retry counted that as done. Status said
"falling back", the config named the fallback, and the buffer stayed on
the old server.

**That is the round-1 silent no-op arriving through a third event
ordering** — first as "no re-attach at all", then as "re-attach cleared
by an unrelated buffer", now as "re-attach satisfied by the server we
were supposed to replace". The fix separates the two facts that were
being carried by one field: `probe.primary` is the server the verdict
applies to and survives initialization; `probe.watching` is the
failure poll and is cleared by it.

The existing fixture could not reach this ordering at all — its `serve`
sleeps, so the primary can never initialize before `--version` returns.
The new one execs the fake LSP for `serve` and delays 0.6s before
reporting 3.0.0.

**2. `buf_key` was the most recently loaded Lean buffer.** Written on
every Lean `buffer.after-load`, so a second Lean file opened before the
verdict became the rebuild target while the latch still watched the
FIRST buffer's server. Target buffer and primary server are one fact and
are now armed together, exactly once. Both files in the new test share a
package, so mis-targeting shows up as a stranded buffer rather than as
two unrelated servers.

**3. The failure message hardcoded `lake serve`** after the latch became
command-agnostic, telling a user whose `my-lean-wrapper` failed to go
debug lake. `configured_command()` names what is actually configured,
arguments included.

**4. The ledger** now records all fifteen bites across the three rounds,
both prior review rounds' findings (the round-2 block was lost when an
earlier edit script aborted before writing), and the durable lesson.

That lesson, recorded for the handoff: **six tests across three rounds
were written, ran green, and pinned nothing** — caught only by biting.
The shapes are enumerated in the ledger; the rule is that a test is not
evidence until the mutation it targets has been shown to fail it. Two
of the six are subtle enough to be worth naming here: a bite that
RAISES is swallowed by the hook's pcall and "passes" for the wrong
reason, and a fixture whose `serve` sleeps cannot reach any ordering
where the primary comes up first.
2026-07-25 19:23:50 -04:00
Levi Neuwirth 857a86ff59 docs: bottom-panel Stage 2 framing (revision 3)
Closes review round 2 — 1 blocking, 2 high, 1 medium — decides both
remaining open items, and re-integrates canonical `main` @ `ccf29e3`
(#172 + #157; documentation plus one `src/buffer.rs` regression test,
no protocol or Stage 2 source anchor moved).

**R2-1 (blocker) — the seam is three boundaries, not one.** Rev 2 asked
for a single document-bottom accessor. That is wrong: once a panel is
installed the present single value must DIVERGE, because several of its
consumers must not move at all. `text_area_bottom`
(`pmacs-gpu/src/main.rs:8490`) is today `status_band_top`,
`geometry_capacity_bottom`, and `document_text_bottom` at once. Rev 3
defines all three, classifies every one of its ~19 call sites as
status-owned / document-owned / geometry, and records that four sites
rev 2 named (`:3175`, `:3185`, `:6601`, `:6607`) consume a status-band
HEIGHT and no bottom coordinate at all, while the status background
`:5908` and status text `:7134`/`:7922` must stay at the physical
window bottom.

The acceptance is now a contrast assertion: installing a panel moves
every document-owned consumer WHILE the status band stays
pixel-identical. "Everything moved" alone is passed by a blanket
rewrite of the helper, which is exactly the wrong implementation.

**R2-2 (high) — epoch exactness.** `accept_frame_geometry` returns
`Advanced | Duplicate | Rejected` instead of a boolean that cannot
separate reconcile-needed from already-current from stale; if a boolean
is ever kept internally it must be named `advanced`, since `Duplicate`
is also accepted. Rev 2's exhaustion wording permitted retaining stale
geometry, which is not fail-closed — a real resize after exhaustion
would keep painting a panel sized to disowned geometry. The grid path
now clears `frame_geometry` to unknown and reconciles hidden, and the
frontend takes a terminal latch so a retained matching `Present` cannot
resurrect the band; only a fresh session clears it.

**R2-3 (high) — parent acceptance 52 splits.** 2A has no semantic panel
projection, so it can only prove the extracted painter honors an
explicit `None` map plus the `src/window.rs:562` comment fix. The real
contract is production-reachable only in 2B and is reasserted there
beside 42/43/44.

**R2-4 (medium) — touched gates named**: `statusline_segments_acceptance`,
`m11_5_semantic_acceptance`, `gpu_initial_target_acceptance`,
`gpu_font_acceptance`, beside the vterm, folding, and GPU suites.

Open items decided: `BASE_DIVIDER_HEIGHT = 4.0` at scale 1.0, scaled by
`FontMetrics::scale`, whole strip painted `ui.divider` and used as the
exact hover/drag hit rect; `TEXT_TOP` stays `16.0` unscaled, with
Q#BP15a's "all quantities use the frontend's current scale" narrowed to
font-derived metrics and the divider. Wholesale surface-inset/DPI
scaling is recorded as separate work, not smuggled in.

The ledger's bottom-panel lane keeps its census correction and gains
the three-boundary one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 19:23:49 -04:00
Levi Neuwirth 69f9a1bdf9 docs: bottom-panel Stage 2 framing (revision 2)
Closes review round 1 — 2 blocking, 3 high, 3 revision points — and
rebases the ground truth onto `main` @ `d152120`.

Both blockers were rev 1 asserting something the parent framing
already decided otherwise:

- **R1-1.** Rev 1 said all 23 census reads route through
  `primary_document_window`. Q#BP14 routes only the **Projection**
  class that way; focus/input (#13-#15, #23), focus chrome and
  surface-routed (#16-#19), and focus/session (#20) keep their own
  authorities. Rev 1's rule would have broken remote-op validation and
  application, `DispatchIdle`, presence, focused search/menu/completion
  routing, and terminal bell ownership. §3.2 restores the four classes
  as a table and the acceptance asserts each separately — the
  focus-class assertions are the load-bearing half, since a test that
  only proves "the document is used" passes with them wrongly
  rerouted.
- **R1-2.** The three `src/statusline.rs` active reads have two
  dispositions, not one. Only `:644` selects the wrong window; `:629`
  and `:675` must keep tracking actual focus, because grid contexts
  need a truthful `active`, revalidation must notice a focus change,
  and parent acceptance 42 requires a document provider to be able to
  observe `active = false` while the panel is focused.

The three high findings:

- Q#BP2S1 resolves to frontend-owned epochs (option 1) — a font or
  scale transaction can need to invalidate an old `PanelFrame` while
  the derived `CellSize` is identical, which daemon value dedup cannot
  detect. Rev 2 adds the four-row transition table, splits grid
  allocation from semantic acceptance into two APIs rather than one
  ambiguous method, moves the grid allocator off `saturating_add` to
  checked-with-fail-closed, and defines the initial epoch and both
  exhaustion behaviors. Rev 1's "rejects a lower-or-equal epoch
  carrying different data" was itself wrong: a lower epoch carrying
  identical data is still stale.
- The `panel_capable` flip is narrowed to an authenticated semantic
  session negotiated at **v21 or later**. Denying a v20 peer the new
  events is insufficient if the daemon still places its window in a
  side panel it cannot render — the gate is on placement.
- Parent acceptance criteria 37-55 are declared authoritative and
  mapped to slices 2A/2B, with rev 1's eleven drafts demoted to
  refinements. The painter-extraction criterion now pins cells, the
  returned cursor, the focused window's `view_top` mutation, and
  passive-window state.

All four scout obligations are closed (§5), and the pixel formula is
treated as contract work, not implementation detail:

- The shared/terminal-only validator boundary is named exactly.
- Four new outbox tail-coalescing tags beside the existing four.
- **`State::mono_advance` is unsafe to adopt**: absent a `FontFacts`
  probe it samples the document's first shaped glyph, which would make
  panel columns document-dependent. The declaration uses the existing
  stable normal-face `probe_mono_advance` instead, and declares zero
  usable geometry when it returns `None`.
- `BASE_DIVIDER_HEIGHT` does not exist. Rev 2 decides its scaling and
  requires **one** document-bottom accessor routing every consumer
  (caret, hits, minimap, terminal geometry, clipping, edge scrolling)
  — a second unrouted seam is precisely the Stage 1 `Layout::compute`
  two-caller defect. The concrete base value is left open for round 2.

Also: the coherence statement now names journey steps 7-10 instead of
claiming none, and drops rev 1's overclaim that this advances
background-work visibility — a panel gives output a placement but adds
no join key to COHERENCE §9's four disjoint activity planes.

The ledger's bottom-panel lane is updated from "no branch and no
framing yet" to the framing's real state, and carries an explicit
correction: that entry was itself the source of rev 1's census
mis-statement.

Factual corrections: `InitialTargetResult` is at `message.rs:1145`;
`primary_document_window` has four references and two production paths
(`daemon.rs:1639`, and `daemon.rs:2998` via `primary_document_buffer`,
which is census #22); fifteen PRs merged since the parent's last
re-scout, not eleven.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 19:13:44 -04:00
Levi Neuwirth c2eeb60dc7 docs: bottom-panel Stage 2 framing (revision 1)
The re-framing `docs/bottom-panel-framing.md` rev 4 §2 requires before
Stage 2 (the GPU panel band) is implemented. Re-scouted against
canonical `main` @ `5aa9044`, protocol v20.

It does not restate the parent's decisions; it records what the
re-scout found. Every source anchor Stage 2 inherits had moved, but
none of the parent's mechanical model was falsified. Two facts held
and are load-bearing: protocol is still v20, so Q#BP9 resolves to
**v21** with no reservation needed, and both byte pins
(`InstanceMessage::InitialTargetResult`,
`FrontendEvent::TerminalPointer`) are still their enums' final
variants.

Four findings:

- **Q#BP2S1, new and open.** Stage 1 landed a daemon-side geometry
  epoch allocator (`declare_frame_geometry`), but Q#BP15a specifies a
  frontend-owned epoch echoed by every `Present`. The landed allocator
  also dedups on value and uses `saturating_add`, which is neither
  wrapping nor the fail-closed the framing asks for. Three resolutions
  are stated with a recommendation.
- **The §1.3 census is essentially unrouted.** Stage 1 built the
  `primary_document_window` seam but it has one production caller;
  ~80 direct `.active` reads remain. This is Stage 2's bulk, not its
  tidy-up, and the stage plan sequences it first.
- **The statusline active read is three sites, not one.**
- **Four scout obligations are still open** and are named rather than
  papered over, including the GPU-side pixel formula inputs.

Also carries the staged plan, draft acceptance criteria, the coherence
impact per `COHERENCE.md` §20 (§14 is the section it serves), and four
questions for the user.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 19:13:44 -04:00
Levi Neuwirth 04c5ad13e8 docs: record the terminal-config lane
Stage 1 of the terminal config/copy-mode arc is in review; Stage 2 is
not started. Records the four decisions forced by scouted ground truth,
the four bites against four different wrong implementations, the two
reusable test instruments, and the gate results.
2026-07-25 18:41:21 -04:00
Levi Neuwirth 3377db070a fix(lean): correct the server lifecycle; round 2 review
Three P1 lifecycle defects and two P2s. The focused suite was 20/20 with
every one of them live, which is the part worth keeping.

**1. The crashed primary respawned forever underneath the fallback.**
Round 2 skipped the retire call for terminal servers to avoid corrupting
them — but the crash had already armed `next_restart_at`, and
`maybe_restart` fires on every elapsed backoff with no attempt ceiling.
The broken command kept respawning under the live fallback.

The right call depends on the state, and each is wrong for the other:
`forget` REQUIRES a terminal state and removes the client outright,
which also drops the restart timer; `stop` is for a live one and
corrupts a terminal one (its not-initialized branch parks it in
`ShuttingDown` forever). `retire_server` now dispatches on state.

**2. Re-attachment targeted whatever buffer was active when the
asynchronous verdict landed.** `_attach_buffer` is an active-buffer-only
seam, and "some attachment now names a different server" is satisfied by
an unrelated Rust buffer — clearing the retry and leaving the Lean buffer
stale forever. The initiating buffer is now captured and the retry waits
for it.

**3. A failing fallback retried every tick forever, silently**,
contradicting acceptance 27's promise that a second failure surfaces.
"Waiting for the old server to go" and "attempting the replacement" are
now separate: once the old one is terminal or gone, the replacement is
attempted EXACTLY once, and a spawn failure is reported.

**4. The Lake version parser was being applied to arbitrary wrappers.**
`version_below_3_1` encodes lake's output contract; a working
`my-lean-wrapper` reporting "wrapper 1.0" would have been replaced
despite its server initializing fine. The version probe is now gated on
the command's basename being `lake`. The FAILURE latch stays
command-agnostic — that one keys on the server actually not starting,
which is true of any command.

**5. An unconfigured Lean server was reported as a failure** and latched,
poisoning the session so a later configuration could never take effect.
Absent config or command now means disabled; only a configured command
that produced no attachment is a failure.

**6. The ledger recorded pre-fix counts** after the fixes were pushed.
Now 25/25 and 3,214. That is the #161 fmt-blocker error in a slower
form: verification must describe the pushed tree.

Sign-offs requested in review: `M.fallback` is now `M._fallback`, an
underscored test seam, and its idempotence check compares args as well as
command — the same command with different arguments is not "already
applied". Dropping the `command ~= "lake"` guard stands for the failure
latch only.

Five regression tests added, and **three of them were too weak on first
write; only bite-testing found it**:
  * asserting "no live non-fallback server" misses a respawn loop,
    because a respawning server sits in `crashed` most of the time —
    `attempt` is the observable that counts respawns;
  * returning to a buffer with `find_or_open` re-fires
    `buffer.after-load`, which repairs the attachment regardless of the
    code under test — `switch_buffer` is the honest return;
  * a MISSING command fails synchronously inside `after-load` where the
    rebuild happens inline, so the async race cannot occur — only the
    probe path exercises it.
Each of the five now fails against the exact round-2 mutation it targets.
2026-07-25 18:35:11 -04:00
Levi Neuwirth eec731b8f4 docs: dired Stage 2 framing rev 2 — review round 1
Seven findings, two blocking. Every checkable claim was verified against
c8ec8f3 before being acted on; all seven held.

F1 (blocking) — the rename contract reached one path owner. Verified the
other four: Buffer::set_name documents itself as for "rename operations"
and set_buffer_path never calls it; rec.uri is cached per LSP attachment
and read at ~20 sites; dired's buffers are PATHLESS so no buffer-keyed
rebind can reach them; and the workspace-edit origin restore does not
fail gracefully — find_or_open on a renamed-away path hits
resolve_target_buffer's NotFound arm, which creates an empty path-backed
buffer, so it materializes a phantom at the obsolete path and selects it.

Rev 2 replaces the rebind with EditorCore::reconcile_rename — whole
registry, equality-or-path-component prefix, updates file_path AND name,
called by BOTH the async drain and apply_resource_op so the two cannot
drift — plus a new resource.renamed(old, new) hook so path-keyed Lua
consumers reconcile. lsp.lua recomputes rec.uri, issues didClose/didOpen,
and re-runs ensure_server because #161 keys affinity on project root, so
a cross-root move needs a different server. dired.lua follows its
handles. Verified the ordering the design needs already holds:
_async.tick calls _tick() before resuming any coroutine.

F2 (blocking) — deletion of visited paths had no policy. New §6 decides
all four cases. An unmodified visited buffer is killed; a MODIFIED one
refuses that entry, deliberately diverging from Emacs, because an
orphaned buffer is indistinguishable from a normal one and the next
C-x C-s silently resurrects the file. The check runs before the confirm
so the prompt states the skip. Adds a symmetric resource.deleted hook.

F3 (high) — the key table silently changed approved scope. The parent
lists `w` and contains no `M`. Restored `w` (Q#DR20); `M` is now an
explicit new-scope decision (Q#DR19) that REFUSES symlinks, since the
parent already ruled that the fixture's symlink-perms rejection "carries
over unchanged" and rev 1's warn-after-the-fact contradicted it.

F4 (high) — Q#DR13 contradicted the R contract. Narrowed to three
classes: set-based (D, M, C), flag-based (x), point-based (R, w).

F5 (high) — five falsifying acceptance items added, and the bite matrix
now names six mutations including "dispatch-all-then-await" and "add a
completion source to confirm".

F6 (medium) — take_settled_renames was underspecified. Took the
reviewer's preferred shape: tick returns a structured TickOutcome so
settle identity and rename metadata stay in one transaction.

F7 (2b) — defined the full C command flow with an up-front collision
scan and one confirm (declining copies the non-colliding entries), pinned
remove_dir_all's lstat safety at the primitive, and stated
dired.recursive-deletes as boolean/default false.

Not done, and said so: R is not widened to the marked set. Multi-file
rename needs a target-directory concept that does not exist, so R stays
point-based and is NAMED as a class rather than left an unstated
exception.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126d2sikA6jZpFin3rtLCSK
2026-07-25 18:31:11 -04:00
Levi Neuwirth 7247f647c0 Merge canonical main (#158, #156) into the docs lane
Both conflicts were docs-only and resolved as unions, with one repair
taken from main: #156 fixed a pre-existing corrupted duplicate of the
"GPU initial target LANDED — #148" bullet in the handoff, whose tail ran
into the protocol-version text. This branch still carried the broken
copy, so the resolution keeps main's repaired `- Protocol **v20**` bullet
and drops the stub, along with main's now-superseded "Stage 1 IN REVIEW
as PR #165" sub-bullet.

Refreshed the canonical base to `d152120` and widened the stale-header
note from two lanes to three: #158 merged but its lane still reads
"PR #158 OPEN".
2026-07-25 18:22:07 -04:00
Levi Neuwirth adf10cc9c3 docs: record the inline-math slice (#158) as landed
Brings the three required docs current after #158 merged, and discharges
the follow-up that framing named for itself.

COHERENCE.md section 16 audits the claim that the GPU frontend exceeds
the TUI "under real divergence pressure" without a privileged frontend
emerging. Inline math is the sharpest instance of that so far -- the GPU
typesets $...$ while the TUI shows LaTeX source, and the TUI fallback is
a named deferral. Section 25 makes that update ride the PR, so the
enumerated list gains the case along with what keeps it inside the rule:
the slice reserves no protocol version and adds no wire surface, so the
divergence is presentational and both frontends read the same model.

docs/inline-math-framing.md carried a licence error the slice framing
flagged in its own section 9 and deliberately did not fix in-branch,
since the parent is a merged document. Latin Modern Math is under the
GUST Font License, not the OFL; the row now says so and records the
~717 KiB bundled size.

docs/agent-handoff.md records the landing and re-anchors section 1 to
d152120. The bullet leads with the facts a fresh agent would otherwise
have to rediscover: the whole slice lives in pmacs-gpu because pmacs-gpu
depends only on pmacs-protocol and never on pmacs; the v0 subset is 34
Greek symbols, sub/superscript and \frac; an unsupported command fails
the WHOLE span back to source, so most inline spans in a real paper
still show LaTeX by design; and math is suppressed while the caret is
inside its span.

docs/active-work.md removes the merged lane per its own update protocol
and adds a Closed entry. Four things there are reusable beyond this arc:
a stale frontend binary is invisible from the source tree, so diagnose
with strings on the binary rather than by re-reading a checkout that is
already current; the dangerous integration was the one that did NOT
conflict, so decide from the shared-file set rather than from whether
git complained; integration is proved by predicting the other side's
test-count delta and checking it; and m4_5_basedpyright has no timeout,
hangs forever, and is intermittent, so an earlier clean sweep proves
nothing. It also corrects a claim I recorded on main: the branch's
missing CI was not an unidentified cause -- a conflicting PR builds no
merge ref, so no pull_request run is created.

Docs only; no code changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZjWMjwPXhPbt9upku9mCk
2026-07-25 18:16:31 -04:00
Levi Neuwirth cdaea66203 fix(lean): make the fallback actually produce a working server
Round 1 review, four P1s. All real; the first two mean the fallback did
not work at all.

**1. The latch swapped the config but never spawned or re-attached.**
Nothing re-fires an attach on a config change and `attach_buffer`
early-returns for a live attachment, so the buffer stayed bound to the
server that had just been stopped. The user got a config edit and no
language server. `fire_latch` now rebuilds through a new
`pmacs.lsp._attach_buffer` export.

Two mechanics had to be right for that rebuild to happen at all:

  * It is **retried on the tick**, because `pmacs.lsp.stop` leaves the
    state `shutting-down`, which `server_is_live` counts as LIVE — an
    inline re-attach early-returns the stale record and the swap is a
    silent no-op.
  * The latch **does not stop an already-terminal server**, and this is
    a substrate bug worked around rather than a style choice.
    `LspManager::stop` on a `Crashed` client takes its not-initialized
    branch, terminates the dead process, and sets `ShuttingDown { ..
    None }` on the premise that "the next exit observation cleans up" —
    but the exit already happened, which is what made it `Crashed`. No
    further event arrives, so the client is stuck in `ShuttingDown`
    forever: `server_is_live` reads it as live so `attach_buffer` never
    rebuilds, and `forget` refuses it for not being terminal. Stopping a
    dead server is what makes it un-replaceable. Named in framing §6; the
    fix belongs in `stop` and changes behavior for every language.

**2. A missing `lake` bypassed probe and latch entirely** — the single
most likely real failure. `ensure_server` swallows a synchronous ENOENT
and returns nil, so there was no attachment, and the hook keyed on
`active_attachment()` returned before arming anything. The hook now keys
on the buffer's LANGUAGE and treats a Lean buffer with no attachment as
the failure itself.

**3. `waitForDiagnostics` omitted `version`.** Lean's
`WaitForDiagnosticsParams` is `{ uri, version }` (v4.9.0,
`src/Lean/Data/Lsp/Extra.lean`); the request is how a client says which
revision it wants. It looked correct only because the fake server echoes
any payload — so the fake server now validates and returns InvalidParams
without it.

**4. The ledger stated the dangerous stacking order** in one sentence
and the correct rule in the next. Fixed to say BEFORE. A safety rule
written twice with opposite senses is worse than not written.

Also (P2): the probe/latch suite now drives the production path —
`buffer.after-load` -> ticks -> probe drain -> latch -> re-attach — with
real executable stubs, and asserts the originally opened buffer ends up
on a LIVE server. Round 1's acceptance 36 asserted every server was
terminal, i.e. pinned the ABSENCE of the fallback it claimed to test.
`M.fallback` is a table so the suite can point it at a working stand-in;
the probe now spawns `cfg.command --version` rather than a hardcoded
`lake`, which is also more correct for a user who configured a wrapper.

`swap_to_fallback`'s `command ~= "lake"` guard is gone: the latch fires
only when the configured server actually failed, one visible fallback
beats no server, and `probe.latched` is what keeps it to exactly one.

Three new bites, all against the committed tree: no re-attach after the
swap -> three latch tests fail; hook keyed on the attachment -> the
missing-`lake` case fails; `waitForDiagnostics` without `version` ->
acc37 fails with the server's InvalidParams.
2026-07-25 18:03:29 -04:00
Levi Neuwirth 05984f1b1b docs: frame terminal configuration and copy mode
Two stages, one arc, no protocol change. Stage 1 makes the terminal
configurable (profiles, scrollback, escape key) and binds the opening
command; Stage 2 adds copy mode and search over scrollback. They are
independently releasable and get separate branches and PRs.

Three scouted facts shaped the design, two of them ruling out the
obvious plan.

Profiles cannot be a config-registry setting: ConfigValue is four
scalars and there is no table kind, so profiles join pmacs.lsp.config
and pmacs.pair.sets as a raw Lua table while the registry holds only
scalars.

Search cannot reuse isearch in place: SearchStore addresses matches as
byte ranges into a buffer's rope, and a terminal identity buffer is
empty by construction.

An in-place copy mode would be the seventh dispatch shadow, which
COHERENCE section 6 grades weak and growing by one island per modal
feature, with no transient-keymap mechanism to migrate to.

Copy mode therefore materializes the retained rows into an ordinary
read-only buffer. isearch, motion, selection and the kill ring work
with no new substrate; the "keys must not reach the child" problem
dissolves because the snapshot is not a terminal; and describe-key
stays truthful because the bindings are buffer-local. The cost, stated
in the doc, is that the snapshot is point-in-time rather than a live
freeze.

Four review rounds produced the load-bearing parts: the escape-key
cache is owned by TerminalSession so its lifecycle is the terminal's,
with three acceptance pins that each fail a different wrong cache; the
snapshot needs set_round_trip_input because a Lua intercept does not
set Buffer::read_only and an optimistic CrdtOp would mutate both the
daemon buffer and the mirror; the double-escape must encode the
configured chord rather than a hardcoded ETX; and the two open-time
settings resolve through the global chain because they are read before
the terminal buffer exists.

No code changes in this commit.
2026-07-25 18:02:35 -04:00
Levi Neuwirth d152120ff8
Merge pull request #156 from levineuwirth/docs-bottom-panel-landed
docs: record bottom-panel Stage 1 (#155) landing
2026-07-25 22:00:02 +00:00
Levi Neuwirth dea1d20e4e docs: refresh the canonical base for #158
The inline-math slice landed while this PR was open. Its own merge
removed its ledger lane, so the stale-header note above still names
exactly two; only the base anchor needed moving.
2026-07-25 17:58:00 -04:00
Levi Neuwirth c2dfe1a89d docs: framing for dired Stage 2 (marks and operations)
Continues docs/dired-framing.md, whose §§6-7 carry the approved shape of
marks and operations. This re-verifies every claim in them against
main @ c8ec8f3 — Stage 1 changed three of the files Stage 2 leans on
most — and adds what the parent did not decide: the batch-execution
contract, the confirmation surface, the staging cut, and acceptance.
Decisions continue the Q#DR scheme from Q#DR12.

Five corrections to the parent, one load-bearing:

- The rename rebind belongs in the drain (the parent's decision, kept)
  but CANNOT be implemented in `AsyncRuntime::tick`: AsyncRuntime has no
  buffer registry and no core. The seam is `pmacs._async._tick`, one
  layer up, which already has `lua` in scope. AsyncRuntime harvests, the
  binding rebinds.
- Line references drifted (tick 991 -> 1003, the FsUnit arm 1022 ->
  1046, apply_resource_op's raw lookup 3248 -> 3249).
- The frozen fixture has NO mark-and-operate layer — eight commands, two
  keys, and its "marks" are wdired text-position marks. So Stage 2 has
  no in-repo reference implementation, which the parent's "45 tests pin
  dired/wdired behavior" reads as implying it does.
- No y_or_n exists anywhere, and there is no runtime minibuffer.lua at
  all — pmacs.minibuffer is Rust-only.
- `remove_blocking` already deletes files AND empty directories, so
  `remove_dir_all` is needed only for non-empty ones. This is what makes
  the staging cut possible.

Also carries a verified pre-existing defect, confirmed by probe rather
than inferred: a fire-and-forget non-stream job leaks its pending entry
forever (only stream eviction and take_result remove entries, and the
Lua handle has no __gc). Named as a deferral. The same probe establishes
that a settled job IS still readable at drain time, which is what makes
the rebind design sound.

Recommends splitting Stage 2 at the "needs a new Rust primitive" line:
2a is the mark layer plus d/x/D/R/M on the five ops that already exist,
plus the rename correctness fix; 2b adds mkdir/copy/remove_dir_all and
+/C and recursive delete. The reasoning is that 2a's only Rust is the
rename rebind, whose design is subtle enough to deserve a reviewer's
whole attention.

Coherence impact per COHERENCE.md §20 is stated in §0.5, including the
honest part: Stage 2 must add a `rename_paths` field to `PendingJob`,
which is a one-off where §9 wants a general owner/purpose — though a
side map, the alternative, is worse by §9's own diagnosis of the
parse-job link. 2b grows the closed JobKind enum 12 -> 15.

Touches only this file: docs/active-work.md and docs/agent-handoff.md
are held by the open docs PR #169.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126d2sikA6jZpFin3rtLCSK
2026-07-25 17:55:14 -04:00
Levi Neuwirth 5aa9044686
Merge pull request #158 from levineuwirth/inline-math-slice
Inline math: the first vertical slice (detect → parse → layout → draw)
2026-07-25 21:49:50 +00:00
Levi Neuwirth a72ce72349 Merge canonical main into the bottom-panel landed-doc lane
`main` moved through #158-#166 (Lean 4 Stage 2, COHERENCE.md, find-file,
the dired framing and Stage 1, the GPU terminal input fix) while this
documentation branch waited. Both required docs conflicted; neither
conflict was a code signal.

Resolution:

- `docs/active-work.md`: main's ledger is the base — every lane it has
  gained since this branch was cut is kept verbatim. Only the
  bottom-panel lane is replaced with this branch's "Stage 1 MERGED;
  Stage 2 (GPU band) is next" section, and only the bottom-panel entry
  is added to "Closed since the last snapshot".
- `docs/agent-handoff.md`: main's version is the base. This branch's §1
  bottom-panel bullet, its §1 roadmap Arc 7 entry (which also records
  that DAP is now unblocked), and its four §5 ops lessons are inserted
  at their anchors.

One repair rides along. Main's `docs/agent-handoff.md` carried a
garbled fragment at §1: a duplicated, truncated "GPU initial target
LANDED — #148" bullet whose body was the tail of the old head-of-`main`
anchor bullet, leaving the `SUPPORTED=[6..=20]` protocol enumeration
orphaned mid-sentence. The fragment is removed and the enumeration is
restored as its own bullet.

No code changes; the merged tree's non-doc content is main's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 17:47:51 -04:00
Levi Neuwirth 914bf3f02f docs: record the Stage 3b lane and its stacking constraint 2026-07-25 17:40:31 -04:00
Levi Neuwirth ea0632412e docs: record dired Stage 1 (#165) as landed
#165's own commits could not update the handoff snapshot to name the
merge that contains them, so the protocol obligation lands here.

- `docs/agent-handoff.md`: absorb the dired lane into §1, replacing the
  placeholder that promised exactly this. The bullet carries Stage 1's
  durable substrate facts — why the tolerant `read_dir` had to be Rust,
  why exposing the core normalizer beat mirroring it in Lua, the
  fixed-width `_layout` contract Stage 3 reads offsets from, the
  ambient-action buffer guard, treating a failure as the answer instead
  of probing, the per-entry error cap, the first mode-scoped keymap and
  the pre-existing test it broke, and the dedication a descent does not
  carry. Refresh the head-of-`main` anchor and the last-updated line.
- `docs/agent-handoff.md` §5: two ops lessons that cost real time. A fix
  must be committed before it is bitten, because `scripts/bite` restores
  by `git checkout --` and reverts to HEAD; a CONFLICTING PR runs no CI
  at all, because `pull_request` workflows build a merge ref GitHub does
  not create while the branch conflicts, and nothing reports the absence.
- `docs/active-work.md`: remove the merged lane per update-protocol rule
  4 and summarize it under "Closed since the last snapshot", keeping the
  two forward items Stage 2 needs (the rename rebind is first-match-only
  over a raw path, and Q#DR5's seam is the main-thread drain). Refresh
  the canonical base. Flag the two lane headers that still call a merged
  PR "IN REVIEW" — #161 and #166 — rather than editing lanes another
  thread owns.
- `COHERENCE.md`: #165 is no longer a PR. Per §25 the audited claims this
  work changed were updated when it landed; this corrects their tense in
  seven places and the two prose lines that still asserted dired was in
  flight.
- `docs/dired-framing.md`: status line to MERGED, and state plainly that
  Stages 2 and 3 each still need their own framing.

Docs only; no code, no gate-relevant change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126d2sikA6jZpFin3rtLCSK
2026-07-25 17:39:27 -04:00
Levi Neuwirth 6ad9f7e788 docs: record the third integration and the basedpyright sweep hang
Third merge of main into the lane, at b889873 (GPU terminal input #166).
Unlike the first two this one produced NO conflict -- and it is the case
that shows why a clean git merge-tree is not a reason to skip
integrating. #166 lands 41 lines in pmacs-gpu/src/main.rs, the same
heavily-rewritten file as the first integration; the two edits merged
silently only because they sit in different regions of it (#166 is
entirely in the headless probe, this lane rewrites the render path).
Merging the PR on that clean auto-merge would have shipped a combination
no gate had run.

Reconciliation, run against what #166 actually added rather than against
a pass/fail: it adds 3 library tests, 2 to vterm_stage3_acceptance, and
0 to pmacs-gpu. Predicted lib 1,826 -> 1,829, CRDT 2,003 -> 2,006, GPU
unchanged at 202; that is exactly what ran. Suite count 91 -> 92 is
#161's new lsp_multi_root_acceptance binary. All three sides' markers
verified live in the shared file.

Also records an ops trap that cost hours this session:
m4_5_basedpyright_initializes_and_negotiates_encoding does not time out,
it hangs forever, parking a --workspace sweep at 38 of 92 suites with a
live basedpyright langserver child. The per-suite M4 gate already skips
it; the workspace sweep needs the same flag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZjWMjwPXhPbt9upku9mCk
2026-07-25 17:38:23 -04:00
Levi Neuwirth 1176f34ebc Merge remote-tracking branch 'githubsucks/main' into inline-math-slice 2026-07-25 17:29:35 -04:00
Levi Neuwirth 40709fafb9 docs: measure the CI coverage gap and record the vterm as-framed audit
The lane opened in the previous commit was scoped to the Vterm Stage 3
acceptance. Measuring it properly shows the problem is much larger and
not vterm-specific.

Comparing cargo test --list under CI's exact flags against the same
flags plus crdt: 3,024 versus 3,288. 264 tests are dark in CI, and the
single worst line is the library itself at 177 -- cargo test --lib
--features crdt is a required local gate that CI has never run. Ten
suites run zero or one test, including gpu_initial_target (#148's entire
acceptance, 1 of 14), gpu_invocation (#141's, 1 of 14), and a37, the
Stage 3 real-daemon/real-PTY/real-wgpu path that #135 built precisely
because a decoded-message fixture would prove none of the three fit
together.

The lane now carries the per-target table, the verified flag combination
for the fix, a two-part fix shape (a crdt leg on the test job, plus the
GPU-requiring suites onto the existing gpu-render job that already has
lavapipe), and an explicit instruction to sort deliberate exclusions
from accidental ones first -- some of the 264 are perf suites that are
ignored by default and belong to their own jobs, while m10_10_perf has
no ignore attribute and no job naming it.

docs/vterm-framing.md gains an as-framed audit section. The arc is
structurally complete and every test named in the Stage 2 verification
map exists, but criterion 22's "without thrash" clause was never pinned
anywhere -- the word appears nowhere in src or tests -- and that clause
describes exactly the defect #166 fixed. Of the nine Stage 3 tests, only
three drive a real daemon, so the six that construct EditorState
directly could never see a dispatcher-loop defect; a31 passes on the
broken tree for that reason. Four of the nine, including a37 and Stage 3
review round 1's own presence regression guard, do not run in CI at all.
The section also records what was not audited: section 11's blanket
claim about deferral safety covers roughly twenty items and none were
spot-checked.

docs/gpu-terminal-input-framing.md scores bet B2 true now that the
reporter has confirmed typing works, and retracts Q#GT5. The bash fixture
behind it does not reproduce in real use and was almost certainly
measuring its own timing rather than a product behaviour; it is marked
retracted rather than deleted so nobody re-derives it from an earlier
revision.

docs/agent-handoff.md section 5 gains the lesson the confirmation cost:
a daemon-side fix is not deployed until the daemon is restarted from a
tree containing it, and rebuilding a binary does nothing to a running
process.

No code changes.
2026-07-25 17:17:23 -04:00
Levi Neuwirth b70393762e test(lsp): gate the non-UTF-8 fixture on linux, not merely unix
CI round 1: both macOS jobs failed on the acceptance case added last
commit. APFS enforces valid UTF-8 in filenames, so `std::fs::write` with
a 0xFF byte in the name fails with EILSEQ ("Illegal byte sequence")
before `pmacs.fs.canonicalize` is ever called. The fixture cannot be
built there.

That is a filesystem refusing to represent the case, not a behavioral
difference: the subject — `to_str()` returning None for a non-UTF-8
resolution — is platform-independent Rust, and the Linux run pins it.
`#[cfg(unix)]` was the wrong granularity; review had asked for unix
gating on the symlink tests and I applied the same gate here without
checking whether the filesystem, rather than the API, was the
constraint.

Gated `#[cfg(target_os = "linux")]` with the reason in place, rather
than skipped at runtime, so a future failure here is a real failure and
not a silent no-op.

Ledger records both CI-round facts: this one, and that
`composition_overhead_under_ten_percent` is load-sensitive under a
parallel workspace sweep (it reported -4.6% realistic overhead in the
same run that tripped its 10% budget at 18.8%, which is noise, not work).
2026-07-25 17:03:10 -04:00
Levi Neuwirth b3c8230a84 docs: record the second main integration and its gate rerun
Main advanced twice inside one review round (#161, then #166), the
second landing while the first integration's sweep was still running.
The ledger now names both integrations, how each doc conflict was
resolved, and the verification numbers for the twice-merged tree -- plus
the lesson that a lane in review against a fast-moving main reruns its
gates per integration, not per push.
2026-07-25 16:44:57 -04:00
Levi Neuwirth 1c9904e217 Merge canonical main (#166) into the dired Stage 1 lane
Main moved again while this lane was gating: the GPU terminal-input fix
merged as #166. One conflict, in COHERENCE.md's journey table, resolved
as the union -- this lane owns step 7's file half, #166 owns step 8's
GPU-terminal addendum.
2026-07-25 16:41:21 -04:00
Levi Neuwirth 8c924ebf41 docs: record #166 as landed and open the CI-coverage lane
The handoff snapshot and the active-work ledger both still described the
GPU terminal input work as in review. Per their own update protocols this
should have ridden #166; it did not, because the review that surfaced the
CI-coverage finding came after that PR was already green, and expanding
an approved PR to carry a new lane would have been the wrong trade.

docs/agent-handoff.md section 1 gains the #166 entry: the split into a
frontend-kind-neutral liveness half and a grid-only geometry half, the
extracted dispatcher loop body, the trap about the no-placement release
that reads like liveness and is not, and why the one-line guard was
rejected.

docs/active-work.md moves the lane to "Closed since the last snapshot"
and opens a new one: the Stage 3 real-path acceptance is dark in CI.
The workflow never enables the crdt feature, so every crdt-gated
acceptance test is not merely skipped but never compiled -- which covers
a37 (real daemon, real PTY, real wgpu) since #135 as well as the two
tests #166 added beside it. The fix is one step on the gpu-render job,
but it needs its own lane because it would run a37 under lavapipe for
the first time, and neither its timing budgets nor its wgpu path have
been exercised on that adapter. The lane also asks which other
crdt-gated suites are dark for the same reason.

Recorded alongside it: #166's three unit pins are not crdt-gated and do
run under CI's exact flags, including the controller-release pin whose
only job is catching the plausible wrong fix, so the regression
protection is live even though the real-daemon evidence is local-only.

No code changes.
2026-07-25 16:17:24 -04:00
Levi Neuwirth 08e2807fcc fix(dired): correct the reporting-channel claim #161 falsified
The module doc said an uncaught raise inside a `pmacs.async` coroutine
"goes to *errors*, not the status line". #161's COHERENCE finding shows
that is wrong, and in the worse direction: `pmacs.error` is never
defined in production, so `step()`'s guarded report is dead and the raise
falls through to a bare `error()` inside `pmacs._async.tick()` -- whose
result `EditorState::tick_async` discards with `let _ =`. The failure
reaches nowhere at all, and dired would look like it silently did
nothing.

So the per-coroutine `pcall` plus `pmacs.editor.set_status` is
load-bearing, not tidy, and the doc now says which channel is dead, which
is live, and that the acceptance suite observes the live one -- the
corollary COHERENCE draws from that finding.

The ledger records the integration, the reruns on the merged tree, and
the ops lesson that cost three CI runs: a conflicting PR has no merge
ref, so GitHub creates no `pull_request` run and nothing reports the
absence.
2026-07-25 16:14:06 -04:00
Levi Neuwirth bdd7611d0e Merge canonical main (#161) into the dired Stage 1 lane
Multi-root LSP affinity merged as #161 (`main` @ `46a1b8f`) while this
lane was in review, which made the PR conflict -- and a conflicting PR
has no merge ref, so GitHub silently stopped running CI on it after the
first push. Integrating rather than rebasing, per the #135/#137
precedent: the review anchors stay addressable and every gate is rerun
against the merged tree.

One conflict, in COHERENCE.md's in-flight list, resolved as the union of
both truths -- and #161 is now merged, which its own text still called a
PR.

The overlap to watch is `src/lua_bindings/mod.rs`: #161 widened the
`lsp.list()` row builder while this lane added `pmacs.path` and the
read_dir listing conversion. The merge was textually clean, which the
folding arc's lesson says is not the same as compiling, so the full gate
suite reruns from here.
2026-07-25 16:04:00 -04:00
Levi Neuwirth aff3a60332 docs: correct the purge's reachable-leak claim; record the 3a lane
Rev 5 said acceptance 34's second edge was a killed buffer. Implementing
it showed that is false: the Rust core fires exactly five hooks —
buffer.after-edit, buffer.after-load, buffer.after-switch,
frontend.detached, process.after-tick — and there is **no buffer-kill
hook**, so lsp.lua never tears an attachment down and the drain keeps
reaching that server. The premise (the drain builds its sid list from
`attachments`) was right; the inference needed attachments to be removed
on kill, and nothing removes them.

The reachable leak has the same root cause by a different path.
`attach_buffer` drops a sid from `attachments` the moment
`server_is_live` reports false and rebuilds against a fresh server — so
`crashed` / `stopped` is the event *least* likely to be drained, and an
event-driven purge leaks in exactly the case it exists for. The purge
therefore polls `pmacs.lsp.list()`, which enumerates the manager
directly. Acceptance 34's second half now exercises a server in **no**
attachment, which is the shape that discriminates: bitten, an
event-driven purge fails it while the attached case still passes.

§0.1 finding 6, Q#LN9, and acceptance 34 all updated; the wrong wording
is left visible with its correction rather than quietly replaced, since
the mistake is the useful part.

Ledger gains the Stage 3a lane: branch, worktree, what ships, both
corrected claims, the `install_async` load-order trap, the recorded
bites, the one knowingly unpinned guard, and gate results.
2026-07-25 16:01:18 -04:00
Levi Neuwirth adf90e5351 docs: record the GPU terminal input fix in the three required docs
COHERENCE.md section 25 and the handoff/ledger update protocols make
these ride the PR.

COHERENCE.md:
- Section 6 named one optimistic key classifier and attributed it to the
  GPU. There are two, one per replica frontend:
  crate::optimistic::classify_key belongs to the pmacs --attach TUI
  replica, and pmacs-gpu has its own unrelated optimistic_insert_text /
  optimistic_crdt_insert. The section's "kept honest by
  dispatch_idle_for" claim is confirmed for both, which this
  investigation verified rather than assumed.
- Section 16 graded per-frontend degradation strong on the evidence of
  per-frontend fold projection. That grade stands, but the practice is
  enforced by convention rather than structure, and this defect is the
  counter-example; the note says so and points at what is now structural.
- Section 2 step 8 records that the terminal was broken outright on the
  GPU frontend, not merely undiscoverable.

docs/agent-handoff.md section 5 gains four lessons: adjacency does not
make two operations alternatives (and two individually sound idempotence
guards can be jointly useless); bite against every pre-image the fix
could have taken, since the obvious guard here fixes the storm and
introduces a controller leak; a quiet child is an instrument, because a
frame storm hides inside a chatty fixture and a geometric readout is
satisfied by an oscillating geometry; and TerminalMode::Raw makes
sh-based input fixtures useless because there is no ICRNL.

docs/active-work.md gains the lane entry with the branch, the bite
matrix, the named out-of-scope items, and the gate results.
2026-07-25 15:28:47 -04:00
Levi Neuwirth 14881b26c0 docs: record dired Stage 1 review round 1
Framing rev 7 adds S1-10..S1-12 -- the three findings that changed
behavior, each stated as the durable lesson rather than as a diff:
painting takes a buffer and seating takes the world, so any post-await
cursor operation needs an active-buffer guard; the rendered columns are
a contract Stage 3 is planned against, so precision yields to width; and
`open_directory`'s changed-nothing-on-failure invariant is itself a
probe, which is why the symlink descent no longer lists the target
twice. Plus the tolerant-channel note: cancellation was never a backstop
for a dired listing, because nothing cancels one.

The ledger records the round, the updated counts (dired 25 + 25 CRDT,
sweep 3,189 across 92), and the process lesson that cost me the fixes
once: a mutation-bite helper restores with `git checkout --`, so a fix
must be committed before it is bitten.
2026-07-25 15:24:25 -04:00
Levi Neuwirth b775f1703a fix(daemon): stop resizing a semantic frontend's PTY twice per tick
The dispatcher loop applied BOTH terminal-layout syncs to EVERY attached
frontend. A semantic session satisfies both conditions, because it has a
term_sizes entry from AttachRequest and a semantic terminal declaration,
so its PTY was resized twice on every tick forever: the grid arm
installed the TUI placement size, the semantic arm installed the declared
content rectangle, and each arm's own idempotence guard only ever saw the
size the other had just written. The child took a SIGWINCH storm at tick
cadence and the screen reflowed continuously, which is what made typing
into a GPU terminal impossible while output kept flowing.

The grid arm is also the only per-tick controller-liveness release a
semantic frontend gets, so simply skipping it for those frontends trades
one defect for another: a GPU window that switches away from its terminal
would hold the controller forever, and no peer could resize that PTY
again. The semantic arm cannot take over that job, because the
buffer-follow snapshot clears the viewport declaration that would drive
it.

sync_terminal_layout is therefore split into a frontend-kind-neutral half
(panel reconciliation plus controller liveness, which read only views,
windows and the controller) and a grid-only geometry half (TUI placement
plus the resize). The dispatcher runs the neutral half for every attached
frontend once per tick, then exactly one geometry arm per frontend kind.
sync_terminal_layout survives as the composition of both halves, so the
in-process editor loop and LOCAL are unchanged.

The loop body is extracted into sync_terminal_layouts_for_tick, which
makes the grid/semantic exclusivity structural rather than two adjacent
ifs, and lets the tests drive the real loop body instead of a
re-implementation.

The release that fires when a window has no placement stays in the grid
half deliberately: a semantic frontend has no window_placements entry at
all, so moving it into the neutral half would release a GPU session's
controller on every tick.

Bite-verified against two pre-images, because one is not enough here --
the naive guard fixes the storm and introduces the controller leak, so a
single revert would score the fix complete when it is not:

  pin                        main    naive guard   split
  settle (acc 2+3)           FAIL    pass          pass
  controller release (acc 6) pass    FAIL          pass
  grid still resizes (acc 5) pass    pass          pass

Real-path acceptance: a quiet child that counts SIGWINCH reports 144
frames in 4 s and WINCH 1..12 on screen against the pre-fix tree, versus
a settled screen with the fix. Acceptance 4 (input reaches the child and
returns) is a keep-working pin and passes on both sides -- key transport
was never the defect.

No protocol change; stays v20.
2026-07-25 15:22:04 -04:00
Levi Neuwirth a516a46359 docs(lean4): rev 5 round 1 — fix the empty-marker case, sweep citations
Two review findings, both revision edits.

**Q#LN8's marker test was wrong in the other direction.** Rev 5 fixed
the directory case by reading a byte and requiring a non-nil read — but
an **empty** `lean-toolchain` reads nil at EOF too, so that rule
declines a marker that exists, silently, falling through to
`pmacs.project.detect`. Marker semantics here are `lean4-mode`'s
`locate-dominating-file` semantics: existence, not content, and a
`lean-toolchain` can legitimately be empty.

The discriminator is `read`'s second return, probed on LuaJIT 2.1:

| Path | `io.open` | `f:read(1)` | Verdict |
|---|---|---|---|
| file with content | handle | `"l"`, no error | marker |
| empty file | handle | `nil`, no error | marker |
| directory | handle | `nil`, `"Is a directory"` | decline |
| missing | `nil` | — | decline |

So `local data, err = f:read(1)`, declining only on a non-nil `err`. The
rule needs no per-platform re-probe: both directory behaviors are
declines, since a platform whose `fopen` refuses a directory fails at
`io.open` and one that opens it fails at `read`. There is no platform on
which a directory both opens and yields a byte.

Acceptance gains **24b** (an empty `lean-toolchain` marks a root) beside
24a, with the obligation that each be shown to fail against the
implementation satisfying only the other. A suite carrying just one is
satisfied by a resolver silently wrong for the other case — which is
precisely how rev 5's first answer got written.

**Citation sweep.** Round 4 stated the `project_root_for` correction in
§0.1 without editing the citation in §2.5; the correction and the fix
are different acts, and noting one is not doing the other. Review caught
a second stale citation (`handle_server_requests` at :1448), which
prompted a sweep of every `file:line` from §2.4 onward. Four more were
stale. All six: `project_root_for` 513 → 592, `ensure_server` 527 → 610,
`handle_server_requests` 1448 → 1549, `take_typed_edit` 12798 → 12827,
`pair.lua` 213 → 229, `compile.lua` 264 → 266. Six others were verified
good and left alone, listed in §0.1 so the next sweep knows what has
already been checked.

Q#LN15's present-tense "the change is small and spans two files" now
reads as past tense with its PR number, since that stage landed. Its
pre-#161 line numbers stay as written — historical record, not
navigation.
2026-07-25 15:17:51 -04:00
Levi Neuwirth 9a0df21618 docs: frame the GPU terminal double layout-sync defect
Reported symptom: text input in a terminal does not work on the GPU
frontend while the TUI is fine.

Root cause, measured against a real daemon, a real PTY child, and the
real pmacs-gpu attach client: the dispatcher loop applies BOTH
terminal-layout syncs to EVERY attached frontend each tick. A semantic
session satisfies both conditions -- it has a term_sizes entry from
AttachRequest and a semantic terminal declaration -- so the PTY is
resized twice per tick forever, because the grid arm runs the TUI
placement helper that subtracts a modeline the GPU never draws. The
result is a SIGWINCH storm plus screen reflow at tick cadence, which
makes an interactive line editor unusable while child output keeps
flowing. Measured 730 terminal frames in 20 s for a static screen.

Revision 2 answers Q#GT4 from the code rather than deferring it, which
changes the fix from a one-line guard into a split of
sync_terminal_layout: the grid arm is also the only per-tick
controller-liveness release a GPU session gets, and the semantic arm
neither performs it nor can be made to, because the buffer-follow
snapshot clears the viewport declaration that would drive it.

The doc also records three falsified hypotheses, why the Stage 3
real-path acceptance passes on the broken tree, and a COHERENCE.md
section 6 citation error (two replica frontends have two different
optimistic classifiers; the audit names only one).

No code changes in this commit.
2026-07-25 15:09:07 -04:00
Levi Neuwirth 8b685dc127 docs: record dired Stage 1 (PR #165) and what it falsified
docs/dired-framing.md rev 6: §0 gains the Stage 1 implementation notes
(S1-1..S1-9) -- the normalizer is exposed rather than mirrored (so B2 is
false by one small binding, in the direction Q#DR2 preferred); R2-3's
dedication claim is falsified by the display policy; acceptance 3c
cannot pin the descent routing and now says so; dired is the first
builtin to bind a mode-scoped key, which one pre-existing lib test
assumed impossible; `C-x d` takes no completion source on purpose;
ownership is the handle table alone; the mark column ships blank; a
symlinked directory needs a probe; and interactive origin does not
survive an await.

COHERENCE.md, per its §25 (an audited claim this PR changes updates
here, riding the PR): §1.1's interactive-file-opening fact, §2's journey
step 7, §4's beginner-level `files`, §14's tree bullet (Stage 1 landed a
flat listing and did NOT invent a tree convention), and §15's Priority 1
list. Step 3 stays **Missing at the CLI** with the mechanism spelled
out: `pmacs .` still exits 1, and this arc deliberately does not claim
the CLI path -- it supplies the buffer a directory should resolve to.

docs/active-work.md: the dired lane rewritten for Stage 1, including why
the branch is a fresh cut rather than a rebase of `dired`, the durable
substrate facts, the bite results (one VACUOUS, recorded rather than
relabelled), and the verification. Its canonical-base line was four
merges stale and now names 8c86d34.

docs/agent-handoff.md: one forward pointer only. The handoff describes
merged state, so it absorbs the substance when this merges.
2026-07-25 15:08:24 -04:00
Levi Neuwirth 0fe1051d25 docs(lean4): rev 5 — re-scout Stage 3 and split it into 3a and 3b
Stages 1 and 2 landed (#160, #161). Re-scouting Stage 3 against `main`
@ `46a1b8f` — six merged PRs past the rev-4 snapshot — produced three
findings that change the plan and four that confirm it. Two were
established by running Lua in a fresh `EditorState` rather than by grep,
and are marked *probed* in §0.1.

**Stage 3 violated this document's own splitting rule.** §4 says "no PR
in this arc mixes a cross-cutting substrate change with Lean feature
content" and "a reviewer looking at Stage 3 sees only Lean" — while §4's
own risk column for Stage 3 read "two `lsp.lua` generalizations". Those
cannot both be true. One generalization shipped as Stage 2; the other is
Q#LN9's dispatch seams, which modify `handle_server_requests` —
confirmed the only production drain of LSP events, since
`LspManager::take_all_events` has no non-test caller. By the test that
justified splitting Stage 2 out, that is cross-cutting substrate. Stage
3 is now 3a (seams + canonicalizer, no Lean) and 3b (the Lean server),
strictly sequential.

**The Lean resolver could not satisfy the contract Stage 2 documented.**
#161 established that a configured root reaches `file_uri_for` verbatim
and that the resulting URI is the affinity key. Probed:
`pmacs.editor.file_path()` is not canonical — opening
`<tmp>/linkpkg/sub/./../sub/a.lean` through a symlink yields
`<tmp>/linkpkg/sub/a.lean`, lexical collapse only. No canonicalize
binding is exposed to Lua, and `pmacs.project.detect` canonicalizes but
returns nil without a marker. So one Lake package opened by two
spellings would spawn two `lake serve` processes — the bug Stage 2 was
built to prevent, re-entered through Stage 3's door. New Q#LN20 adds a
synchronous `pmacs.fs.canonicalize`; it rides 3a, and it serves every
future function-valued root rather than only Lean's. Two alternatives
are recorded with why they were rejected — the `detect`-anchored walk in
particular is incorrect, not merely inelegant.

**`pmacs.fs.stat` is unusable in the resolver.** It is async and the
resolver runs synchronously inside `ensure_server` ← `attach_buffer` ←
`buffer.after-load`, with no coroutine to await on. Probed: `io` and
`os` are exposed in the sandbox, so the marker walk uses `io.open` — the
opposite of what a reader would assume, hence Q#LN8 now says so. One
edge, also probed: `io.open` succeeds on a directory, so the walk reads
a byte rather than testing for a handle, and acceptance 24a bites the
version that does not.

Confirmed rather than changed: Q#LN7's stop-before-respawn is necessary
(default policy is OnCrash, the termination handler never consults the
exit code, and `maybe_restart` has no attempt ceiling — a broken `lake`
respawns forever; `stop()` setting `restart = Never` is what disarms
it); the response seam works as specified, since `Response` events are
pushed unconditionally and `send_request` returns the keying id.

One confirmation narrowed the design. `handle_server_requests` builds
its sid list from `attachments` and `push_event` is uncapped, so
subscribers fire only for servers with a live attachment. That turns
acceptance 34 into a reachable leak: killing the buffer with a request
outstanding strands the registration behind a drain that no longer runs.
The purge is now driven from both edges and 34 exercises the buffer-kill
path, which is the one a user can reach.

Also: §9 states the lane's coherence impact per COHERENCE §20 (journey
steps, interaction islands, config registry, background attribution),
including the honest note that 3b makes §2's step-3 grade marginally
worse by adding one more instance of the silent-spawn-failure class.
Three items are named in §6 rather than paid: the uncapped event queue,
the dropped `cfg.restart`, and surfacing the spawn failure itself.

Acceptance keeps every rev-4 number. The two split sections are
bulleted with literal labels because a markdown ordered list renumbers
from its first item, and 3b's criteria are non-contiguous; round 3's
finding 4 was stale references surviving a renumber, and not renumbering
is the cheaper way to not repeat it. Stale cross-references from the
split were reconciled in the same pass, and `project_root_for`'s
citation was corrected from 513 to 592 per COHERENCE §25.
2026-07-25 15:05:44 -04:00
Levi Neuwirth 7071c27757 Merge githubsucks/main (46a1b8f) into inline-math-slice
Lean 4 Stage 2 (#161) landed while this branch's first-ever CI run was
in flight, which put the PR back to CONFLICTING at an unmoved head.

Merged rather than rebased, same as the 8c86d34 integration and for the
same reason: the PR is awaiting review rounds and a rebase would break
every review anchor.

The sole conflict was docs/active-work.md, as it was last time and for
the same structural reason -- every merge to main edits the lane ledger,
so a long-lived PR re-conflicts there and only there. Both sides' lanes
kept verbatim; main's updated Lean 4 heading taken over the stale one.

This integration is code-disjoint from the lane. Intersecting main's
changed files (COHERENCE.md, builtin/runtime/lsp.lua,
src/lua_bindings/mod.rs, tests/lsp_multi_root_acceptance.rs) against the
lane's own changed-file set leaves exactly docs/active-work.md, so none
of the first integration's pmacs-gpu/src/main.rs auto-merge risk recurs
here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZjWMjwPXhPbt9upku9mCk
2026-07-25 14:48:47 -04:00
Levi Neuwirth 8b457de878 docs: record the post-integration gate run
Separates the pre-integration numbers, which described a tree 28 commits
behind, from the ones that describe what the PR now proposes.

The GPU test count is the integration proof rather than merely a pass: it
went 199 to 202, and e547a90 added exactly three tests to pmacs-gpu,
which is the entire delta on main since the merge base. Both sides' tests
are therefore present and running, and neither was dropped by the
auto-merge. Confirmed structurally as well -- main's fix survives as the
deferred closure form rather than the eager one that panicked, with its
regression test, alongside this lane's math work in the same file.
2026-07-25 14:37:15 -04:00
Levi Neuwirth f3e065dc78 Merge canonical main (8c86d34) into the inline-math slice
The lane was 28 commits behind. Merged rather than rebased, per the
#135/#137 precedent: the PR is awaiting review rounds and a rebase would
break every review anchor.

The only conflict was docs/active-work.md, where both sides add lanes.
Kept both: main's lanes verbatim, with this lane leading since it is the
one in flight. The conflict was pre-existing rather than introduced by
the dired or Lean 4 ledger commits -- it already conflicted against main
at e745068.

The integration surface, derived from git diff merge-base..main rather
than from another PR's file list, is pmacs-gpu/src/main.rs: main gained
72 lines there from e547a90, the minimap all-blank-slab divide-by-zero
fix, and this lane rewrites large parts of the same file. Git auto-merged
it textually. A clean auto-merge is not evidence the tree compiles, so
the full gate suite is what discharges it; the ledger records the
post-integration numbers separately from the pre-integration ones, which
described a tree 28 commits behind.
2026-07-25 14:32:48 -04:00
Levi Neuwirth 3952db9657 Merge remote-tracking branch 'githubsucks/main' into HEAD 2026-07-25 14:26:21 -04:00
Levi Neuwirth 7c01c93226 docs: dired arc framing (revision 5) + post-merge doc refresh
Lands the approved dired framing on main as its own docs PR, and brings
the two required docs current after find-file merged as #162.

The framing was approved after two review rounds (seven findings, then
six) and revised twice more since: revision 4 recorded what implementing
Stage 0 falsified in the approved text, and revision 5 adds the coherence
impact statement that #163 made mandatory for every framing.

The coherence statement is new work, not a restatement. COHERENCE.md
section 20 Priority 1 already names this arc -- a find-file surface and
directory-argument handling -- so the framing now states which journey
steps it touches (7, and partially 3), that it adds no interaction island
because its keys are a mode-scoped keymap through the ordinary registry
and wdired is a mode swap rather than a modal layer, that it adopts the
config registry for dired.kill-when-opening, and that it inherits the
worker-attribution gap for its read_dir jobs without worsening it. It
also draws the boundary against the adjacent Journey Stage 1 arc: CLI
directory handling belongs there, the two meet at resolve_target_buffer,
and dired supplies the buffer a directory should resolve to rather than
growing a second directory surface.

One convergence worth recording: section 2 grades the golden journey
broken at step 3 because pmacs on a directory exits 1, and the mechanism
it cites -- File::open succeeding on a directory, then read_to_end
returning EISDIR -- is the same one Stage 0 pinned in its
accepting-a-directory test, where the pcall turns it into a status
message instead.

The handoff snapshot was stale through eight merges. It now anchors on
main at 2af1ab3, records COHERENCE.md as required reading and a required
framing input, and carries the two minibuffer facts find-file
established: a custom completion source cannot descend directories, and
a selected candidate shadows typed text -- both of which apply to M-x and
switch-buffer, not just find-file.

The ledger gains the dired lane with Stage 1's scope, the reason its one
Rust change cannot be done in Lua, and the rebase note for the dired
branch, whose framing commits become redundant when this lands.
2026-07-25 14:09:49 -04:00
Levi Neuwirth 35085b54d1 fix: rustfmt the acceptance suite and pin two untested arms (round 1)
The blocker was process, not design. The test file was committed before
`cargo fmt` ran, so the reflow of five over-width assertions sat
uncommitted in the working tree while the branch as pushed failed the
first gate in CLAUDE.md. The "fmt clean" reported on the PR described
the worktree, not the branch. Gate results are only meaningful run
against the pushed tree, so this commit lands the formatting first and
the gates are re-run against it.

Two pins review asked for, each covering a branch the nine acceptance
tests left untested:

- A **string** `config.root` as an affinity key. acc17 covers only the
  function form, so `return configured, "config"` had no test. The bite
  puts both files in their own marked project: drop the config arm and
  they key on their own detected roots and spawn two servers, so one
  server on the configured root is only reachable if the override wins.
- `root = false` reads as unset. Defended by a truthiness check rather
  than `~= nil`, previously by comment alone. Under `~= nil` the config
  arm returns `false, "config"` and `file_uri_for(false)` returns nil, so
  the file lands on a rootless server instead of its detected project.

Each was falsified against exactly the mutation it targets and neither
against the other.

Also documents an asymmetry review caught: `project_root_for`'s
"detected" arm is canonicalized for free because `pmacs.project.detect`
canonicalizes before walking, but a **configured** root — string or
resolver return — is fed to `file_uri_for` exactly as written, and the
affinity key is that URI. On macOS a resolver returning `/var/…` and a
detected `/private/var/…` are therefore different keys for one
directory, silently yielding two servers for one project. There is no
Lua-side canonicalizer to normalize it, and Stage 3's Lean resolver is
the first real consumer, so the obligation is stated in the
`config.root` doc comment where that resolver's author will read it.
2026-07-25 11:19:43 -04:00
Levi Neuwirth 92f57d8894 docs: record Stage 1's landing and the Stage 2 affinity lane
Stage 1 merged as #160 (`main` @ `0827dd1`); the Lean lane header and
branch line now say so, and Stage 2 gets its own subsection.

Edits stay inside the Lean lane. PR #156 is still open against both this
file and `docs/agent-handoff.md`, and it rewrites the snapshot header,
the canonical-base line, and the whole bottom-panel lane — so those are
left alone rather than merged twice. `agent-handoff.md` is untouched for
the same reason plus its own: §1 describes what is on `main`, so it
updates at merge, not during review.

Records the one finding this stage turned up but did not fix:
`ensure_server` never forwards `cfg.restart` to `pmacs.lsp.spawn`, so a
`restart` in `pmacs.lsp.config[lang]` is silently dropped on the
auto-attach path. Pre-existing, and out of scope for a PR whose
acceptance 16 pins existing attach behavior as unchanged.
2026-07-25 11:03:54 -04:00
Levi Neuwirth 1a5805366a docs: record the Lean 4 lane in the active-work ledger
Review round 1 flagged that neither ledger knew about this branch, and
`docs/active-work.md`'s stated job is exactly the volatile open lanes.
Records the branch, base, framing revision, what Stage 1 ships, the
discharged Q#LN1 obligation, the Q#LN4 blast radius, and the four
implementation findings that are not in the framing (the `warning`
colour collision with `number`, `Some(1)` resolving to `@function`
rather than `@constructor`, the `module > declaration > def` nesting,
and `injection_aliases` being a write-only proxy). Also carries forward
the two Stage 2 corrections the framing already holds, since that lane
starts next.

Deliberately ADDITIVE ONLY -- one new section, zero deleted lines. PR
#156 is open against both this file and `docs/agent-handoff.md` and owns
the snapshot header, the canonical-base line, and the bottom-panel
lane's status. Touching those here would collide with a PR already in
review, which is the "frozen reviewed PRs do not absorb moving
overlapping work" lesson from #135/#137.

`docs/agent-handoff.md` is deliberately untouched: its §1 snapshot
describes what is ON `main`, so it gets updated when this merges, not
while it is in review.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 10:18:38 -04:00
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 14c1c01043 feat(math): caret-driven suppression, the draw pass, and the slice acceptance
The two halves that touch live rendering, landed together because the
acceptance criteria that make either honest need both.

Suppression (Q#MS3/MS4/MS5/MS11). Detection runs in the per-line chunk
builder — the chunk-build path, never the edit path — and substitutes
each suppressed span's source bytes with ONE spacer chunk BEFORE tab
expansion, so a literal tab inside a span vanishes with it while tabs
outside keep their SourceTab provenance. The gate reads the EFFECTIVE
caret (own_cursor, which optimistic edits predict forward — F4's
no-flap requirement holds by construction) plus both own-selection
endpoints. Three motion paths can flip a gate without a content
change, and each now re-runs the per-line chunk compare, gated on a
one-scan "does the visible slice hold a $" check: the CursorByte arm,
finish_optimistic_edit (the text re-chunks under the OLD caret there;
without the hook a typed char rendered one keystroke stale), and the
Decorations arm — whose "no decoration change needs a reshape" premise
acquires exactly one exception, the Selection endpoints Q#MS11 made
suppression inputs.

The line-reuse predicate (acceptance 11, the #120 edge). Per-line
math state is cached in lockstep with line_chunk_cache: every detected
span with the gate bit it was built under. The scroll-reuse path
refuses a retained line whose cached bits disagree with the CURRENT
caret/selection — content is unchanged on every reuse path, so the
cached span set is authoritative and the gate bits are the only
variable. The acceptance test drives the stale-gate case through
rebuild_lines_reusing_scroll directly and fails if the gate is removed
from the predicate.

The hit map (B1'). hit_test_source_byte rebuilds its runs from a
whole-slice chunk walk, so it now reads the substitutions BACK from
the per-line caches — never re-planned under a possibly-newer caret —
keeping the map and the shaped glyphs one source of truth.

The draw pass (Q#MS6/MS7). Every MathItem::Glyph draws from its own
mini-buffer with Attrs pinned to the bundled math family (F8b), placed
at layout's exact x and the shaped line's REAL baseline; the
mini-buffer itself is positioned by the line_y cosmic-text actually
produced for it, so no font-metric rederivation can drift. Fraction
rules ride the bg quad batch after the decoration washes and under the
glyphs. Wash geometry gains Q#MS11's intersection rule: a wash
touching a suppressed span widens to the box's whole reserved
rectangle (a match strictly inside the span produced a zero-width
interval before), while the round-3 exclusive-end fix keeps a
non-intersecting wash off the box.

Acceptance (framing §5). Criteria 5-11 and 14-16 run on real pixels
through render_to_view: drawn ink where a literal-spacer control
renders none, with the before-region pixel-identical; the fraction
rule as a full-width run with operand ink both sides; caret-inside
rendering EXACTLY as math-disabled (driven through the real
CursorByte arm, which owns the refresh — a direct helper call would
not have pinned the wiring); every failure mode (unbalanced, unknown
command, $$, uncoverable glyph) pixel-equal to disabled; box clicks
snapping to the span start with the trailing edge landing after the
span; the scroll-reuse stale-gate bite; reflow confined to the
affected line with the after-text shifted by exactly the quantized
projection difference; selection gating and the whole-rectangle wash;
and the licence provenance pair. Criterion 17 is discharged
differentially: cargo tree -e features output for ttf-parser is
byte-identical with and without this crate's dependency line.

Also folded in, per the round-3 close-out: the F6 documenting test
($a$$b$ is eaten by the $$-opaque rule; one separating character
restores both spans), the depth-search bound raised 6 -> 8 so a
metric shift cannot make the "floor is dead code" expect fire with a
misleading message, the MathBox { end, .. } pattern nit, and the
active-work.md lane entry.

Named v0 approximations, deliberate: the peer-caret half of
acceptance 14 is pinned at the mapping level (unit tests), not
pixels; a soft-wrapped spacer draws its box whole at the first run's
origin (the one-rectangle model); the fit budget reads the bundled
code face even under a custom set_font family — the draw anchors to
the real shaped baseline either way, so only the fit margin is
approximate.

Clippy is CLEAN across the workspace at -D warnings for the first
time on this branch: the draw pass consumed every formerly-dead item,
and the three lints it could not fix (a test-only accessor, one doc
string, one manual midpoint) are fixed here.

Gates: cargo fmt --check; cargo clippy --workspace --all-targets
-- -D warnings; 1,815 default + 1,992 CRDT library tests; M4 121
(basedpyright skipped); 199 pmacs-gpu tests under PMACS_REQUIRE_GPU=1;
workspace sweep 3,131 across 88 suites (isolated XDG_CONFIG_HOME);
git diff --check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 22:04:35 -04:00
Levi Neuwirth 7f7ad1934c docs: record bottom-panel Stage 1 (#155) landing
Handoff §1 gains the arc entry: the window-parameter substrate, the two
production `Layout::compute` callers, the recursive minima, hiding as a
durable transition, per-window input gating, the per-frontend jump
origins, and the shared initial-target load seam. §5 gains four durable
lessons, three of them the same class:

- a guard with no production caller passes every direct-call test;
- a geometric readout (`at_bottom`) is not a state predicate;
- a PTY does not translate LF to CRLF, so text equality over clipped
  output is vacuous;
- widening an ambient resolver into a scoped one can make a total
  function partial — which is what took CI red on all four Test jobs.

The roadmap position, the arc's named deferrals, and DAP's unblocking
are recorded too.

active-work closes the lane, refreshes the canonical base to `e745068`,
and keeps Stage 2's named obligations plus the two gating facts found on
the way (the sweep needs an isolated XDG_CONFIG_HOME; compile_mode
acceptance is load-sensitive, verified pre-existing).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 20:33:32 -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 cbf7782726 fix(math): review round 3 — mapping bug, whitespace defect, real MATH gaps
F1 was a real bug pinned by my own committed test. `end` in
ChunkSource::MathBox is EXCLUSIVE, so source position `end` is the first byte
AFTER the span — but the arm claimed it for the box's left edge, and the test
asserted that wrong value while calling the byte "interior". Consequences it
would have caused once overlays land: a search match starting just after a
span washes the whole box it does not intersect, violating Q#MS11; a peer
caret after the span draws at the box's left edge; caret geometry jumps
backwards. The same class existed in projected_to_source for a line-FINAL box,
where `within` clamps to the run length and the arm returned `start`
unconditionally, so a click past end-of-line landed on the span start. Both
committed hit tests put a chunk after the box, so that edge was never
exercised; there is now a test with the box last.

F2: parse_scripts peeked for the next marker without skipping whitespace, so
`x^2 _i` built a NESTED script — drawing the subscript displaced right by the
superscript's width — and `x^2 ^3` parsed where TeX errors, contradicting the
module's own "whitespace is insignificant" rule.

F3: layout is now fallible. A character the math font cannot draw used to
yield zero metrics and still emit a Glyph item, rendering tofu at zero advance
over its neighbour. Q#MS8's rule is "failure is always show the source", and
the draw pass needs a refusal signal — changed now, before that pass consumes
the API.

F4: the fraction gap was a hardcoded `thickness * 2.0` while the MATH table's
FractionNumeratorGapMin / FractionDenominatorGapMin went unread. Reading them
moved the flagship \frac{a}{b} from 0.732 to 0.867 and the fallback boundary
from depth 3 to depth 5. The round-2 review's hand-arithmetic estimate of
~0.85 was right; my 0.732 was inflated by the guess. The depth-SEARCHING test
absorbed the change without edits, which is the property it was written for.

F5: TeX's \epsilon and \phi are the lunate/symbol forms (U+03F5, U+03D5), not
U+03B5/U+03C6. Their italic mappings had to land with the seed change, since
both sit outside math_italic's U+03B1..03C9 run and would otherwise render
upright beside italic neighbours.

F7: the line-box budget derivation moved out of the test into
`line_box_budget`, so the draw pass and the acceptance test cannot compute
different splits while both stay green.

F8: the live-code clippy items are cleared. The 25 that remain are all
dead-code awaiting the draw pass.

189 pmacs-gpu tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 20:19:40 -04: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 f708ccb2a4 feat(math): box layout, and measure the real height budget
MathBox/MathItem composition for the Q#MS2 subset: characters advance a pen,
scripts shift by the MATH table's superscript/subscript amounts at script
scale, and \frac stacks its operands around a rule at the math axis. Inline
\frac sets its operands one style down, which is TeX's rule and also what the
parent framing's Tier 3 specifies — and it is load-bearing for Q#MS10, since
full-size operands would not fit the line at all.

The height budget is now measured rather than assumed, and the round-2 review
was right to insist on that. Two things were wrong.

First, my own test derived the budget from the MATH font's metrics. Q#MS10
says the budget is the LINE BOX, whose baseline the CODE font places —
JetBrains Mono ascends 16.32 px and descends 4.80 px at 16 px inside the 22 px
line, against Latin Modern Math's 12.90/3.10. Using the wrong font made a
plain \frac{a}{b} score 0.485 and appear to fall below the floor, which would
have meant the flagship case never rendering.

Second, with the budget derived correctly, B6 holds — \frac{a}{b} scales to
0.732 — but rev 3's guessed fallback case does not. A doubly-nested fraction
scores 0.744 and still renders; the floor is not tripped until depth 3, at
0.580. Round 2 predicted precisely this surprise-pass. Worth keeping: depth 2
scores HIGHER than depth 1, because the binding constraint flips from descent
to ascent as nesting grows asymmetrically, so "deeper is always tighter" is
false.

The test therefore SEARCHES for the tripping depth instead of hardcoding it,
and fails if no depth trips the floor at all — which would mean the fallback
arm is unreachable and the floor is dead code. Acceptance 12 records the
measured table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 18:39:26 -04:00
Levi Neuwirth 320bcce276 feat(math): bundle Latin Modern Math and read its MATH constants
Font, licence, and the metrics half of Tier 3.

The bundled font is Latin Modern Math under the GUST Font License, added as
fonts/GUST-FONT-LICENSE.txt — deliberately a separate file from fonts/OFL.txt,
which covers JetBrains Mono only. GFL is LPPL-derived, not the SIL OFL; the
framing's F6 corrected that error and this is the discharge. At 733,736 bytes
the font is now the largest embedded asset in the repository.

ttf-parser is declared with default-features = false and only
"opentype-layout". Verified differentially: the ttf-parser feature set from
`cargo tree -e features` is byte-identical with and without this dependency
line, so the declaration widens nothing and forces no rebuild of the font
chain.

That check also corrected acceptance 17, which asserted `std` would be absent.
It is not — fontdb already enables it via `std = ["ttf-parser/std"]`, upstream
and independent of us. As written the criterion would have failed a correct
implementation, so it is now stated as the differential property that actually
matters.

MathConstants reads only what the Q#MS2 subset needs — axis height, script
scale percent, the two script shifts, and fraction rule thickness. Reading
more would be speculative: constants for deferred constructs have no consumer
to validate them, which is the Q#LX5 discipline applied to metrics. A font
with no MATH table is a typed error rather than plausible-looking zeros, so a
bundled-font regression cannot be silent (Q#MS7).

math_italic implements TeX's convention as the framing's table states it:
ASCII letters and lowercase Greek italic, uppercase Greek upright, digits and
operators unchanged, with U+210E for `h` because the 1D4xx run has a hole
there and arithmetic would land on a reserved codepoint.

Five tests, all against the real embedded bytes rather than fixtures, since B5
is the bet that would sink Tier 3 if false. One goes beyond the framing: every
italic mapping must resolve to a glyph the bundled font actually has, because
a mapping that produced tofu would be worse than the roman fallback it
replaces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 18:27:34 -04:00
Levi Neuwirth 4b1af277e1 docs(math): slice framing rev 3 — close review round 2
Four findings, one of them a real gap.

R2-2 is the substantive one. Rev 2's math-italic fix stopped at ASCII, which
reintroduced the same defect one symbol over: the parser resolves \alpha to
U+03B1, so $\alpha x$ would have drawn an upright alpha beside an italic x —
mixed styles inside a single expression, with the Greek seed map being the
slice's second flagship case after x^2. The mapping now follows TeX's actual
convention and states it as a table: ASCII letters and lowercase Greek in math
italic (U+1D434 and U+1D6FC runs, with the U+210E hole for h), uppercase Greek
deliberately upright, digits and operators unchanged. Acceptance 13 gains
$\alpha$ and $\Gamma$ cases so the uniformity is pinned rather than assumed.

R2-1 was self-inflicted: rev 2's Q#MS3 rewrite added a corrected copy of the
tree-sitter paragraph without deleting the original, leaving the stale rev-1
text immediately below the new one. Removed.

R2-4 sharpens Q#MS11 from "covers" to "intersects". For selections the
distinction is vacuous, but a search match can genuinely half-overlap a span —
searching "2$ af" in "before $x^2$ after" matches from inside the span to
outside it — and the whole-rectangle rule must apply there too, since the box
has no interior byte map to place a partial wash against.

R2-3 moves the follow-up section after prior art; the sections now run 0
through 9 in order.

Acceptance 12 also absorbs a round-2 non-finding: the reviewer's arithmetic
puts \frac{a}{b} near 0.85 and suggests \frac{x^2}{y} also clears the 0.6
floor, so the fallback case has to be computed against the real font rather
than guessed — otherwise the fallback half of the test surprise-passes by
rendering. The criterion now says so and asks for the computed scale to be
pinned.

Framing only; no implementation, no runtime code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 17:57:39 -04:00
Levi Neuwirth 67976dfc5f docs(math): slice framing rev 2 — close review round 1
Nine findings, two of them decisions the implementation could not have
proceeded without, one a compliance error. Two rev-1 claims were wrong rather
than imprecise and are withdrawn where they occur.

F1, the fraction height budget, was hidden inside "a box that would exceed the
line" without saying width or height. Lines cannot grow — metrics are uniform
at 16 px over a 22 px line — and a textstyle fraction does not fit. Q#MS10 now
picks fit-to-line scaling with a 0.6x floor and source fallback below it,
records why overdraw and line growth were rejected, and states the honest
consequence: v0 shrinks nested math uniformly where TeX would use style
levels. B6 makes the floor falsifiable against the flagship case.

F3 was a genuine contradiction: acceptance 10 demanded source-width boxes
while Q#MS4 implied layout-chosen width, and that gap IS the caret-toggle
reflow question. Resolved for layout-chosen, because pinning to source width
leaves every fraction sitting in a large permanent gap, whereas reflow is
transient and tied to a deliberate caret move. Acceptance 10 is rewritten to
bound it rather than forbid it.

F2 withdrew the zero-glyph strut: a RichChunk's only width is its text, so the
mechanism is the SourceTab precedent — projected spacer text, width quantized
to whole advances. B1 is restated against the mechanism that exists.

F5 would have bitten real prose immediately: the rev-1 rule renders "prices
are $5 and $6 today" as math. Pandoc currency guards adopted, spans confined
to one line, both in acceptance.

F6 is factual and a licence matter. Latin Modern Math is the GUST Font
License, not OFL, and is 733,736 bytes rather than ~200 KB — verified against
a local TeX Live copy. The bundled licence file must say GFL, and this becomes
the largest embedded asset in the repo. The parent framing carries the same
error; correcting it is recorded as a follow-up rather than done here, since
the parent is already merged.

F4 names the new shaping-invalidation edge as the #120 trap class, including
that suppression must read the effective caret or flap during optimistic
typing. F7 puts math italic in scope, without which the flagship case renders
a roman x. F8 clarifies that glyph IDs are still resolved internally and that
drawing must pin Attrs to the math family. F9 closes the smaller gaps: the
detection hook wording, an overlay rule for selections and washes, degradation
of deferred display syntax, and folding Char/Symbol.

Framing only; no implementation, no runtime code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 16:07:03 -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 f8020320e2 docs(math): frame the first inline-math vertical slice
The obvious next unit was Tier 2 (parser) plus Tier 3 (layout). This framing
rejects that for the arc's own reason: the substrate lane's Q#LX5 refused to
land the parser ahead of layout because MathNode's shape is only validated by
a consumer, and that argument does not stop at Tier 2. MathBox is only
validated once a renderer consumes it, so Tiers 2+3 with no Tier 4 reproduce
the same objection one layer up.

The unit is therefore thin and vertical: characters, sub/superscript and
fractions, detected, parsed, laid out against a real OpenType MATH table, and
drawn — with breadth deferred to follow-on work against an API that has been
exercised rather than only designed.

The scout also found a load-bearing error in the parent's integration table.
It places math_parse.rs and math_layout.rs in the core crate, which cannot
work: pmacs-gpu depends only on pmacs-protocol and never on pmacs, so a
core-crate parser is unreachable from the frontend that renders it, and
ttf-parser reaches only pmacs-gpu. Both also contradict the parent's own
"instance never learns a pixel" contract and its "pure frontend
responsibility in v0" protocol section. Q#MS1 moves both modules into
pmacs-gpu.

Two decisions buy the slice most of its simplicity. Rendering is suppressed
whenever the caret is inside a span, which makes editing-shows-source and
moving-away-renders mutually exclusive states and removes the
caret-inside-math problem rather than approximating it. And layout positions
characters rather than glyph IDs, which is sufficient for this subset and
explicitly does not generalise: glyph variants are what stretchy fences and
big operators need, and both are deferred.

Acceptance puts every claim that something reaches the screen on real pixels
through the existing headless render seam, because a layout engine wired to
nothing would pass any weaker test.

Framing only; no implementation, no runtime code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 15:15:36 -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 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 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