Commit Graph

243 Commits

Author SHA1 Message Date
Levi Neuwirth ed544fab41 fix(process): arm the setsid dependency, correct the orphan account
Review round 2, three findings.

setsid is util-linux, not coreutils, and the standard `cargo test --lib`
gate must not hard-fail on a tool the README does not declare -- a
minimal or BusyBox container would fail without ever testing pmacs. The
hard assert becomes skip-unless-armed via PMACS_REQUIRE_SETSID, which is
the pattern the silent-skip lane already established, so the test cannot
quietly report `ok` having never run where the tool is guaranteed. CI
arms it on Linux; README declares it. Both arms verified against a PATH
with setsid genuinely removed: unarmed skips with its message, armed
FAILS with the diagnostic.

The durable causal account was wrong, and this corrects it in the
framing, the handoff and the ledger. basedpyright's console script runs
bundled node through `subprocess.run` and WAITS
(nodejs_wheel/executable.py:50, verified in the installed 1.39.6). It
does not exit at spawn. What orphans node is pmacs: `shutdown()` SIGTERMs
the recorded pid -- the Python wrapper -- which dies without forwarding
the signal, leaving node at PPid 1 holding the pipes. The refutation was
already in hand: the initialize handshake succeeds, which a wrapper that
exited at spawn could not have done, and the PPid 1 observation was taken
after shutdown had killed it.

The fix is unaffected -- the deadlock and its bite are unchanged -- but
the parked follow-up changes target: not "tolerate servers that
self-orphan" but "stop orphaning them", i.e. signal the process group
rather than a wrapper pid that swallows the signal. Framing section 5 P2
restated.

Also corrects a stale CI-ordering claim: the handoff said pyright must
stay unarmed until the timeout lane lands, but #195 is this PR's base and
gave every job a timeout-minutes. The one live reason is that CI does not
install basedpyright at all. The ci.yml comment asserting the job has no
timeout-minutes was stale for the same reason and is rewritten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-29 22:54:06 -04:00
Levi Neuwirth 46ac2074c3 Merge remote-tracking branch 'githubsucks/main' into process-teardown-stdin-deadlock
# Conflicts:
#	docs/active-work.md
2026-07-29 22:16:50 -04:00
Levi Neuwirth d5a1dc2660 docs(active-work): record the CI-falsified reproduction and the acc28 flake
Round 2 facts for the lane-4 entry: the dash/bash divergence that
falsified the `<&0` form and how the positive control caught it, the
eleven-suite Bet 2 result, and the evidence that acc28 on macos/lua54 was
a flake -- a rerun of the same job on the identical head, not an
assumption.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-29 21:09:35 -04:00
Levi Neuwirth 36a37f6086 docs(active-work): add the lane-4 process-teardown deadlock entry
Integrated late, immediately before push, per the ledger-contention
rule. Records the measured base, the recovery command, the defect, the
reproduce-first diagnosis method, the full gate table with the
revert-verified bite, and what is deliberately parked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-29 19:29:31 -04:00
Levi Neuwirth 5d32c1edc8 merge: integrate main @ e003b81 (PR #190) into the CI lane
Pure insertion: main's ledger taken whole with this lane's 3a block
placed before the parked lane. Verified against both parents --- zero
lines removed relative to main, the lane-3a block byte-identical to
this branch's, no conflict markers, and the update-protocol rule 6 seam
check clean.

Deliberately NOT absorbed here, because absorption is not mechanical
and this PR is approved on its current content. `main` now carries
THREE lanes describing merged PRs: the resource-op delete guard
(#190), the generated-buffer immutability framing (#188), and this
arc's own silent-skip arming (#194). Rule 4 forbids relabelling any of
them and permits removal only once their durable facts reach
`docs/agent-handoff.md` --- and none of the three PRs touched that
file, so all three absorptions are genuinely owed rather than
overlooked.

Two belong to other arcs. The third (#194) belongs to this one, and
its durable fact is not yet written down anywhere: that
`PMACS_REQUIRE_*` arms an otherwise-vacuous skip, and that
basedpyright stays unarmed until the reader-join hang and the CI
timeouts both land. That wants a handoff bullet, which is content
rather than a merge resolution.
2026-07-29 15:10:22 -04:00
Levi Neuwirth 2554bfcbb9 review round 2: arm the required-checks name-coupling trap, restore m6
P3 --- required status checks are name-coupled to job names, and a
required context that no longer exists does NOT fail. It leaves every
PR pinned on "Expected --- waiting for status" forever, which is `main`
becoming unmergeable by policy rather than by a red run. Three of this
lane's own deferrals will do exactly that: the macOS matrix trim
removes two contexts outright, and nextest or the serial/parallel split
rename or add jobs.

The rule is now in the ledger entry --- any job rename, removal, or
matrix change updates the branch-protection required-checks list in the
same motion --- and it is recorded HERE deliberately, because this is
the single entry that both enabled protection and named the lanes that
will invalidate it. Arming the warning anywhere else would separate the
trap from the thing that sets it.

P4 --- the rewritten top comment said "everything else keeps 25 against
a sub-4-minute observed max" and dropped the clause noting that
`m6-perf-gates` keeps its own tighter 15. Restored. Worth the fixup in
a change whose entire subject was comments matching reality.

Beyond the PR, and taken here rather than deferred: `TEST_IMPROVEMENT.md`
on `main` still said "no branch protection on `main` (verified via API:
404, so every job is advisory)" and listed §5.1 as open. Both went
stale during this session, and THIS lane is what made them stale, so it
carries the correction rather than leaving it for whoever touches the
file next. Struck through in both places rather than rewritten: the 404
was a true reading at audit time, and the document is the arc's scoping
record, so what changed is more useful than a clean-looking present
tense. Note also that protection shipped wider than §5.1 proposed ---
all 12 contexts required, not the cheap-jobs-only starter --- which the
correction states.

Verified: YAML parses; the seam check from update-protocol rule 6 finds
no double blanks; `git diff --check` clean.
2026-07-29 14:16:27 -04:00
Levi Neuwirth 63c5545979 review round 1: anchor the ceilings on observed execution, record §5.1
P1 --- the ceiling was justified against the wrong number. Revision 1
cited "~14.6 min, ample headroom", which was one reading quoted as a
property, and this ledger's own rule applies to it: a census is a
reading, not a constant. Re-measured over two windows --- 17 min max
over 25 runs, 15.8 over 12, both macOS/luajit, every other job under
4 --- so a flat 25 was about 1.5x the observed tail, not "ample".

Two facts shape the fix. `timeout-minutes` counts EXECUTION, not queue,
so the 33-minute wall-clock run in that window executed its longest job
in 17 and no run in observed history would have been killed by either
value. And the real exposure is the case no window contains: a cold
cache. A stable-toolchain bump invalidates Swatinem's key on every leg
at once, and a cold macOS debug build plus suite is the plausible way a
HEALTHY run overruns --- presenting as four legs timing out
simultaneously the day after a Rust release.

So the test job takes 35 (~2x its observed max) and the rest keep 25
(~6x theirs), and the diagnosis is written into the workflow BEFORE the
event: simultaneous four-leg timeouts after a toolchain release are a
cold cache, not a hang; a single leg timing out beside passing siblings
is the hang case these ceilings exist to catch. 35 still beats the
360-minute default by an order of magnitude, so the basedpyright
arming this gate unblocks is unaffected.

P2 --- §5.1 was missing from both lists, and review was right that the
omission matters. But its premise had gone stale, which is worth
recording rather than quietly working around: branch protection is ON.
It was enabled earlier in this session, and I re-verified against the
API rather than trusting either the review or my own memory of doing
it:

    {"enforce_admins":false,"force_push":false,
     "required_checks":12,"strict":false}

Recorded in the ledger as DONE with the settings and the reasoning for
each --- `strict` off so a PR need not rebase every time `main` moves,
`enforce_admins` off so the user keeps an override. This also settles
the concurrency comment, which justifies exempting `main` pushes by
appeal to "the branch-protection record": that record exists, so the
justification is real rather than aspirational, and no softening is
needed.

P3 --- the double blank line before the parked lane, third PR running.
Fixed, and added to the ledger's own update protocol as step 6, since
fixing the instance three times has not stopped it: a block ending in a
blank line inserted above a heading already preceded by one leaves the
seam, and it survives review by sitting beneath the level anyone reads
at. The rule now names the check.

Verified: YAML parses; ceilings are 25 except test at 35 and
m6-perf-gates at its tighter 15; the seam check finds no double blanks
anywhere in the ledger; `git diff --check` clean. Workflow and ledger
only.
2026-07-29 14:12:47 -04:00
Levi Neuwirth ef5bd89e84 merge: integrate main @ b7bf2c6 after PR 190 review round 2
Keep both active-work lanes while taking the silent-skip arming and
generated-buffer framing changes from current main. The resource-op lane
retains its round-2 fixes and updates its recorded merge-base.
2026-07-29 13:49:06 -04:00
Levi Neuwirth 156567b5dd docs: record PR 190 review round 2
Record the path-normalization and partial-first-operation fixes, their
acceptance criteria and bite pre-images, and the green round-2 gate
results in the active-work ledger.
2026-07-29 13:32:08 -04:00
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 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 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 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 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 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 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 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 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 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 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 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