The diff channel's missing ticket, and --- because this was the fourth
recurrence of one shape --- a census of every async continuation in
`git.lua` rather than another instance-by-instance note. Three
continuations, one dispatcher, one synchronous impostor, each with
whether it carries an invocation-time ticket, whether it needs one, and
what shared state it writes.
Also states why the fix is two channels sharing one mechanism rather
than one shared counter: a single counter would make `d` cancel an
in-flight `g`. And records that `state.diff_buffer` is deliberately
still read at continuation time --- "do I already have a live diff
buffer?" is a question about now, not about the invocation --- so it is
not a fifth instance.
P1a's citations re-pointed at `723afa7` and the untouched claim
tightened from "no diff line reaches these names" to something
checkable: `show_diff_buffer`'s body and `open_status_panel`'s
`listview.open` are byte-identical to `4002734`, and no commit on this
branch adds a `commit_to` call anywhere.
Section-local: nothing outside this lane's entry is touched or
reflowed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The durable half of this lane is a boundary question, not the missing
line. `scripts/gate`'s header names handoff section 3 as the owner of
its reasoning, and this precondition lived in section 5's hazard
register --- a coherent cause for the omission rather than mere
oversight. A requirement the script was never told to encode is one it
will keep not encoding.
So section 3 gains it NORMATIVELY: the build joins the protocol-bump
block as a third line, with its own load-bearing bullet covering the
mechanism (pmacs-gpu has no tests/ directory, so cargo never uplifts
its bin), the measurement that makes it conditional, and why it was
latent until per-worktree target directories stopped hiding it.
Section 5 keeps the INCIDENT and its signature, which is history rather
than contract, and now says so: twelve
`gpu_invocation_acceptance::crdt::*` failures on a target directory
with no `debug/pmacs-gpu`, first seen on PR #228's first gate run.
Recast so that seeing the signature again reads as "the script was
bypassed", not "the requirement moved".
The script's header keeps citing section 3 and ONLY section 3. Citing
both would split one executable contract across two homes and weaken
the script's only clean boundary at the same time as Q#GR-4 declines to
build any automated check for prose drift. A boundary that is neither
enforced nor singular is not a boundary.
The ledger records Q#GR-1's observed answer rather than the question:
both sweeps run alone from the same cold disposable target with
`debug/pmacs-gpu` asserted absent beforehand --- default exit 0 with
the binary still absent afterwards, crdt exit 101 with exactly twelve
failures --- plus the silent-skip finding, which is the part nobody was
looking for: a54 reported `ok` in that cold crdt sweep because its only
non-spawning path is its skip branch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two more P2s, both the same shape as round 1's P1 --- module-level
mutable state read at continuation time instead of captured at
invocation time --- plus the third instance of that shape, which is
still open and which no review finding covers: the diff path has no
generation counter at all, so two `d` presses in flight together are
last-writer-wins on the single `*git-diff*` buffer.
Also corrects a citation round 1 got wrong. The P1a block names two
lines that must not be touched, and its second one (`:854`) pointed at
`local unstaged = …` inside `diff_plan`, not at a display call. The site
was always `show_diff_buffer`'s `pmacs.window.display`. A stale pointer
in a block whose entire purpose is "leave these alone" is worse than
none, so it is corrected rather than silently re-numbered.
Section-local: nothing outside this lane's entry is touched or
reflowed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two findings, both about a claim that could not be falsified.
The --self-test plan put the failing step last. With the failure last,
a runner that ABORTS on failure and one that CONTINUES produce
identical output, so the witness for Q#GR-2 policy --- the suite keeps
going --- would have passed on a runner doing the exact opposite. The
plan is now three lines with a passing SENTINEL after build-crdt,
asserted to have written its own log. That is the only thing that
distinguishes the two behaviours, and it turns Q#GR-2 from a declared
policy into an observed one.
The plan test also now pins the EXACT command, not only the step name
and its position. A build-crdt running plain cargo build would leave
the gate exactly as unsound while looking repaired --- the crdt sweep
needs those specific features, which is the whole defect.
The ledger still recorded the superseded boundary decision: "section 3
gains it, section 5 keeps the incident, and the script cites both".
Revision 2 replaced that with section 3 as the sole normative home and
the script citing section 3 alone. active-work.md is the volatile
cross-machine record, so a recovering machine reading the stale entry
would have rebuilt revision 1 wrong boundary. Now updated, and it says
which decision it supersedes rather than silently replacing it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The lane said "normal review; no merge authorization", which is no
longer the state: two of three blockers are fixed and the third makes
the PR merge-blocked behind the destination-capture lane.
P1a is recorded as deliberately NOT fixed rather than outstanding.
commit_to is the right mechanism and is not Lua-reachable outside a
directory open --- DirectoryDestinationLua is nonconstructible by
design and minted only in the path.open-directory dispatch --- so the
fix is a prerequisite lane and this one adopts it afterwards. Recorded
with the mechanical check that no diff line in either fix reaches the
four named symbols, so a later reader does not have to take it on
trust.
The P2 entry keeps the reasoning for teardown over a canonicalized
preflight, because the rejected option is the one that looks obviously
better: a Lua canonicalizer would be a second copy of the Rust alias
table and would go stale the day that table gains a name,
reintroducing this exact bug for the new alias. Keymap::bind is the
authority because it is what decides. Also recorded: there is no
Lua-reachable canonicalization to build on, verified, and no binding
was added to invent one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Three review findings.
The normative build requirement goes entirely into handoff section 3.
Revision 1 proposed section 3 gaining it while the script header cited
both sections, which splits one executable contract across two homes
and weakens the single clean boundary the script has --- at the same
time as Q#GR-4 declines to build any automated check for prose drift. A
boundary that is neither enforced nor singular is not a boundary.
Section 5 keeps the incident and its signature, which is history rather
than contract.
Q#GR-1 observation procedure was unsafe and insufficient. "Delete
pmacs-gpu from a target directory" mutates a live worktree build
directory, and removing one binary does not establish that the other
artifacts and feature permutations are cold --- a stale dependency
graph can satisfy the run for reasons the experiment never sees. Now: a
disposable target, the binary asserted ABSENT before each run as a
recorded precondition, and the two sweeps run separately so neither can
be explained by the other having built the binary first. That last
point is the same accident that hid this defect for the whole life of
the shared target dir.
The attribution criterion had no feasible witness. gate_script_acceptance
deliberately runs no gates, so plan assertions prove name and order and
nothing about runtime behaviour. The obvious seam is a trap: making
PLAN_FILE injectable would turn the script into a general command
executor through its runner eval --- the same class of defect this
script own review already caught in --acceptance and fixed with a
parse-time refusal. Reintroducing it one lane later, in the tool whose
purpose is to be trustworthy, is not a trade worth making.
Q#GR-5 proposes --self-test over a HARDCODED two-line synthetic plan,
true and false, with the failing one named build-crdt. No injection,
no real gate, and it tests the thing actually under test: whether the
runner names the right gate when a command fails. Whether cargo build
really fails is cargo business.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
--protocol promises the CRDT workspace sweep. That sweep documented
precondition is cargo build --workspace --no-default-features
--features luajit,crdt (handoff section 5:532-535), and the plan
emitter at scripts/gate:187-204 has no build step at all --- read from
the source, not inferred from the failure.
The interesting part is why it stayed invisible. Before #225 every
worktree on this machine resolved to one shared CARGO_TARGET_DIR, which
almost always already contained a pmacs-gpu binary, so the precondition
was satisfied by accident on essentially every run. Per-worktree target
dirs start empty. So this is not a bug #225 introduced; it is a
pre-existing gap in the documented procedure that #225 stopped hiding.
That also decides the urgency. A red gate is fine --- it stops you. The
hazard is the reverse: a GREEN --protocol run whose crdt sweep was
decided by what happened to be in the build directory rather than by
the diff. A gate reporting coverage it does not have is exactly what
#225 exists to prevent, so the tool shipping with this gap teaches the
opposite of what it is for.
Observed on PR #228 first gate run: twelve
gpu_invocation_acceptance::crdt::* failures, all "build pmacs-gpu
before this acceptance suite", with debug/pmacs-gpu absent from the
fresh target dir.
The durable half is a boundary question rather than a missing line. The
script header names handoff section 3 as the owner of its reasoning,
and this precondition lives in section 5 --- a coherent cause for the
omission, not oversight. Q#GR-3 proposes section 3 gains it, section 5
keeps the incident and its signature, and the script stops naming
section 3 as its only source.
Q#GR-1 is marked as the one thing this lane will not accept on
reasoning: whether the default sweep also needs the binary must be
established by deleting it and running both sweeps. The whole defect is
a precondition nobody checked, and establishing its replacement by
reading would repeat the error at one remove. The mechanism section
states its own inference (the failing tests are namespaced ::crdt:: and
so are probably feature-gated) and marks it unverified.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The lane heading still said the PR was not opened. Per the standing
correction from #171 and #215, the PR number belongs in the ledger when
the PR exists, not when review asks for it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Implements docs/git-integration-framing.md revision 5 (approved
2026-08-09). COHERENCE.md section 15's largest named gap gets something
to attach to: a user can now answer "what have I changed?" without
leaving the editor.
*git-status* a `pmacs.listview` panel over
`git --no-optional-locks -C <root> status
--porcelain=v2 --branch -z`. RET visits the file, `d`
shows its diff, `g` refreshes.
*git-diff* the file-level diff, in a generated buffer rendered as
plain text --- there is no bundled `diff` grammar and no
hunk model anywhere in the tree.
NO WIRE CHANGE: no pmacs-protocol edit, no PROTOCOL_VERSION bump, no
DecorationKind variant. That is load-bearing for scheduling, not a
coincidence --- gutter markers (Stage 2) need all three and must be
scheduled alone, while this lane could run beside another.
One additive `listview` change, and the framing was wrong to say there
would be none: an optional `keys` table on the open spec. `d` cannot be
bound from outside the primitive safely, because a name collision
disambiguates to `<2>` and the name a consumer passed is not necessarily
the buffer it got. Keys are INSTALLED ONCE with the panel's buffer and
COMPARED on reopen: `Keymap::bind` refuses duplicates, and the async
completion model re-opens on every refresh, so a naive implementation
would have errored on every successful refresh.
One config-registry setting, `git.enabled`, through `pmacs.config.define`.
Facts measured against real git rather than reasoned about, each pinned
by a test:
* `ProjectKind::Git` means a BARE repository, and a language marker
beside `.git` wins --- so pmacs reports `kind = "rust"` for its own
repository. This module never asks pmacs whether something is a repo;
it runs `rev-parse --show-toplevel` and lets a non-zero exit answer.
* `git diff --no-index` implies `--exit-code`: exit 1 means it
SUCCESSFULLY found differences. The untracked predicate is exit in
{0,1}; only >= 2 is failure. Under the naive predicate every untracked
diff --- the case `--no-index` exists for --- would render a failure.
* An unborn HEAD makes `git diff HEAD` exit 128. Detected from
`# branch.oid (initial)` in output already being parsed, never from a
second `rev-parse`. `AM`/`AD` carry both states and get two labelled
patches; rename/copy is asserted UNREACHABLE, because `git mv` on a
staged-but-uncommitted file yields `1 A.`, not a `2` record.
* Under `-z` a rename's origin is the NEXT NUL-terminated field, not a
tab-joined suffix, so the record tokenizer is new rather than ported
from `tests/fixtures/pmacs-magit/`. What ports is that fixture's
SEPARATION --- pure `parse_*` over a string --- and its case coverage.
The fixture is untouched: it exists to prove the package system can
host this, and bundled code becoming its dependency would make
`m8_6_acceptance` test less than it claims.
Coherence impact, stated per CLAUDE.md:
* Section 14: `*git-status*` is the FIFTH `listview` call site and the
first outside `lsp.lua` --- the evidence P5 asked for that the
primitive generalizes past its first consumer.
* Section 6: no new interaction island. `d` is an ordinary buffer-local
binding through the primitive's own path, so `describe-key` reports
the truth and `init.lua` can rebind it. The count stays at six.
* Section 9: NEGATIVE, and named as such. A spawned process does not
appear in `*workers*` --- that view is `async.lua`'s job list. This
adds a fifth background thing with no single place to see it. Every
spawn is labelled, which is better than anonymous, but a label is not
attribution. Accepted only because these are short-lived reads.
* Journey: no step added. Git is not a journey step and this does not
make it one.
Section 15's "no Git integration at all ... anywhere in the tree" is
narrowed here. It was literally false when written ---
`tests/fixtures/pmacs-magit/` is a tracked, installable package that
spawns git and parses porcelain v2 --- and the product gap it described
is what this closes.
Five things found by biting the suite rather than by reading, recorded
in docs/active-work.md: `listview.open`'s `seat_cursor` walks DOWN from
wherever the cursor is (so a re-opened panel lands one row low, and the
completion handler seats unconditionally from line 0); a selection test
that inserts ONE row above the selection is vacuous against exactly that
off-by-one; `{:?}` on a Rust string cannot build a `-z` fixture, because
Lua's decimal escape swallows the digit after `\0` --- which made one
test pass while parsing nothing; a path may contain a newline, so rows
escape it; and untracked rows sort after every tracked row.
Gates: scripts/gate --acceptance git_status_stage1_acceptance
--acceptance listview_acceptance --acceptance config_registry_acceptance.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
COHERENCE.md section 15 grades contextual affordances "weak" and says
the Git affordance list "has nothing to attach to yet". For a daily
driver this is the largest remaining gap --- not the deepest (sections 7
and 9 are), but the one a user touches every working hour.
Stage 1 is read-only and panel-based: a `*git-status*` listview over
`--porcelain=v2 --branch -z`, and a file-level `*git-diff*` in plain
generated text. THE STAGE LINE FALLS AT THE WIRE, and that is a
scheduling decision as much as a design one: `DecorationKind` is a
closed enum, so gutter markers need new variants and a
PROTOCOL_VERSION bump. Bumps are a strict serialization point --- this
session recorded eight broken assertions from one --- so Stage 1
touching no wire is what lets it run beside other lanes, and Stage 2
must be scheduled alone.
FOUR REVIEW ROUNDS, and the doc records what each one caught, because
the pattern is the useful part:
- `ProjectKind::Git` means a BARE git repo; a language marker beside
`.git` wins, so this very repository reports "rust". A `kind ==
"git"` gate would have failed on the repo it was written in. The
rule is now: never ask pmacs whether it is a git repo --- run git
and let it resolve its own worktree.
- `tests/fixtures/pmacs-magit/` already exists: 1,914 lines, a
porcelain-v2 parser, 32 tests. Section 15's "no Git integration
anywhere in the tree" is literally false; the PRODUCT gap is real.
The tokenizer is deliberately REWRITTEN for `-z` rather than
ported --- newline-delimited and NUL-delimited v2 are different
grammars.
- `listview.open` resets collapse and always seats line 1, so
selection preservation is the consumer's job, not the primitive's.
And `d` is not on its key surface; binding it needs an additive
`keys` table, which makes "no listview modification" false.
- `Keymap::bind` REFUSES duplicates, and the refresh path re-opens
the panel --- so a naive `keys` implementation would have failed on
every successful refresh.
Two git exit states were measured, not assumed. `--no-index` implies
`--exit-code`, so an untracked diff exits 1 ON SUCCESS --- under the
first predicate, every untracked diff would have rendered a failure row
instead of the diff it had just produced. And `git diff HEAD` exits 128
in an unborn repository, which is exactly a fresh `git init` with the
first files staged.
The unborn policy was then enumerated from a real unborn repository
rather than reasoned about, which closed one case by RULING IT OUT: a
`git mv` of a staged-but-uncommitted file emits `1 A.`, never a `2`
record, so rename/copy is unreachable without a HEAD and needs no
policy. `AM` and `AD` are ordinary there and carry both states, so they
render TWO labelled patches --- `--cached` alone loses the worktree
edit, plain `git diff` alone loses the staged base. The split is
unborn-only: with a HEAD, one total is the question this lane asks.
Section 9 impact is recorded as NEGATIVE and not dressed up: spawned
processes do not appear in `*workers*`, so this adds a fifth
unattributable background thing. The process is labelled; a label is
not attribution.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* build: scripts/gate — a target dir per worktree, and one gate suite
Parallel worktrees do not work on this machine, and the reason is one
exported variable: every checkout builds into one CARGO_TARGET_DIR, and
cargo takes an EXCLUSIVE LOCK on it. Two lanes building at once do not
run in parallel — the second blocks — and they invalidate each other's
artifacts, so alternating between them recompiles from scratch. Parallel
development under that arrangement is slower than serial.
MEASURED, BECAUSE THE FIRST PLAN WAS WRONG. The shared directory is
285G, which drove a proposal to add sccache so per-worktree directories
would not lose artifact sharing. That number is years of accumulation
across TWO projects (pmacs and levcs share it). Measured directly: a
cold `cargo test --workspace --no-run` is 80s and 19G. And sccache
across two target directories hits 50% on C/C++ and **0.00% on Rust** —
rlibs embed their target-dir path, so dependency artifacts are not
bit-identical between directories and `--extern` hashes cascade into
misses. There is no sharing worth buying back. sccache stays configured
and earns its keep on C/C++; it is not what makes parallel lanes work.
The script also owns the FIXED gates, because a procedure living only in
prose gets executed differently each time — twice in the session that
motivated this:
- a sweep run with `--tests` instead of `--workspace`, silently
dropping pmacs_protocol and pmacs_gpu, including protocol tests that
same lane had just written;
- a sweep piped through `grep` before anyone read it, so an
intermittent red could not be matched against ci-red-signatures —
a row needs its fragments. That is registry note U2, and then U3
when it happened AGAIN.
Hence durable per-gate logs with the sweep paths printed. The remedy is
real: this lane's own run diagnosed its failures from the log without
re-running anything.
WHAT THE SCRIPT IS NOT AUTHORITATIVE FOR. Handoff §3 keeps policy and
keeps CHOOSING the touched acceptance suites, which arrive only via
`--acceptance`. No script can infer those from a working tree, and one
that guessed would report coverage it does not have.
THREE HAZARDS SPECIFIED RATHER THAN LEFT TO CHANCE:
- `cmd | tee log` reports TEE's status, so a failing gate would exit 0
and the suite would read green. `pipefail` is not POSIX.
- `cmd > log; rc=$?` never reaches the assignment under `set -eu`
(which scripts/bite already uses) — the shell exits at the failing
command, so nothing prints which gate failed or where its log is,
destroying the point of capturing it. The runner is therefore an
`if` condition, the only `set -e` exemption.
- CARGO_TARGET_DIR (env) OVERRIDES build.target-dir in config.toml, so
a per-worktree config file silently does nothing. Only a
per-invocation value beats it.
Pruning is dry-run by default, `--force` to delete, and refuses any
directory without a `.pmacs-gate-target` marker. "Live" means a git
worktree record carrying NO `prunable` line — git keeps listing a
worktree whose directory was deleted without `git worktree remove`, and
treating listed as live would make exactly the reclaimable directories
permanently ineligible.
ONE HONEST FINDING FROM MUTATION TESTING. Three mutations came back
vacuous, and all three are redundant defences rather than test holes:
git already returns resolved physical paths from both
`rev-parse --show-toplevel` and `worktree list --porcelain`, so canon()
is belt-and-braces; and the prune path guards the marker twice. Recorded
in the script and the tests so a later reader does not mistake a
"vacuous" result for a gap — or delete a defence because a test did not
notice.
VERIFICATION. 11 acceptance tests over the no-gates paths (running the
script for real inside the suite would recurse), each pointed at a
tempdir via PMACS_GATE_TARGET_ROOT so the real managed root is
unreachable — a prune bug is unrecoverable. Mutation-tested: `--tests`
in the sweep, an unconditional CRDT sweep, and pruning on a dry run all
fail their intended test.
Observed in a real run, which is how the framing said to confirm the
parts a test cannot: the failed-gate names and log paths print, the
ambient directory is created and reaped by the exit trap, and every log
appears. The run exits non-zero because of R8 — the pre-existing,
merge-base-confirmed listview failure — which means `scripts/gate`
cannot go green on this machine until R8 is diagnosed. That is a
property of the tree, not of this change.
Framing: docs/gate-script-framing.md (revision 4, approved).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* fix(gate): two ways the script could do harm, and four smaller defects
Review round 1 on #225. Neither blocking finding was a design gap ---
both were the implementation failing to honour its own framing, which
is the case a framing document cannot prevent by itself.
PRUNE COULD DELETE EVERY MANAGED DIRECTORY. §2.6 requires the live
worktree set to be ESTABLISHED. The code piped `git worktree list`
straight into awk and the caller masked the result with `|| true`, so
running from outside any repository produced an EMPTY live set --- and
an empty live set means "every managed directory is an orphan", so
`--prune --force` would have deleted all of them, live lanes' artifacts
included. The failure mode was silent and total.
Two refusals now, and they are deliberately redundant: not inside a
worktree, and the enumeration itself failing. `live_worktrees` captures
git's output and returns non-zero rather than emitting nothing, so
"I cannot tell what is live" is unrepresentable as "nothing is live".
An empty porcelain listing counts as failure too --- a repository always
has at least its own worktree.
--ACCEPTANCE WAS SHELL-INJECTABLE. The name is interpolated into a
command the runner evaluates, and nothing validated it, so
`--acceptance 'x; rm -rf ~'` would have run. Now an allowlist of what a
cargo test target can actually be named --- letters, digits, underscore,
hyphen --- refused at parse time, before any gate. Rejection rather than
escaping: there is no legitimate suite name that needs quoting.
FOUR SMALLER ONES:
- Log directories carried a whole-second timestamp, so two runs in the
same worktree within one second shared one and could overwrite each
other's evidence --- reintroducing U2/U3 through a naming choice.
The PID is now part of the name.
- The ownership marker is DOCUMENTED as one line, so it is enforced as
one line instead of read head-first. Acting on the first line of a
file we did not understand is how a corrupted marker authorises a
deletion.
- The `prunable` test returned green when `git worktree add` failed,
so the only coverage of that rule could silently never run. It now
fails loudly.
- Its cleanup ran after the assertions, so a panicking assertion would
have left the real repository carrying a stale worktree record. Now
a `Drop` guard.
MUTATION TESTING, HONESTLY REPORTED. The injection and marker fixes bite
individually. The two prune guards do NOT --- each alone satisfies the
outside-repo test, so mutating one at a time reads as vacuous. Removing
BOTH fails the test, which is what establishes that the test detects the
unsafe state rather than being blind to it. Recorded in the test so a
later reader does not delete one guard on the grounds that nothing
noticed.
ALSO: handoff §3's ambient-root caveat still said "until the
ambient-root isolation lane lands". #206 merged; the five variables are
now belt-and-braces for external and integration paths, and `scripts/gate`
sets them regardless.
R8 PROMOTED. `docs/ci-red-signatures.md` gains the reason it stops being
a catalogued curiosity: with the gate suite reduced to one command, R8
makes that command exit non-zero on a clean tree EVERY TIME, and a gate
that is always red is a gate nobody reads. `docs/active-work.md` gains a
lane. It is still not a regression from #223 or #225 --- the merge-base
control says so --- and the lane's first job is diagnosis, because a
change that made the assertion pass without explaining the prefix strip
would convert a visible failure into an invisible one.
15 acceptance tests. Observed run re-confirmed: failed gates named with
log paths, ambient directory created and reaped, distinct log directory,
exit 1 from R8 alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: the #225 lane, and R8 diagnosed to a stray /tmp/.git
TWO LEDGER GAPS, both found by review.
and — the part that matters — an explicit GATE STATUS: NOT GREEN
section. `scripts/gate` exits 1 on this branch and on a clean `main`
because R8 fails m4_acceptance and therefore the sweep. That is a merge
blocker under the standing rule, and #225 is the worst possible lane to
grant a silent exception to: it is the lane that makes the gate suite
authoritative, and a tool shipping with its own gate red teaches the
opposite of what it exists to teach.
The lane also records that it was written after the PR existed, again,
because review asked again. Two lanes in a row now. The correction from
only evidence of that.
R8 DIAGNOSED, and the `TMPDIR` hypothesis was right:
1. `display_path` (builtin/runtime/lsp.lua:2397) shortens a location
against the DETECTED PROJECT ROOT before rendering it.
2. `project.detect` walks UPWARD for a marker; from
/tmp/.tmpXXXX/r.rs it reaches /tmp.
3. This machine has a stray `/tmp/.git` — an EMPTY DIRECTORY, not a
repository. The `.git` marker is directory-only, so an empty
directory still matches.
4. Root resolves to /tmp, the prefix is stripped, and the rendered row
is exactly the observed `.tmpXXXXXX/r.rs:12:3`.
Controlled, not inferred: the same test with TMPDIR outside /tmp PASSES.
THE CODEBASE ANTICIPATED THIS BY NAME. src/project.rs:208 documents
`detect_project_within(start, markers, stop_root)` as existing "so a
stray marker in a temp-dir's ancestor (e.g. a developer's /tmp/.git)
can't leak into a fixture that lives below it." The mechanism exists;
this fixture does not use it.
So the row splits, and the halves need different fixes. The failure is
ENVIRONMENTAL — nothing about pmacs is wrong when a real project root
sits above a file, that is the feature, and removing /tmp/.git makes the
gate green immediately. The fixture being ENVIRONMENT-DEPENDENT is a
real defect, and bounding its detection is what retires the row.
PROVENANCE UNRESOLVED, and I am not going to assume in my own favour:
/tmp/.git is dated 2026-08-07 23:17, inside this session's window, and
may have been created by this session's own work — a stray git
invocation from /tmp would do it. The earlier merge-base control stays
valid as "this tree has it" but says nothing about WHEN the environment
acquired the marker, so "pre-existing" must not be read as
"long-standing".
Nothing deleted: /tmp/.git is outside the repository and I cannot
confirm I created it, so removing it is the user's call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: rebase onto the R8 fix; scripts/gate now exits 0
#226 (`dcb852e`) retired R8 by bounding the LSP fixture's project
detection. This branch rebases onto it, and the thing that was blocked
is now demonstrable: **`scripts/gate` exits 0** --- all nine gates green
in one command, the first time the tool has passed the suite it exists
to run. That is #225's own acceptance criterion, and it could not even
be stated while the script did not exist on `main`.
REBASE RESOLUTION, per the standing rule that #226's R8 documentation is
authoritative. Every conflict was in R8 text this branch wrote while the
row was still an open investigation:
- two in `docs/ci-red-signatures.md`, both resolved to #226's retired
row with this branch's pre-fix copy dropped;
- the framing-doc pair --- e71e1bd added `docs/r8-fixture-boundary-
framing.md`, 7cfba73 removed it --- both SKIPPED. They are net-zero
here and `main` owns that file authoritatively; replaying the second
would have deleted `main`'s copy, which is the one failure mode a
mechanical "resolve each conflict in turn" would have walked into.
TWO STALE LANES REMOVED. This branch's "R8 --- NEEDS A LANE"
investigation block describes a diagnosis that has since happened and a
fix that has since landed. And #226's own lane arrived through the
rebase still saying "OPEN, HELD FOR REVIEW"; Rule 4 retires it now that
it has merged, its durable facts already being in the retired registry
row and the handoff section 6 census. Leaving either would have left the
ledger asserting that a merged fix was still an open investigation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: frame the R8 fixture-boundary fix (revision 2)
R8 fails m4_acceptance deterministically on one machine, and the
diagnosis is that the fixture never bounds its own project detection.
`display_path` (builtin/runtime/lsp.lua:2397) shortens a location
against the DETECTED PROJECT ROOT. `pmacs.project.detect` walks upward
for a marker; from /tmp/.tmpXXXX/r.rs it reaches /tmp, where this
machine has a stray EMPTY `.git` directory. The `.git` marker is
directory-only, so an empty directory matches, the root resolves to
/tmp, and the prefix is stripped.
THE PRODUCT BEHAVIOUR IS CORRECT AND IS NOT CHANGING. Shortening a
location against its project root is the feature. The defect is that
the fixture's assertion depends on whether the developer's /tmp happens
to contain a `.git`.
THE MECHANISM ALREADY EXISTS AND THIS SUITE ALREADY USES IT.
`src/project.rs:208` documents `detect_project_within(.., stop_root)` as
existing "so a stray marker in a temp-dir's ancestor (e.g. a
developer's /tmp/.git) can't leak into a fixture that lives below it."
It is exposed to Lua as `pmacs.project.set_search_boundary`; eight test
files make fourteen real calls to it, five of them in m4_acceptance
itself --- one carrying that same hazard as a comment. `open_against_fake`
(tests/m4_acceptance.rs:7985) is one helper that missed the pattern.
THE WITNESS PLANTS ITS OWN HAZARD, so the proof is not a property of
this machine: an empty `.git` in a temporary ancestor, the file one
level below, boundary at the file's parent. With the boundary the row
renders absolute; reverting it strips the prefix deterministically on
every machine, including CI where /tmp/.git does not exist. The
/tmp/.git observation stays as corroboration, not as the bite.
`scripts/gate` is deliberately NOT a criterion: this lane branches from
main, where that script does not exist (it is unmerged on #225). Naming
it would make this lane depend on an artifact absent from its own base.
R8 lands first on its own merits; #225 then rebases and takes "gate runs
green" as ITS criterion.
Q#R8-1 records a limitation rather than discovering it later:
parent-as-boundary is correct only while fixtures put the file as a
direct child of the fixture root. A future nested fixture cannot fix
itself by passing a deeper path --- the boundary is DERIVED from the
parent, so a deeper path clamps sooner, never later.
Provenance of /tmp/.git is left permanently unresolved, and the document
says why no timestamp is treated as authoritative.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* fix(tests): bound the LSP fixture's project detection — retires R8
`open_against_fake` never set a search boundary, so the panel tests'
rendered paths were shortened against whatever project root detection
found ABOVE their tempdir. On a machine with a stray `/tmp/.git` that
meant `/tmp` --- and the assertion that spells a path out failed
deterministically. Registry row R8.
THE PRODUCT BEHAVIOUR WAS NEVER WRONG AND IS NOT CHANGED. Shortening a
location against its project root is the feature; a file that really is
inside a project really should render relative to it. What was wrong is
that a fixture's assertion depended on the contents of the developer's
/tmp.
THE MECHANISM WAS ALREADY THERE. `src/project.rs:208` documents
`detect_project_within(.., stop_root)` as existing "so a stray marker in
a temp-dir's ancestor (e.g. a developer's /tmp/.git) can't leak into a
fixture that lives below it" --- naming this exact hazard. It is exposed
to Lua as `pmacs.project.set_search_boundary`, eight test files make
fourteen real calls to it, and five of those are in this same file, one
carrying that hazard as a comment. This was one helper that missed a
pattern its own file already used.
THE WITNESS PLANTS ITS OWN HAZARD, so the proof is not a property of one
machine. `a_planted_ancestor_marker_does_not_reach_the_rendered_row`
creates an empty `.git` in a temporary ancestor with the file one level
below, and asserts the row stays absolute. Reverting the boundary fails
it with `proj/r.rs:12:3` --- relative to the PLANTED marker, not to
/tmp, because the nearer ancestor wins. That is what makes it bite in
CI, where no /tmp/.git exists; confirmed by also running it with TMPDIR
outside /tmp.
Resting the bite on /tmp/.git would have been the same mistake as a test
that passes only where the developer happens to be standing.
/tmp/.git IS DELIBERATELY LEFT IN PLACE. Deleting it would hide the
hermeticity defect rather than fix it, its provenance is unresolved, and
it is the only thing on this machine that reproduces the row --- which
makes it useful, not merely untouchable. The R8 fix is verified WITH it
present.
VERIFICATION. The R8 test passes on the machine that reproduces it. Full
m4_acceptance 151/0. `--lib` 1920, `--lib --features crdt` 2105,
`-p pmacs-gpu` 241, fmt, clippy, `git diff --check`. The full workspace
sweep exits 0 across 113 targets --- the first fully green local sweep of
this session, R8 having been the only obstacle.
`scripts/gate` is deliberately not a criterion: this branches from main,
where it does not exist. #225 rebases onto this and takes a green gate
run as ITS criterion.
R8 is RETIRED CAUSALLY --- mechanism removed plus a discriminating,
portable witness --- and moved to the retired section with its
disposition. What the retirement does NOT claim is stated there: 113
`new_with_roots` constructions in this suite alone, an unknown number
equally unbounded, harmless only while their assertions do not render a
path. That census is now a named §6 follow-on, because the next one will
otherwise look like a fresh mystery rather than a known class.
Framing: docs/r8-fixture-boundary-framing.md (revision 2, approved).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs(tests): reunite the listview doc comment with its test; state the PR
Three review findings, one of which is mine to own plainly.
I REPORTED A SHA I NEVER VERIFIED. The previous message named the PR
head as `21f0ed1`. That object does not exist in this repository. The
true head is `78d8e1c` --- local tip, `githubsucks/r8-fixture-boundary`,
and the PR all agree, and it is what was reviewed. No command in that
turn ever printed `21f0ed1`; I asserted an identifier instead of
reading one, which is precisely the failure a head-SHA check exists to
catch. Verified this time before writing it down.
THE DOC COMMENT DOCUMENTED THE WRONG TEST. Inserting the new witness
anchored on `#[test]\nfn flat_listview_...`, which sits BELOW that
test's 17-line doc comment --- so the comment about outline and flat
listview consumers ended up introducing the planted-marker test, which
touches neither, while the test it was written for was left bare. Moved
back. No behaviour change; both tests still pass.
That is a general hazard of anchored insertion worth naming: anchoring
on the `fn` line silently steals whatever documentation precedes it.
STALE STATE IN TWO DOCS. The framing still said "Pre-implementation.
Awaiting approval" after being approved and implemented, and the ledger
lane said "PR PENDING" after #226 opened. Both now record approval,
implementation, the PR link, and that it is held for review with no
merge authorization.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: retire the long-lines lane, the QoL arc having closed at #223
Rule 4, applied in its stated order: durable facts to
`docs/agent-handoff.md` FIRST, then the lane goes. The ledger is for
volatile branch/checkpoint/recovery state, and none of that survives a
merge worth keeping.
What moved to the handoff, because it is architecture rather than
bookkeeping:
- The GPU is not a grid consumer, so it could not inherit
`view_left`; `code_scroll_left` is its own, in pixels, local
viewport state with no wire and no version bump.
- The work was ONE transform and ONE clip, written before any
consumer moved. glyphon honors `TextBounds` so the text layers
clip themselves; the manual quad and squiggle renderers do not,
and nothing needed them to before this stage because no
code-relative x could be negative.
- Washes crop rather than drop — the same boundary Stage 4's review
caught the TUI painter getting wrong.
- Both lifecycle resets are observed PRE-MOTION, because a later
cursor motion repairs the offset anyway.
- `pmacs_protocol:📜:follow_left`: the follow rule beside
`classify`, called by both frontends. The approved exception to
Stage 5's scope, recorded in the framing doc §1.2a.
- The two witnesses that exist because mutation testing found the
TESTS wrong rather than the code — the unwitnessed glyph layer, and
the completion predicate a far-off-left test could not catch.
"Test a boundary AT the boundary" is the transferable part.
The `## Long lines (QoL arc)` block is removed whole; nothing else in
the ledger referenced it.
NOT REMOVED, and not an oversight: the QoL Stage 1 (#219) and Stage 2
(#220) blocks. Rule 4 would now reach them too — the arc they belong to
is closed — but Stage 2 carries a live named follow-on, capability-aware
keymap resolution (Q#Z3 option A, deliberately deferred), plus the
"first eager state consumer" restore-seam finding. Those need re-homing
before their blocks can go, which is a judgment beyond retiring the lane
that was asked for. Flagged rather than done.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: complete the retirement — Stages 1 and 2, and the stale anchors
The previous commit removed the long-lines lane and stopped short. Two
things were still wrong.
STALE ANCHORS. The handoff was dated 2026-08-06 with `main` at
`db1bbe9` — five merges behind. Now 2026-08-08 at `9a26ac8`, with the
arc's five merges named in the ancestry chain, and the same in
active-work's canonical-base record and recovery floor (a tree at
`db1bbe9` no longer passes; it would lack the whole arc).
**The recovery path was re-exercised, not SHA-swapped.** That file
warns that advancing the base is exactly when the commands are most
likely to have rotted and that a swapped SHA reads identically to a
verified one — so: fresh clone into an empty directory, `githubsucks`
alias added, `git fetch --prune`, `9a26ac8` confirmed an ancestor of
`githubsucks/main`, and a worktree recovered with the three-argument
form. All four steps clean.
STAGE 1 AND STAGE 2 BLOCKS. Keeping them left false live planning
standing: "Stage 3 is long-line wrap/scroll, which is a design round:
no horizontal viewport exists at all" — written before #221–#223 built
one. A merged lane that still describes the future is worse than no
lane.
Re-homed first, per Rule 4's order:
- **FG-INV is a CONSUMER contract**, and it lives on the protocol
type because that is where consumer authors read it. It had been a
doc comment on a PRIVATE PRODUCER FIELD, which is why the one
consumer never honored it.
- **Seven tests covered that flag and all seven tested the
producer.** None asserted a consumer acts on it. "Add a test for
the flag" had already been done — §5's enforcement/documentation
drift in a second register.
- **`install_state_dirs` is the eager-state-consumer seam.**
Builtins and `init.lua` run before it, so `pmacs.state.read` at
module load returns nothing, always. `saveplace` and `recentf`
escape it only because both read lazily. Any future eager consumer
belongs at the same seam.
- **A GPU-only binding cannot be expressed**: `Scope` has no frontend
identity and `FrontendEvent` no command-invocation variant. #220
shipped commands without bindings for that reason, not preference.
- **Capability-aware keymap resolution** is now a named §6 backlog
item: CROSS-CUTTING, NOT STARTED, needs its own framing. It says so
explicitly, and says not to start it as a half-lane attached to
another stage's branch — which is how it would arrive by accident.
No implementation, no lane, no design.
What is deliberately NOT preserved: the recovery commands for
`full-grid-resync` and `gui-zoom`. Those branches are merged; a
recovery command for a branch nobody should check out is the kind of
stale instruction this ledger exists to avoid. The framing docs remain
on disk as the historical record.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: the live schema range is v6..=v22, and #224 gets its own lane
Two review findings against the retirement.
THE SCHEMA RANGE. The canonical-base paragraph still said `v6..=v21`.
The upper bound moved to **v22 at #221**, which added
`InstanceMessage::LineWrapFacts` — so that line had been wrong for two
merges, including the one this branch is retiring the lane for.
Verified against `pmacs-protocol/src/message.rs` rather than carried
forward: `SUPPORTED_PROTOCOL_VERSIONS` is `6..=22`, `PROTOCOL_VERSION`
is 22, and `ADVERTISED_PROTOCOL_VERSION` is **20** and did not move.
The paragraph now says so, and says the advertised constant must not be
edited to chase the range — it is a permanent baseline, and the session
version is settled one message later by the frontend's counter-offer.
It also now states which claims it governs: historical `v21` statements
elsewhere describe a stage as it landed and are correct there. Only this
current-state paragraph tracks the live range, so only this one goes
stale when the range moves.
A LANE FOR #224. This file requires a lane for **every open PR**, and
the PR that retires other lanes is not exempt. Added with the branch,
the ref-not-a-SHA recovery command, the docs-only scope, and the
verification — including that the recovery path was re-exercised rather
than SHA-swapped, and that the full gate suite is deliberately not
re-run for a change that cannot reach it.
The entry is honest about its own lateness: it was written AFTER the PR
existed, which is the standing correction from #171 and #215 being
missed again, and it took review asking. Back-dating the block to look
compliant would have destroyed the only evidence that the practice
still slips. It carries its own retirement instruction — next
absorption after #224 merges.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: scope the two provenance paragraphs that claimed to be current
Both said "this line is the head-of-`main` anchor". Neither was, and a
provenance note that claims currency is worse than a plainly historical
one — it disagrees with the real anchor at the top of the file while
looking authoritative.
The bottom-panel paragraph keeps its `v6..=v21` facts, which are correct
for the stage they describe. What changes is scope: "a current session"
becomes "a session at that anchor", and the closing clause now says
these statements describe the historical `6c9e765` anchor, with the
live range pointed at "Repository authority" in `docs/active-work.md`
(`v6..=v22` since #221; advertised baseline still v20). The
counter-offer mechanism is called out as still current independent of
which numbers it carries, since that part did not go stale.
The second was MY inconsistency, introduced earlier on this branch. I
rewrote that bullet's opening to "Beneath the QoL arc, at `db1bbe9`"
and left its closing claiming to be the head-of-`main` anchor, so the
bullet contradicted itself. It now names what it actually is — the
`db1bbe9` ancestry chain — and points at the top of the file.
Both cross-references name the file they point into. "Repository
authority" is a section of `docs/active-work.md`, not of this one, and
an unqualified "above" would resolve to nothing here.
Noted, not acted on: the bottom-panel arc has its own currency drift in
this file (§1 says Arc 7 COMPLETE at #213, two later paragraphs still
call Stage 3 the remaining step). Pre-existing, unrelated to the QoL
retirement, and not this PR's to fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: frame QoL Stage 5, GPU horizontal scroll
Stage 4 merged as #222, so the lane advances to its last stage. Rule 4
still does not apply — the arc closes when Stage 5 merges, not before.
THE FRAMING'S FIRST FINDING CORRECTS STAGE 4'S. §1.3 there said the GPU
"needs a mechanism that does not exist", named it the fact most likely
to invert the cost estimate, and I endorsed the Stage 4/5 split partly
on that basis.
Half of it holds: `Scroll::horizontal` really is discarded throughout,
because glyphon 0.11 never applies it when placing glyphs — three
doc sites and three asserting tests. But that is not the only
mechanism. The document `TextArea` already carries an explicit `left`
origin and a `TextBounds` clip whose `left` is `gutter_clip_left`, and
horizontal scroll is `left: text_left - offset_px` with the clip
unchanged. glyphon then drops what falls left of the gutter — the same
"paint from column 0, clip at the edge" shape the grid renderer uses,
expressed in pixels. It is machinery the file already depends on, not
new machinery.
The split stays right for the reason that survives: the three consumers
Stage 4 named — caret (`code_byte_px`), decoration geometry
(`push_glyph_extent_rects`), hit testing (`gutter_aware_rel_x`) — each
produce x relative to `text_left()` and each need the same offset,
applied ONCE or they disagree. Shipping that inside Stage 4 would have
made one reviewable change into two unreviewable ones. But it was
justified partly by an overstatement, and saying so is cheaper than
letting a future reader inherit it.
No wire, no version bump: the GPU owns its viewport locally, exactly as
it owns `scroll_top` and `code_scroll_residual`. The parallel with
`ui.line-wrap` is misleading and the doc says why — the MODE is buffer
state and needed v22, the OFFSET is viewport state and needs nothing.
Five questions, each with my vote. Q#G3 is the one I am least sure of:
the GPU can resolve a proportional family, where "column" has no fixed
pixel width, so column-for-column parity with the TUI is unachievable.
I lean to defining the behavior in pixels and accepting imprecise
correspondence rather than gating a navigation feature on a font
choice — but that is a product call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: Stage 5 revision 2 — a clip, not just an offset
Two functional findings and two record repairs.
Q#G3 WAS BUILT ON A FALSE PREMISE, and the correction makes the lane
stricter rather than looser. Revision 1 said the GPU can resolve a
proportional family and proposed accepting a new TUI/GPU divergence to
accommodate it. It cannot: `family_is_monospace_everywhere` gates the
family across all four weight/style combinations,
`apply_font_facts` falls back when that fails, and
`unresolvable_and_proportional_families_fall_back` REQUIRES the
fallback. Answered as monospace-only by the font contract that already
exists — and the consequence is that the TUI-parity witness becomes
UNCONDITIONAL for every font the GPU supports. Revision 1 would have
introduced a font-dependent behavior difference to solve a problem the
codebase had already solved, in the lane whose purpose is removing
unchosen divergence.
"THREE CONSUMERS" WAS INCOMPLETE IN A WAY THAT WOULD HAVE SHIPPED A
DEFECT. Shifting the `TextArea` clips glyphon's text because glyphon
honors `TextBounds`. The manual quad and squiggle renderers have no
code-area scissor at all — nothing stops them painting into the gutter,
and today nothing needs to, because no code-relative x can be negative.
Scrolling makes that false.
So the framing now requires TWO shared things: one screen↔code
transform, and one code clip rectangle every code-relative painter
intersects with. The paths are tabulated with sites — caret rect
(`:9698`), caret-painted predicate (`:9734`), glyph extent rects
(`:9766`), inline math origins (`:9434`), completion anchor (`:7606`).
The two caret sites are the sharpest, and one of them falsifies a claim
revision 1 made: `:9734` has no left-edge test, so "the scroll
indicator inherits the fix" was false — `code_byte_painted` reuses it
and would call an off-left byte painted. And `:9698` does not merely
lack a check, it DOCUMENTS the absence as safe ("the caret x can't
precede `text_left`"). A comment asserting an invariant this lane
deletes is worse than silence.
Q#G2: "inert under wrap" was too weak. The offset must be RESET to zero
on the wrap transition, as the TUI already does — `horizontal_follow`
assigns `view_left = 0` on the wrap branch. Inertness hides a stale
value that reappears the moment the buffer toggles back to `truncate`,
before any cursor motion. G5 gains a witness that an inertness-only
implementation fails.
RECORDS. Rule 4's Stage-5 removal precondition was not actually met:
the handoff still described Stage 4 as upcoming work. Stage 4's durable
facts are now transferred — the unsnapped per-window column with a
per-line effective edge, the line-absolute walk, the three-way cell
designation, `Viewport::visible_cols` and its five adopters, the
wrap-branch reset, the `#[serde(default)]` persistence, and the absence
of any wire. The ledger's "Stage 4 ahead" / "Stage 4 plan" text is
corrected to Stage 5, and its Rule 4 note now says the removal is
legitimate BECAUSE those bullets exist.
And the journey-step claim is withdrawn. Revision 1 said this lane
completes journey step 4; step 4 is scored on welcome/help/tutorial
discoverability and COHERENCE.md:395 holds it Partial for reasons this
lane does not touch (`C-h` deletes a word, no tutorial). Restated as
preserving interface comprehension with no scorecard movement. §16 is
the direct target. Writing an unearned mark into a scorecard is how a
coherence document stops being ground truth.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: Stage 5 revision 3 — four corrections, one of them impossible
Q#G1 CONTRADICTED THE Q#G3 ANSWER IN THE SAME DOCUMENT. It still said
the GPU's font "need not be monospace" and that Q#G3 makes "column"
ill-defined — both falsified by the answer two sections below, in the
same revision that wrote it. The pixel-storage vote is unchanged, but
its reasons narrow to the ones that survive, and the conversion is now
stated as EXACT: columns × the supported monospace advance. That is
what makes the unconditional parity witness checkable at all.
Also removed `follow_cursor`, which I invented. The GPU's pass is
`ensure_caret_painted`, and it is now named rather than cited by line —
robust against the transposition that put these two sites at each
other's line numbers in review.
Q#G2 WAS MISSING THE BUFFER-SNAPSHOT RESET. The GPU zeroes `scroll_top`
and `code_scroll_residual` when a snapshot installs a new buffer; the
horizontal offset must reset there for the same reason. Without it a
buffer switch INHERITS the previous document's leftward viewport,
showing the new buffer scrolled sideways until a cursor motion repairs
it — a worse symptom than the wrap case, because nothing about the new
buffer explains it.
THE GUTTER ASSERTION WAS IMPOSSIBLE, not merely imprecise. Revision 2
proposed asserting that nothing paints left of `gutter_clip_left`. With
line numbers on, the gutter DELIBERATELY holds digit glyphs and
diagnostic-sign quads, so that assertion fails on a correct
implementation — a test that can only be satisfied by removing the
gutter. Replaced with the checkable form of the same intent: the gutter
rectangle is byte-identical before and after a horizontal scroll, and
the left-edge rule is checked against code-relative geometry only. It
still catches a code painter bleeding into the gutter, because that
changes those pixels.
THE COMPLETION ANCHOR HIDES, IT DOES NOT CLOSE. `completion_anchor_px`
already returns `None` when the anchor scrolls out, so nothing draws
while the daemon-owned completion state and its key handling are
retained; actual closure is `CompletionPopup { anchor: None }`, which
is the daemon's to send. Revision 2 said "closes", which would have had
a viewport-geometry lane quietly redefining when a completion ends.
Specified as: no completion paint while the anchor is off-left, popup
reappears when it scrolls back, session semantics unchanged.
Ledger drift fixed: it still called the framing revision 1 with five
questions open.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: Stage 5 revision 4 — witnesses for the two rules that had none
Both additions cover requirements the framing had already stated and
then left untested, which is how a rule becomes a comment.
THE SNAPSHOT RESET (Q#G2). Revision 3 added the buffer-snapshot reset
and tested only the wrap one. The witness now scrolls buffer A to a
non-zero offset, installs a buffer B snapshot, and asserts the offset
is zero and B renders at its code origin BEFORE any `CursorByte`
arrives.
The pre-cursor scoping is the entire test. A later cursor motion
repairs the offset regardless, so a witness that waits for one cannot
distinguish "reset on snapshot" from "repaired on first motion" — and
the second is the defect. Same shape as the wrap witness, which is also
scoped to before any motion, and for the same reason.
THE MINIMAP (Q#G4). The vote is "no movement", and the implementation
already supports it: the minimap derives from the summary, the surface
dimensions and `scroll_top`, with no horizontal input. So the witness
pins an existing property rather than requesting work — which is
exactly why it is worth writing. An offset threaded one seam too far
would break it silently, and nothing else in G5 would notice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: Stage 5 approved, five questions resolved
G1 pixels with exact conversion via the supported monospace advance; G2
automatic cursor-follow only, zeroing on both the wrap transition and
BufferSnapshot; G3 monospace-only by the existing font contract; G4
minimap unchanged; G5 accepted whole, including the snapshot-reset and
minimap-stability witnesses.
The scope boundary is restated in both documents because it is what
keeps this lane small: local GPU viewport state, no wire message, no
protocol bump, no command surface, no minimap movement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* feat(gpu): horizontal scroll — the transform, the clip, and both resets
Stage 5, partial. The mechanism and lifecycle are in; two consumers and
the whole verification set are NOT yet done — see the tail of this
message, which is a status, not a summary.
WHAT IS IN.
The offset, `code_scroll_left`, in pixels (Q#G1). Column parity stays
exact because the code font is monospace by contract, so
`columns × advance` is a definition rather than an approximation.
Local viewport state: no wire, no version bump.
One screen↔code transform (`code_x_to_screen` / `screen_x_to_code`) and
one code clip (`code_clip_left` / `survives_code_clip_left`), which is
the pair framing §1.1 requires. Written before any consumer moved,
because five sites deriving the same offset independently is how the
caret and the glyphs it sits among come to disagree.
The glyph-side mechanism is one line: the document `TextArea`'s `left`
shifts while its `bounds.left` stays at the gutter, so glyphon clips
and the gutter keeps its own pixels.
BOTH LIFECYCLE RESETS (Q#G2), which were the two rules most likely to
be left as comments. The wrap transition zeroes the offset in
`apply_line_wrap` — inertness would park a stale value that reappears
the instant the buffer toggles back to `truncate`. The buffer snapshot
zeroes it beside `scroll_top` and `code_scroll_residual`, or a buffer
switch inherits the previous document's leftward viewport and shows the
new buffer scrolled sideways until a cursor motion repairs it.
`code_caret_rect_in_clip` gains its left-edge test, and its comment is
REWRITTEN rather than extended: it used to assert "the caret x can't
precede `text_left`", an invariant this stage deletes. A comment
asserting something a later stage falsifies is worse than silence. That
also repairs `code_byte_painted`, which reuses it — revision 1's claim
that the scroll indicator "inherits the fix" was false precisely here.
`gutter_aware_rel_x` is now the exact inverse of the transform, with
the gutter clamp applied in screen space first: a click in the gutter
band means "the first visible column", which after scrolling is the
offset, not column 0.
The completion anchor HIDES when scrolled off-left and does not close —
the daemon owns completion state and its key handling, and closure is
`CompletionPopup { anchor: None }`, which is the daemon's to send.
`horizontal_follow` mirrors the TUI's: automatic only, scroll just far
enough, so a caret already visible never moves the view. It runs after
`normalize_code_scroll` because it reads the caret's laid-out x, which
vertical normalization can change.
WHAT IS NOT IN, and must land before this is reviewable:
- `push_glyph_extent_rects` — washes, squiggles and selection extents
still paint at unshifted x and are not cropped at the gutter.
- Inline math origins (`:9434`) — same.
- Every Q#G5 witness. The 228 existing GPU tests pass, which says
only that nothing regressed at offset 0; not one of them exercises
a non-zero offset.
Gates so far: fmt; clippy --workspace --all-targets -D warnings;
PMACS_REQUIRE_GPU=1 -p pmacs-gpu 228/0; git diff --check. The full
two-configuration sweep is deliberately not claimed — the lane is not
finished.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* feat(gpu): the last two painters move, and twelve witnesses say so
Completes Stage 5. `62fb93e` landed the transform, the clip and both
resets but left two code-relative painters at unshifted x and the whole
Q#G5 witness set unwritten; its 228 green tests said only that nothing
regressed at offset 0.
The two painters:
- `push_glyph_extent_rects` — selection/search washes, peer presence
and diagnostic squiggles. Shifted through `code_x_to_screen`, then
CROPPED at the gutter rather than dropped: a selection running in
from off the left edge must paint the part that is visible. That is
the same boundary Stage 4's review caught the TUI painter getting
wrong, and it would have been easy to reproduce here.
- Inline math. The glyph mini-buffers only needed their origin moved —
their layer already carries the code area's `TextBounds`. The
fraction rules are quads in the background batch with no scissor of
their own, so those are cropped by hand.
`crop_to_code_clip_left` is the crop, and `survives_code_clip_left` now
delegates to it, so a caret the crop would discard is never painted.
One boundary rule, not two that agree today.
TWELVE WITNESSES, EACH MUTATION-TESTED. Eleven production mutations —
unshifted wash x, uncropped wash, unshifted math origin, uncropped math
rule, untested caret left edge, missing snapshot reset, missing wrap
reset, unhidden completion anchor, unscrolled glyphs, inverted hit-test
sign, pixel-instead-of-column snap — each fail the intended witness as
an ASSERTION failure, not a compile error. The minimap-stability
witness was mutation-tested separately by threading the offset into
`minimap_vertex_bytes`.
That battery earned its keep immediately. The gutter byte-identity
test's "the code area must actually have moved" assertion is satisfied
by a decoration wash and the caret alone, so it PASSED with
`TextArea.left` pinned to `text_left` — the entire glyph-side mechanism
was unwitnessed and nothing in review would have shown it. Its
replacement isolates the glyph layer: no decorations, and a source line
carrying no caret, whose band is blank at offset 0 and inked after.
ONE DELIBERATE STEP OUTSIDE THE APPROVED SCOPE, and it needs a ruling.
Q#G5 asks for frontend agreement that is "checkable rather than
asserted". Two tests in two crates asserting the same literal is not
that; it is the structural duplication `pmacs-protocol::scroll`'s own
module docs condemn, and that module exists because THIS ARC already
shipped that defect — the scroll indicator, fixed in one copy and left
wrong in the other. So the follow rule moved to
`pmacs_protocol:📜:follow_left`, beside `classify`, and both
frontends call it: `src/editor.rs::horizontal_follow` delegates, and the
GPU converts px <-> columns around it, exact by Q#G3.
The cost is that Stage 5 now touches `src/editor.rs`, which "local GPU
viewport state" does not cover. No wire message and no version bump —
the same argument `classify` already makes. If rejected, reverting is
small: restore the four-line conditional, drop `follow_left` and its
four protocol tests, rewrite the parity witness as a two-sided pin.
GATES, both configurations, five ambient roots isolated: fmt; clippy
`--workspace --all-targets -D warnings`; `--lib` 1920 and `--lib
--features crdt` 2105; horizontal_scroll 11, long_line_readable 3,
line_wrap 6, full_grid_resync 1; `PMACS_REQUIRE_GPU=1 -p pmacs-gpu`
239; `-p pmacs-protocol --lib` 29; both full workspace sweeps;
`git diff --check`.
TWO SWEEP FAILURES, NEITHER THIS LANE'S, both logged:
- R8, new row: `flat_listview_consumers_render_byte_identically...`
fails DETERMINISTICALLY, and the merge-base control is done — it
fails identically on `main`. The row renders with a leading
directory stripped; it is a prefix strip, not width truncation, and
the mechanism is NOT diagnosed. Deliberately not fixed here.
- U3: the R7 selector failed once and passed on rerun. Recorded as a
new incident, NOT an R7 match — different flavor, and its fragments
are unverified because I filtered the sweep output before reading
it. U2 records me making that exact mistake already. Sweeps go to a
file from now on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* fix(gpu): the completion anchor is a point, and the witness now says where
Review round 1. One defect, and a lesson about the witnesses that
missed it.
THE DEFECT. `completion_anchor_px` reused `survives_code_clip_left` and
passed `line_height` as the horizontal extent — a VERTICAL dimension
standing in for a horizontal one. The predicate is
`screen_x + w > code_clip_left()`, so an anchor up to a whole line
height left of the gutter "survived". `completion_dropdown_rect` bounds
`ax` against the right margin only, so that x reached the popup's left
edge and painted over the line numbers.
An anchor is a position between glyphs. It has no width, and the popup
it places is drawn to its right. So the predicate is a point:
`screen_x < code_clip_left()`.
The absent left clamp downstream stays absent, deliberately. This
predicate is what guarantees `ax >= code_clip_left()`; a second clamp
would be a duplicate of the same rule, which is the failure mode this
stage's shared-transform design exists to avoid. It is witnessed
instead.
THE LESSON, which is the more useful half. The existing test placed the
anchor 200px off-left — and 200px off-left fails a width-based
predicate too, so it stayed green straight through the defect. The
mutation battery agreed with it, because every mutation asked only
whether REMOVING a check was caught, never whether the check had the
right shape.
A boundary must be tested AT the boundary. The new witness straddles it
by ±0.05px — the same anchor either side of the edge, which no
width-based predicate can separate — and additionally asserts the
popup's own left edge stays out of the gutter, making "no left clamp
needed downstream" a checked claim rather than a comment. Verified both
ways: the new witness fails against the original predicate, the old one
passes against it.
THE AUDIT that finding prompted. Stage 5 has one other left-edge
predicate, the caret's. Its use of `survives_code_clip_left(rect.x,
rect.w)` is correct — a caret quad genuinely is `CARET_WIDTH` wide —
and it was also only tested far from the edge. It is now walked ACROSS
the boundary a column at a time, asserting painted carets are wholly
inside the code area and hidden ones wholly outside.
That pins an argument that was load-bearing and invisible: because
`horizontal_follow` snaps to whole columns, a caret is never partly
behind the gutter, since `CARET_WIDTH` (2px) is far below any code
advance. Substituting `rect.h` for `rect.w` — the exact error above —
fails it. An over-width smaller than one advance does not, and that is
the invariant rather than a gap.
SCOPE. `follow_left` recorded as the one approved exception to "local
GPU viewport state" in the framing doc, new §1.2a: what it is, why the
Q#G5 parity witness cannot be real without it, and what it does not do
— no viewport state moved, no wire message, no version bump.
GATES, both configurations, five ambient roots isolated, sweeps
redirected to files per U3's lesson: fmt; clippy `--workspace
--all-targets -D warnings`; `--lib` 1920 and crdt 2105;
`-p pmacs-protocol --lib` 29; `PMACS_REQUIRE_GPU=1 -p pmacs-gpu` 241;
horizontal_scroll 11, long_line_readable 3, line_wrap 6,
full_grid_resync 1; both full workspace sweeps; `git diff --check`.
The only sweep failure is R8, confirmed by its recorded fragments —
pre-existing, deterministic, merge-base controlled against `main`, and
not this lane's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: Stage 5 is PR #223, head 55faa45
The ledger said "no PR opened yet", which stopped being true the moment
it was. Records the PR, its head SHA, and the standing do-not-merge.
Rule 4 still applies at merge, not now: the long-lines lane stays until
#223 lands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: the tip is the ref, not a SHA the commit itself invalidates
The previous commit wrote "head 55faa45" into the ledger and, by
existing, made it false — recording the PR moved the head to 4902048.
A SHA pinned in a document that the act of writing it stales is a trap,
not a record.
The ledger already states the correct convention two paragraphs down
("the authoritative tip — the ref, not a SHA"); this follows it, and
says to verify CI against the PR's live headRefOid.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stage 4 is open as #222; Stage 5 (GPU) still closes the arc, so the
Rule 4 exemption above stands unchanged.
Records the two things review added after the framing was approved: the
`Viewport::visible_cols` single clip rule with its five adopters, and
the corrected `ui.line-wrap` description. Both are lane facts rather
than framing ones — the framing decided the coordinate contract, and
these are what implementing it against a real frame turned up.
Also notes R7, so a reader of this lane finds the unrelated red without
having to reconstruct why a sweep in this window went 112/113 once.
No SHA — `githubsucks/horizontal-scroll` stays the authoritative tip,
per this ledger's own rule.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Stage 4 of the QoL arc, framing revision 4 (approved). Under
`truncate`, text past the right edge was UNREACHABLE; moving the cursor
now brings it into view. Automatic only — no commands, no bindings, no
new interaction island (Q#HS2).
THE CONTRACT. `view_left` is an unsnapped per-window display column
(Q#HS7(a)), and each line derives its own effective edge during the
walk it already performs from column 0. Starting at 0 is not laziness:
tab expansion depends on the absolute column from the line start, so a
walk beginning at the edge would put tab stops in the wrong place. The
walk stays line-absolute and only the emit translates.
Where the edge bisects a wide glyph on a given line (Q#HS7(c′)), its
trailing cell paints a styled BLANK rather than a `Continuation` — that
glyph means "the cell before me is a wide glyph's head", and here that
cell is off-screen, so emitting it would name a cell nobody painted.
The mapping designates that cell to the glyph's START byte, which keeps
`byte_at_place` total over visible cells and makes the character the
user scrolled toward clickable. Tabs keep FORWARD rounding (Q#HS7(c″))
— preserved, not chosen.
DECORATIONS TRAVEL WITH THE TEXT. The first version of this commit
translated the base glyph walk and nothing else, which split the frame
in half: at `view_left = 10` a glyph from source column 10 painted at
screen column 0 while its syntax style, diagnostic underline, search
wash and `BufferStyleOverlay` span painted at screen column 10 — or
vanished. Decorations drifting off the characters they describe,
silently, and only once a window had been scrolled.
Every such site carried the same two lines (`start_col.min(max_cols)`,
`end_col.min(max_cols)`), correct only while the left edge was pinned
at zero. `Viewport::visible_cols` is now the one rule all FIVE adopters
share — syntax/LSP styling, diagnostic underlines, search washes,
`BufferStyleOverlay`, and the selection painter — so a future decorator
inherits the translation instead of re-deriving it. It also subsumes
the old `end_col <= start_col` guard rather than sitting beside it.
`StyleSpanOverlay` and `VirtualCellOverlay` are deliberately untouched:
they are documented as viewport-relative, so translating them would be
the mirror defect.
The selection painter was nearly a sixth site with its own copy of the
rule, which I justified by a width it supposedly needed and the
viewport lacked. That was FALSE — the render viewport's
`cell_size.cols` is already `rect.size.cols - gutter_w` and its origin
already sits past the gutter. It now takes that same viewport and drops
its `rect`/`gutter_w` parameters entirely. A canonical rule with one
honest exception is not canonical.
The selection painter had the same defect with a worse failure mode: it
asked `pos_to_display` through the LIVE context, which returns `None`
for a position left of the edge, so a selection beginning off-screen
and reaching into view took `continue` and painted NOTHING. That is the
common shape, not an edge case — select rightward from column 0 past
the window width and the view scrolls with the cursor.
TWO THINGS THE TESTS FOUND, both in `pos_to_display`. My framing note
said a caret sits between characters so never lands inside a glyph;
true for the caret, false for the DESIGNATION direction — the glyph's
start byte must map to its visible trailing cell, so `screen_col` needs
the straddle rule and not a bare subtraction. And the `take == 0` early
return short-circuited the translation entirely, so byte 0 looked
visible at every offset.
`view_left` is inert under `wrap` BY CONSTRUCTION —
`LayoutCtx::effective_left` and `Viewport::left_edge` return 0 while
wrapping — rather than by every caller remembering.
Persisted per leaf at DESKTOP_VERSION 1 (Q#HS5) with both approval
conditions: `#[serde(default)]` and a literal v1 JSON fixture omitting
the field, hand-written because a generated one would gain the field
and prove nothing.
Also: `view_left: window.view_left` in the render viewport, not a
literal 0. My mechanical fill put 0 there and it is EXACTLY the
`aa3cd4d` defect — coordinates and the indicator following the scroll
while the painter stays pinned at column 0.
BITE, per clause. Forcing `bisected = false` fails the multi-line
straddle witness; dropping the backward designation fails the
round-trip witness; removing `#[serde(default)]` fails the v1 fixture;
pinning `visible_cols` to an absolute clamp fails all three decorator
witnesses; restoring the selection painter's live-context lookup fails
the off-screen-start selection witness. Each alone. And with selection
now reading the shared helper, pinning `visible_cols` to an absolute
clamp fails the selection witnesses TOO — which is the check that the
duplication is really gone rather than merely reworded.
One unrelated red, logged as R7 in ci-red-signatures.md — the first
this session with a COMPLETE signature, so a matchable row rather than
a U note. `pmacs-gpu`'s managed-retry attach hit a BrokenPipe once
under full-sweep load and did not reproduce (6 isolated runs plus a
clean 113-target sweep). Per the rerun rule that is intermittence only,
and the row explicitly does not claim harmlessness. Not attributed to
this lane: Stage 4 touches no `pmacs-gpu` file and adds no wire
surface.
Gates: fmt; clippy --workspace --all-targets -D warnings, both
configurations; `cargo test --workspace --no-fail-fast -- --skip
basedpyright` 113 targets exit 0, and the same with --features crdt,
113 targets exit 0; git diff --check. No protocol change, so no version
bump and no protocol-bump matrix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two small revisions, and every Stage 4 question is now answered.
Approval is NOT recorded; no implementation may begin.
Q#HS7(c″) — the tab-straddle mapping. Recorded as PRESERVED rather
than chosen, because it already exists: `byte_at_place`'s doc comment
says it rounds forward to the next character boundary, and the walk
accumulates past the tab byte and returns on the NEXT character's
start column, so every column inside a tab's expansion already yields
the post-tab offset (src/text_view.rs:224, :243-254). The requirement
on Stage 4 is therefore that horizontal scroll not PERTURB it — which
makes its witness a regression test, and one that should fail if the
walk is ever "optimized" to start at the effective edge instead of
column 0.
The obvious objection is that (c′) rounds backward and (c″) forward,
so the framing answers it. A wide glyph's two cells belong to ONE
character: forward-rounding its trailing cell would designate it to
the next character and leave the straddling glyph with no visible cell
mapping to it at all — unreachable by click exactly when it is what
the user scrolled toward. A tab's expansion cells are whitespace
BETWEEN the tab byte and the next character, and forward-rounding them
is already how clicking in indentation lands at the start of the text.
Different directions, one principle: every visible cell is designated
to the byte a user would mean by clicking it.
With (c′) and (c″) the (d) contract is total over visible cells:
ordinary character → its own start; bisected wide glyph → the glyph's
start; tab expansion → the byte after the tab.
Q#HS5 approved as stated, with both conditions written into the
approval rather than attached as advice: `#[serde(default)]` and a
literal v1 JSON fixture omitting the field, asserting restore at zero.
The handoff said "Stage 4 is the remainder". It now says Stages 4 AND
5 remain and the arc closes at Stage 5, carries the Q#HS1 time box,
and states explicitly that Rule 4 must not retire the long-lines lane
at Stage 4's merge. It also records that the unreachable caveat is
missing from the setting's description — the #221 gap — so that fact
lives in the durable doc and not only in a lane block that will
eventually be removed.
Ledger: the question list is consolidated (the accepted answers had
begun duplicating the blocking entries they resolved), keeping the
withdrawn (c)'s reasoning because the trap generalizes to any future
window-wide value derived from per-line content.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
P1 — Q#HS7(c) WITHDRAWN, and my vote was wrong rather than vague.
Revision 2 voted to snap `view_left` to a valid boundary at the moment
it is set. That cannot exist. `view_left` is ONE per-window display
column, but "does column N bisect a wide glyph?" is a PER-LINE
question: column 11 can be a wide glyph's trailing cell on line 3 and
ordinary ASCII on line 4. No setter-time value is canonical for every
visible line. Snapping per line instead is worse — the same source
column would appear at different screen columns on different rows,
destroying the alignment a column-oriented view exists to provide.
Replaced by (c′), a per-line effective edge. `view_left` is stored
unsnapped; each line derives its own edge during the walk it already
performs from column 0. Where the requested edge bisects a wide glyph
on THAT line, the finding's actual question — what occupies the
leftmost cell — is answered: it paints as a space carrying the glyph's
style, and the mapping DESIGNATES that cell to the wide glyph's start
byte. That keeps `byte_at_place` total over visible cells, preserves
the round trip (`place_of_byte(start)` reports the straddle and
designates cell 0), and gives a click there the character a user would
expect. Bytes lying entirely left of the edge are reported not-visible
rather than clamped to column 0, because clamping would make
arbitrarily many bytes share cell 0 and destroy (d).
Recorded as deliberately NOT the mirror of Stage 3's right-edge rule:
under wrap a too-wide glyph is pushed to the next row entirely, and at
the left edge under truncate there is no next row, so the same intent
requires a different rule. Stated so nobody "fixes" one to match the
other.
(d) is amended accordingly: the invariant is a property of
`(view_left, line)`, not of `view_left` alone — which is what makes a
multi-line fixture with differing glyph widths at the same column the
DISCRIMINATING test rather than an extra one. A single-line sweep
passes against the withdrawn design.
P1 — Stage 4 no longer closes the arc, and the stale claim was
load-bearing in the wrong direction. Rule 4 removes a lane when its ARC
is done, so a framing asserting Stage 4 closes it would license
retiring the lane at the TUI merge — orphaning the very Stage 5 that
Q#HS1's time box exists to guarantee, while `truncate` is still a dead
end in the GUI. Both the framing opening and the lane header now say
the arc closes at Stage 5, and the lane carries an explicit "Rule 4
does not apply at Stage 4's merge".
P2 — the stale Stage 3 residue in the ledger claimed the unreachable
caveat is in the setting description, contradicting revision 2 forty
lines above it. Corrected in place: the caveat lives only in the
toggle's status message and a source comment, and a user who sets the
mode in init.lua is told nothing.
Q#HS5 now states the concrete condition rather than an instruction to
check one. Verified: SavedLeaf carries no #[serde(default)] anywhere in
src/desktop.rs, so serde would REJECT a version-1 desktop JSON omitting
a new `view_left`. "Yes, no version bump" is sound only with the
annotation AND a regression fixture holding literal v1 JSON without the
field. The reverse direction already works — an old binary meets an
unknown field, which serde ignores absent deny_unknown_fields, and
there is none in that file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
P1 — THE PROTOCOL-BUMP GATE WEAKENED THE SWEEP IT MEANT TO STRENGTHEN.
Revision 1 said `--tests --no-fail-fast` REPLACES the `--workspace`
line. Measured: `--tests` selects 108 targets, `--workspace` selects
110, and the two it drops are `pmacs_protocol` and `pmacs_gpu`. On a
PROTOCOL bump, dropping the protocol crate's tests is the wrong loss —
and it also silently dropped `--skip basedpyright`.
Worse, this lane's own remediation sweep used `--tests`, so it never
ran pmacs-protocol's 25 tests, including the `scroll::classify` tests
this lane had just written. They pass (verified 25/0), but by luck. A
correction that reproduces the shape of the mistake it corrects is
worth naming, so §3 and §5 both say `--workspace`, additive, with
`-- --skip basedpyright` retained in both feature configurations.
P1 — NO COORDINATE CONTRACT FOR view_left. Revision 1 decided what
moves the viewport and never said what its offset IS — the same
omission as shipping WrapMode with no DisplayCoord. Its verification
sketch named tabs and wide characters with no oracle for either,
because nothing defined what a left edge is.
Q#HS7 is new and BLOCKING, in four coupled parts: the unit; which
columns may be a left edge; the snap rule for an invalid one; and the
invariant rendering and coordinate mapping share. Votes recorded —
display column (tab stops come free, since the walk must start at
column 0 either way and a byte offset buys nothing); a left edge may
not fall inside a wide glyph; snap toward the line start (snapping left
can only reveal a character, snapping right can hide the one the user
scrolled to reach); and snap when the value is SET, not in the painter,
so one canonical value serves both readers.
Part (d) is why it blocks: if the painter clips where the mapper does
not, clicks land on the wrong character — silently, and only on lines
wide enough to scroll.
P2 — THE CAVEAT IS NOT IN THE SETTING DESCRIPTION. Revision 1 said it
was. builtin/runtime/linewrap.lua:23 says only "truncate at the edge";
"unreachable" lives in the toggle's status message and a source
comment, neither of which a user sees who sets the mode in init.lua.
That is a real, small user-facing gap shipped in #221. Claim corrected,
and amending the description is now a Stage 4 deliverable (§6) with the
text depending on which stage has landed.
THE THREE ANSWERS, recorded with the reasoning that decided them:
HS1 — GPU is Stage 5. The distinction that matters is that Stage 3's
defect was never "the frontends differ" but "the frontends differ and
nobody chose that". Time box made concrete per the request: Stage 5
is the immediately-next QoL lane, `wrap` stays default until it
lands, release notes state the asymmetry, and the truncate
affordances name the GUI gap meanwhile.
HS2 — automatic only. No command surface; the cursor-visibility pass
gains a horizontal component.
HS6 — `wrap` stays default, and the reason given is stronger than the
one revision 1 reasoned from. I had framed it as "if scroll makes
truncate good, reconsider the default". With the GPU deferred, a
truncate default would ship a mode navigable in the TUI and a dead
end in the GUI for every user who never opened the setting. HS1 and
HS6 are coupled: the split is only safe because the default does not
move.
Q#HS4 is deferred rather than closed — not live under automatic-only,
but the snap-back hazard is real and rediscovering it costs more than
carrying the paragraph. Q#HS5 stands, with the caveat that §1.4 cites
the struct shape and not serde's behavior on a missing field.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
#221 merged, so the long-lines lane is REWRITTEN rather than removed —
rule 4 removes a lane when its arc is done, and Stage 4 is ahead. Stage
3's durable facts move to the handoff §1, which is rule 4's actual
precondition.
THE FRAMING LEADS WITH THE FACT MOST LIKELY TO INVERT ITS OWN COST
ESTIMATE, because that is what Stage 3 revision 1 got wrong. The GPU
cannot honor horizontal scroll through cosmic-text: `Scroll::horizontal`
is discarded throughout, and not by oversight — glyphon 0.11 never
applies it when placing glyphs. Documented in three places and asserted
by three tests. So the GPU's half needs a mechanism that does not exist,
touching caret placement, decoration geometry, and hit testing, each of
which assumes x starts at `text_left()`. That is Q#HS1: whether the GPU
is in Stage 4 at all.
Also verified rather than recalled: there is NO horizontal scroll
anywhere in the tree (greenfield, not an extension); `paint_line` starts
every walk at column 0, so `view_left` enters the functions Stage 3 just
rewrote and the wrap rule must stay written once; `view_top` is
persisted per leaf at DESKTOP_VERSION 1; `scroll_window`'s comment
already records the cursor-follow hazard; and `goal_col` is unexamined
horizontal state on the same window.
Six questions, each with my vote and the argument against it. The one I
am least comfortable with is Q#HS6: Stage 4 adds capability that exists
only under a NON-DEFAULT mode, which is a conditional surface rather
than a uniform improvement — and Stage 3 chose `wrap` as the default
partly BECAUSE scroll did not exist. If scroll makes `truncate` good,
that default deserves re-examination rather than inheritance.
ALSO A GATE CORRECTION, and it changes what I said in 4d70ff6. I wrote
that "the touched acceptance suites" is the standing gate. That is
CLAUDE.md's list. `docs/agent-handoff.md` §3 — which CLAUDE.md tells me
to read FIRST — already required `cargo test --workspace -- --skip
basedpyright`, a full sweep. I ran the short list. So the eight broken
version assertions were not a gap in the documented gates; they were me
following a summary instead of the gate suite.
§3 now says so, and adds the protocol-bump form (`--tests
--no-fail-fast` in both feature configurations), because even the full
sweep stops at the first failing target and builds one configuration —
it would have shown one or two of the eight, not all of them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
CI red on #221: all five Test jobs, one identical test, every platform
— deterministic, not a flake. The production code was never wrong.
WHY MY GATES MISSED IT. The standing gate is "the touched acceptance
suites", selected from the diff. A PROTOCOL_VERSION bump breaks
version-assertion tests that appear nowhere in it. Worse, CI showed
only ONE of the eight, because cargo stops at the first failing
target; the rest surfaced only under `--tests --no-fail-fast`, and one
at a time would have cost four more red rounds.
Three of the eight were invisible even to that, because they are
crdt-gated real-daemon tests asserting on a live socket. Found by
`--tests --features crdt --no-fail-fast`. That is the handoff's
existing "a local sweep is blind to whichever configuration it does
not build" lesson, hit again by a different lane.
THREE TRIPWIRES, WORKING AS DESIGNED. `assert_eq!(PROTOCOL_VERSION,
21)` in statusline_segments, bottom_panel_stage2b_gpu, and
vterm_stage3 are meant to fire and take a deliberate edit; each says
so in its own comment. Updated to 22 with the reason recorded. Worth
noting the pin that must NEVER be edited —
ADVERTISED_PROTOCOL_VERSION == 20 — did not fire, which is the
mechanism behaving exactly as designed.
FIVE DEFECTS, ONE SHAPE: an absolute contract expressed as arithmetic
on, or equality with, a MOVING constant. Each was true when written
and silently false afterwards.
- `PROTOCOL_VERSION - 1` meaning "below the panel version". Held
only while PROTOCOL_VERSION == PANEL_MIN_VERSION; at v22 it
equalled PANEL_MIN_VERSION exactly, so the fixture's "old" peer
became panel-capable and the daemon correctly sent it a frame.
Now `PANEL_MIN_VERSION - 1`.
- `assert_eq!(PANEL_MIN_VERSION, PROTOCOL_VERSION)` — a coincidence
true only while panels were the newest feature. Replaced by the
two durable bounds: above the advertised floor, at or below this
binary's wire.
- `assert_eq!(PROTOCOL_VERSION, 21)` in a test named
`the_panel_stage_takes_protocol_v21` — the current wire as a proxy
for the panel stage's own version, in a test whose name says which
one it means. Now PANEL_MIN_VERSION.
- `session_protocol_version == "21"` in two real-daemon probes. What
the counter-offer activates is THIS BINARY's wire, so the literal
was only ever right by accident. Now PROTOCOL_VERSION, plus an
explicit `>= PANEL_MIN_VERSION` for the panel capability the
literal had been carrying implicitly.
The codebase already had the right idiom: src/daemon.rs and
pmacs-gpu/src/main.rs spell it `PANEL_MIN_VERSION - 1` in five places.
Every outlier was in tests/.
ALSO LOGGED, NOT FIXED: U2 in ci-red-signatures.md.
`process::tests::m6_1_pty_raw_mode_disables_kernel_echo` failed once
during a full corpus run and did not reproduce (108 targets exit 0,
plus 3 isolated --lib runs at 1917/0). It is in no registry row, so it
is a new incident, and leaked `pmacs --daemon` processes remain an
unexcluded rival explanation. Recorded with a selector this time —
unlike U1, whose name I destroyed by piping through `tail`.
Gates: fmt; clippy --workspace --all-targets -D warnings, both
configurations; --lib 1917/0; --lib --features crdt 2102/0; --tests
--no-fail-fast 108 targets exit 0; --tests --features crdt
--no-fail-fast 108 targets exit 0; PMACS_REQUIRE_GPU=1 -p pmacs-gpu
228/0; git diff --check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The standing correction from #171, #215 and #220: the lane block is
written with the first commit, so it says "no PR yet" until something
updates it. Recording the number here rather than leaving it to be
reconstructed from the branch.
No SHA — `githubsucks/long-lines` stays the authoritative tip, since
any edit to this block advances past whatever SHA it records. That
rule is the lane's own, from #220's review.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Closes Stage 3's remaining obligations.
THE PTY TEST. Every other test in this lane checks a mechanism.
tests/long_line_readable_acceptance.rs checks the complaint: the
shipped binary, a real 80x24 PTY, one source line 200 columns wide,
and an assertion that its tail marker reaches the host. It bites —
`scripts/bite HEAD~2 src/editor.rs --test long_line_readable_acceptance`
against the pre-aa3cd4d editor never paints TAILZQX in 20s.
Its truncate control (an isolated init.lua pinning the mode) is what
makes that marker discriminating, and it is also the honest statement
of what truncate costs today: those bytes are not off-screen, they are
unreachable until Stage 4.
What it does not prove: the workspace has no screen model and no
vt100/termwiz/vte, so this shows the tail was WRITTEN to the terminal,
not which row a human would point at. That is nonetheless the whole of
the report — under truncation the bytes are never emitted at all.
§1.1 WAS WRONG, FOR NINETEEN REVISIONS. `editing.fill-column` is not
an orphaned registry setting "of the exact shape Stage 1 just fixed".
Both cited occurrences are inside `#[cfg(test)] mod tests` — fixture
names in round-trip tests covering one setting per ConfigKind. Two of
those five names are real; three, including this one, are defined
nowhere else in the tree. There is no shipped setting, so the Q#LL4
deliverable "sharpen its description" had no object.
The mechanism is worth more than the correction. A grep hit at a src/
path, a genuine `r.define(...)` call that is real API usage rather
than a mock, and `#[cfg(test)]` about fifty lines above the citation.
Every later revision inherited the conclusion instead of the evidence,
and three review rounds reasoned about the consequences of an orphaned
setting rather than re-checking that it existed. A file:line citation
is not a substitute for reading the scope it sits in.
Had it gone unchecked into implementation, Stage 3 would have shipped
an edit to a unit-test fixture believing it was rewording a
user-visible setting — a no-op with a misleading commit message.
§1.1 is withdrawn in place, keeping the original text and the
reasoning that produced it; §6's answer is unchanged (a setting that
does not exist is a stronger reason not to adopt it) and its premise
corrected. Both fixture sites now say they are fixtures. The approval
is not reopened: nothing else in the document rested on §1.1, which
argued for a display setting separate from fill-column — which is what
shipped.
AND ONE UNCLASSIFIABLE RED, logged as U1 in ci-red-signatures.md. A
`-p pmacs-gpu` run went 227/1 once; every run since is 228/0. The
failing test name was NOT captured, because I piped that command
through `tail -3` and discarded the failure block above the summary.
36 later runs are clean, 6 under deliberate concurrent load — which
per the rerun rule establishes intermittence only, and without a
selector not even that. Deliberately NOT matched against A1 despite
A1 also being GPU-headless-under-load: matching requires an exact
selector and every required fragment, and calling a shapeless red
"probably the known one" is the reputation-by-adjacency that file
exists to deny.
Gates: fmt; clippy --workspace --all-targets -D warnings; --lib
1917/0; --lib --features crdt 2102/0; line_wrap 6/0;
long_line_readable 2/0; folding 21/0; folding_stage2 48/0;
full_grid_resync 1/0; config_registry 16/0; m4 150/0;
PMACS_REQUIRE_GPU=1 -p pmacs-gpu 228/0 (see U1); git diff --check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Stage 2 (GUI zoom, #220) landed. Stage 3 shares no code with it, so this
is a currency merge rather than a dependency --- taken now so the
Stage 3 PR opens against a base it has already been tested on.
# Conflicts:
# docs/active-work.md
Section 5d.6 answered as (b): ScrollPosition and the pure classify go
in pmacs-protocol, string rendering stays in each frontend.
The reading that settles it is sharper than "shared vocabulary". Each
frontend computes its own local layout facts --- which rows are on
screen is a question only it can answer --- while the shared crate owns
the common semantic decision those facts feed. That is not presentation
leaking into the protocol crate; it is the DECISION placed where both
frontends are structurally unable to disagree, with rendering left
where it belongs.
Two properties bound the change: no wire message and no protocol
version bump, since classify is a pure function over values each side
already holds; and the one-copy-fixed defect from 5d.3 becomes
unrepresentable rather than reviewer-guarded, because there is only one
classifier to fix.
Q#LL8 approved. All eight questions answered. Implementation begins.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 16 said the scroll formatter could keep its signature and
change only what callers pass. It cannot, and the byte fallback in
revision 17 made that unavoidable rather than merely untidy.
Every branch of format_scroll_indicator derives from total_lines:
total_lines <= 1 -> All
visible >= total_lines -> All
view_top == 0 -> Top
view_top + visible >= total_lines -> Bot
pct = (cursor_row + 1) * 100 / total_lines
Revision 17 removed the total. Passing byte counts makes the Bot branch
compare rows against bytes --- it would return plausible strings that
mean nothing. Passing any stand-in small enough to satisfy the guards
restores the false All this section exists to remove. And "local
predicates" is not something that signature can evaluate, because it
has no parameter for them.
Resolved by not asking it to. truncate calls the existing formatter
UNTOUCHED, with the same arguments in the same units, so its output is
byte-identical by construction rather than by assertion, and every
existing formatter test stays valid --- including the GPU's
format_scroll_indicator(0, 10, 1, 0) == "All", which correctly pins
line-space behavior. wrap calls a new classifier:
classify(first_visible: bool, last_visible: bool,
byte_pos: u64, byte_len: u64) -> ScrollPosition
All = first && last; Top = first && !last; Bot = last && !first;
otherwise Percent from bytes. No count of rows enters it, so the unit
mixing is not avoided but unrepresentable.
The actual mistake was trying to serve two genuinely different
contracts from one four-count signature. It could only do that by
making units implicit, which is how the contradiction arose. This is
5b.5's identity-case strategy applied to the indicator itself: the old
mode keeps the old code, and the new mode gets code shaped to it.
Leaves one question open. pmacs-gpu depends on pmacs-protocol only,
never on the pmacs lib, so format_scroll_indicator is duplicated
STRUCTURALLY and the classifier faces the same fork. Duplicating it
preserves exactly the condition that produced the earlier defect, where
one copy was fixed and the other was not. Sharing it through
pmacs-protocol makes the agreement structural rather than maintained,
which is the principle that chose byte-anchoring, additive sub_row, and
a content-derived cache key --- but it widens that crate from wire
vocabulary toward presentation, which is a COHERENCE section 16
layering question and not this lane's to settle alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 16 gave the GPU its scroll-indicator total on the premise that
"cosmic-text already knows each line's visual height". It does not.
rebuild_code_slice feeds cosmic-text current_text[vstart..vend] and
nothing else, because Session S1 found that feeding the whole rope made
large-file editing O(file) per keystroke. The GPU's layout holds the
viewport slice plus overscan; it cannot yield total visual rows, nor
the cursor's or top's row ordinal.
Re-shaping the whole document to recover them would reintroduce exactly
the cost that design exists to prevent --- for a status-line readout.
So the aggregate is abandoned rather than relocated. NN% is computed
from BYTE POSITION in both frontends; truncate keeps today's
visible-line percentage. No total, no cache, no invalidation.
Three reasons it is abandoned rather than approximated. Rows-per-line
could be computed as ceil(width / cols) without shaping, but
cosmic-text decides the real break points, so the number could disagree
with what is on screen --- the same approximate-parity trap Q#LL5
rejected for whitespace wrapping. Letting the TUI use row ordinals and
the GPU use bytes would show two percentages for one buffer, which is
this lane's own defect a third time. And All/Top/Bot are unaffected
either way: they are local predicates, they stay exact, and they are
the states a user actually reads.
This retires the cache from revision 15 and the fold-key correction
from revision 16. That correction was right for the design as it stood;
the design moved under it. Section 5d.2 is marked SUPERSEDED rather
than deleted, because "the key was fixed" and "there is no key" are
different states and a later reader should be able to tell which one
happened here.
Adds the large-file guard as a witness rather than an intention: an
indicator paint must leave the GPU's view_range and shaped_top
untouched, must not lay out beyond the viewport in the TUI, and
open_100mb_under_200ms must still pass with wrap as the default mode.
Without those, the decision is an unenforced comment and a later
"improvement" to a real row count would silently restore O(file) work.
Known imprecision, stated rather than left to be discovered: under
folds a byte percentage counts hidden bytes. Folding is TUI-only today,
and it matches Emacs. It belongs to the GPU folding lane, not this one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two corrections to Q#LL8, both found by review of 1c9ff6a, and both the
same shape: a fix that read as complete because it was correct in one
of two places.
The GPU has its own scroll indicator. format_scroll_indicator is
DUPLICATED, not shared --- src/editor.rs:5509 and
pmacs-gpu/src/main.rs:10114, each with its own tests --- and the GPU
passes current_line_starts.len(), a source-line count. So revision 15
would have fixed the indicator in the TUI and left the GPU reporting
"All" for a one-line wrapped buffer.
That is worth naming plainly: it is this lane's own defect, reproduced
inside the section written to close it. The lane exists because two
frontends disagree for reasons nobody chose, and the fix disagreed for
exactly that reason one more time.
Both copies keep their signature. The formatter is a pure function over
counts and is correct as written; what changes is what the callers
pass --- visual rows rather than source lines, visible rows rather than
visible lines. Every existing formatter test stays valid, including the
GPU's own format_scroll_indicator(0, 10, 1, 0) == "All", which pins
line-space behavior and must not silently change meaning.
The lazy total's cache key omitted fold state. Folds are built per
rendered window and can be collapsed or expanded with no edit, no
resize and no mode change --- so all three of revision 15's key
components sit still while the projection underneath them moves. Cache
NN%, toggle a fold, and the stale total is served for the new
projection.
Now keyed on (buffer generation, content width, mode, fold projection),
and specifically on the projection's own components rather than a
revision counter on the fold registry. Components is one entry per
collapsed region, so comparing it is O(folds), and the key IS the thing
it guards --- it cannot be forgotten. A maintained counter can, on
every present and future mutation path, which is the same
did-you-remember hazard as LL7's buffer-switch trigger. Same principle
as byte-anchoring over a row index and additive sub_row over redefining
row: self-validating beats maintained.
Content width, not window width. Wrapping happens in the text area, and
the gutter's width changes at the line-count digit boundary (9 -> 10,
99 -> 100) --- something the GPU's sync_buffer_dimensions comment
already records for its own shaping. Keying on window width would serve
a stale total across that boundary.
Three witnesses added: the reported case in BOTH frontends, a "cache,
then toggle a fold" case that fails against revision 15's key, and a
digit-boundary case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two holes in revision 14, both found by review of this lane's own first
commit, and both would have been discovered during implementation at
much greater cost.
Q#LL7 --- the mode never reached the GPU. Section 4 resolves
ui.line-wrap into Viewport, which reaches the GRID renderer. The GPU is
not a grid consumer: it lays out locally, ignores CellDelta,
BufferSnapshot carries only CRDT bytes, and no InstanceMessage variant
expresses a wrap mode. So truncate would have changed the TUI and left
the GPU wrapping --- the two frontends still disagreeing, which is the
one thing this lane exists to fix. Q#LL5's "character wrap in both" was
equally unreachable: setting Wrap::Glyph at startup is not honoring a
mode that can change.
Specified as an additive variant at v22, appended after the current
final variant with the advertised baseline left at 20 --- the path
FontFacts took at v17 and the panel shapes at v21, and the baseline
constant's own doc reserves moving it for changes that cannot be
expressed additively. This one can.
It carries buffer_id, and is resent on attach, on config change, AND on
buffer switch. The third trigger is the one a FontFacts-shaped design
misses: font size is global, wrap mode is per buffer, so switching from
a truncate buffer to a wrap buffer changes the effective mode with no
config event at all. A design listening only to on_change is silently
wrong and passes every single-buffer test.
Q#LL8 --- the scroll indicator falsifies the locality claim. Revision
14 asserted every vertical consumer is local. format_scroll_indicator
is not: a one-line buffer wrapping to fifty screen rows has
total_lines == 1, so the first branch returns "All" while forty-nine
rows sit below the viewport. The indicator claims the whole buffer is
visible when almost none of it is.
The claim is narrowed rather than abandoned, because the distinction
that bounds the cost survives: a TOTAL is one number, lazily computed
and cached; a PREFIX-SUM INDEX is O(N) resident storage. Stage 3 needs
the first and still does not need the second. All, Top and Bot need no
aggregate at all --- each is a local predicate falling out of the
render walk --- so only NN% pays, which matters because the M1 gate
measures open time on a 100MB file.
Also fixes a notation hazard. Section 7 said pos_to_display returns
"the visual row", which reads as redefining row --- the exact thing
5b.5 forbids, stated two sections apart. Every wrap-point example is
now the explicit triple {row, sub_row, col}. Writing them out makes the
point visible: both coordinates at a soft break share the same row,
because a wrap does not cross a source line. That is the information a
redefinition would have destroyed, and the pair notation hid it.
Status returns to not-approved. Both questions change what gets built,
not how, so implementation waits on them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
A line wider than the window cannot be read past the edge in the TUI.
The GPU is not in that state --- it already wraps --- so the
cross-frontend defect is not unreadability. It is that NEITHER behavior
was chosen: the TUI truncates because a cell walk breaks at max_cols,
the GPU wraps because cosmic-text's Wrap::WordOrGlyph default was never
overridden. Two accidents that disagree, and no way to state a
preference in either.
Eleven revisions of review, and the corrections were the substance.
Revision 1 claimed both frontends render from the same CellGrid, which
inverted the entire cost model --- pmacs-gpu ignores the grid variants
and lays out locally, and its own comment says so. Later rounds caught
an impossible round-trip invariant, a "collision" between two
coordinates that were one position, an off-grid argument against a
function that has no grid, a false binary for view_top, and a
renumbered visible-line space that does not exist. Each is recorded
with its reasoning rather than quietly fixed, because the pattern ---
an assertion that reads as precise while resting on something
unverified --- is more useful to the next reader than any single fix.
All six questions are answered and the framing is approved:
LL1 wrap + truncate, default wrap; horizontal scroll is Stage 4
LL2 buffer-local mode, resolved into Viewport like folds
LL4 do not adopt editing.fill-column; ours is ui.line-wrap
LL5 character wrap in BOTH frontends
LL6 no global map; byte-anchored view_top; additive DisplayCoord
Two of those deserve to be found later rather than discovered:
GUI users lose word wrap. Character-wrap parity is cheap, true, and
matches Emacs, whose default wrap is also a character wrap. But the GPU
has word-wrapped since it existed and nobody opted into losing it. The
alternative was a UAX #14 dependency, because a whitespace-based grid
wrap would give only APPROXIMATE parity against cosmic-text's Unicode
line breaking --- which is worse than honest divergence, since it looks
unified until it is not.
The audit strategy is asymmetric deliberately. The coordinate functions
gain a required context parameter so the compiler enumerates every call
site; DisplayCoord gains an additive sub_row so untouched consumers
stay CORRECT rather than merely findable. Compiler-enforced where
enforcement is possible, correct-by-default where it is not.
Also corrects two ledger headers that still said OPEN for PRs merged
earlier today (#218 at 09:59Z, #217's absorption; #219 at 13:41Z).
Neither lane is removed --- rule 4 removes a lane when its ARC is done,
not when a PR merges, and the QoL arc has this stage left. Read each
block before cutting it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The zoom lane recorded `1e054f7` as the authoritative tip. Committing
that line advanced the tip to b645fe7, so the claim was false before it
was pushed --- self-invalidating, not merely out of date.
The ledger already says this, twice: the docs-absorption and
signal-integrity lanes both note that any edit to their block advances
past whatever SHA it records, so the REF is the thing to trust. I wrote
a new lane without following the convention the file states about
itself.
Removes the SHA and says why, so the next lane inherits the reason
rather than the rule alone.
The base SHA stays. 218d2e7 is a merge commit that already exists and
nothing here can move it --- it is a fact about where the branch
started, not a claim about where it is.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The lane went stale across two review rounds. It said framing revision
4, 13 acceptance tests, and "the three findings review caught" --- all
true when written, none true now.
Brings it current: revision 5, the pushed tip, 15 tests, and the two
later findings. The quantization one gets a real entry rather than a
mention, because its content is a REUSABLE fact about this codebase ---
ConfigKind::Number validates finiteness and bounds and nothing else,
and on_change cannot veto --- so any future setting needing a stronger
predicate than a range meets the same wall.
Also records the verification asymmetry that mattered: the raw-step
bite fails the new witness while the pre-existing 0.37 test still
passes. That is why it had to be a separate test, and it is the kind of
detail a lane summary usually drops.
New section for pre-checkout CI reds, which #220 hit three times. The
job dies in "Set up job" before actions/checkout; grepping the full log
for checkout/cargo/test-result returns 0. No code is fetched, so the
red says nothing about the commit in either direction --- re-running it
is a first execution, not a retry-to-green.
It gets a section here and not a registry row on purpose: matching
requires an exact test selector plus fragments, and no test ran. Making
it a row would mean deforming the row shape. Whether the registry wants
a non-row section for the class is left to its owner.
Written down rather than acted on silently because the registry's
opening instruction is to read it BEFORE attributing a red run to the
environment, and that is exactly the attribution being made here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Ctrl +/- had no effect whatsoever in the GPU frontend. Stage 1 (#219)
fixed what zoom did to the TUI; this is the other half.
NO RENDERING WORK. FontMetrics::scale already derived every GUI
dimension — code size, line height, status band, divider, menu rows,
minibuffer dropdown, gutter advance — and apply_font_facts already
re-metriced all seven buffers in one transaction. This drives the
preference that existed: two settings, three commands, and a restore.
Q#Z1 = (c). Relative zoom needs an origin and the daemon is built never
to know one — font_pref.rs is explicit that it "never learns metrics,
advances, or what resolves". Hardcoding 16.0 would put a pixel constant
on the daemon side; always sending a size would destroy the `None`
state for everyone who never zooms. A configured base is the only
option where the daemon still infers nothing, and the untouched path
stays byte-identical.
THREE THINGS REVIEW CAUGHT THAT REVISION 1 HAD WRONG.
Q#Z3 was not implementable as framed. `keymap_stack::Scope` is
Buffer | Mode | Global and carries no frontend identity, so "bind on
GPU frontends only" does not exist; and FrontendEvent has no
command-invocation variant, so the GPU cannot ask for a command by name
either. A global binding would capture the chord in the TUI and take
away the terminal's own zoom — the very thing the user is pressing it
for. Commands ship; the binding waits on capability-aware keymap
resolution, which is now a named follow-on rather than something
smuggled in here.
The restore seam did not exist. Builtins and init.lua both run BEFORE
install_state_dirs, so a pmacs.state.read at module load returns
nothing, always. saveplace and recentf never meet this because both
read lazily inside functions; zoom must apply with no user action,
which makes it this project's first eager state consumer. Restore lives
at the end of install_state_dirs — by definition the moment state
becomes readable, so it cannot be ordered wrongly and a future third
startup path gets it without knowing it had to ask.
Every size write clobbered the family. set_font replaces both fields
unconditionally, so { size = n } alone silently cleared a configured
family until restart.
BITTEN, THREE WAYS. Dropping family preservation fails 3 tests.
Reverting to the framing's own first parser `^(%d+)$` fails 4 including
the seam restore — it anchors to end-of-subject and rejects the
newline-terminated file the writer emits, which is the contradiction
review caught in the framing before it reached code. Hardcoding the
16.0 origin fails the base test.
Also recorded: a loaded crdt run failed two m6_1 PTY tests with
`stty -a output was: ""`. That is R4/R6's empty-content readiness
family, and it means the readiness-helper audit's scope is wider than
three wait_for_file copies under tests/ — src/process.rs's own tests
carry the shape. Undiagnosed, load-sensitive, green isolated and on a
quiet full run; a scope note for that lane, not a registry row, since
the registry judges red CI runs and these were local.
Verified: fmt, clippy, diff-check, --lib 1900/0, crdt 2085/0,
gui_zoom 13/13, journey 47/0, m4 150/0, gpu 221/0, full_grid 1/1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zooming a terminal with Ctrl +/- left the TUI showing the previous
frame through the new one. Q#FG1 = A, as approved.
THE RULE WAS ALREADY WRITTEN DOWN, ON A PRIVATE FIELD.
src/instance_render.rs:36 says remote frontends "must blank their local
buffer before applying the deltas" — the binding contract, in the one
place a consumer author will never look. The protocol type said only
that full_grid marks "the initial sync ... versus an incremental
frame": a label, from which no obligation follows. So FG-INV now lives
on InstanceMessage::CellDelta, where whoever writes the next frontend
reads it. A resync is a picture of the screen's INK, not of the screen.
The producer diffs against a blank grid, so a cell that should be blank
produces no span. src/frontend.rs then took `CellDelta { spans, .. }`
and discarded the flag. That was correct for exactly one frame — the
fresh-attach frame, which follows Frontend::new's Clear — and wrong for
every resize after, which follows nothing. A font-size change is the
worst case because the terminal reflows in place rather than dropping
content, so the maximum number of stale glyphs survive.
emit_cell_delta joins emit_span and emit_status_overlay as a pure
helper over a writer; apply_message routes through it. No struct
change, no generic parameter, no new pattern.
WHY SEVEN TESTS MISSED IT. Every one asserts the producer SETS the
flag; none asserted a consumer ACTS on it, and no runtime reader
existed workspace-wide. "Add a test for the flag" had already been
done and did not help. Handoff §5's enforcement-vs-documentation drift,
in a second register.
Three unit witnesses, each bitten independently. The empty-spans case
earns its own test rather than folding into the others: under the
plausible `spans.is_empty()` early return the ordering test still
PASSES and only that one fails — and an empty resync is exactly the
frame whose entire content is the blanking.
The PTY acceptance drives a real SIGWINCH, and its mark is anchored to
CONTENT rather than time. A time-based settle was written first and is
unusable: a settled pmacs screen emits per-frame bytes forever, so
"output stopped growing" never becomes true. Anchoring just past the
first painted byte excludes both startup clears by construction —
Frontend::new clears before any frame exists, and the first frame is
itself a resync whose clear precedes its own spans. Bitten against the
original defect: 34,831 bytes after the first painted frame, no CSI 2 J
anywhere in them.
What it does not prove, stated here rather than found in review: the
suites assert on raw bytes, with no screen model and no vt100/termwiz/
vte dependency. This shows pmacs emitted a blank at the right moment,
not that the screen ended correct.
Verified: fmt, clippy, diff-check, --lib 1900/0, crdt 2085/0, m4 150/0,
gpu 221/0, and the grid-driving suites — full_grid_resync 1/1, vterm
1/2/3 9+9+5, m5_5 15, m5_8 5, bottom_panel_stage1 47.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. THE CENTRAL RULE LACKED ITS TIME QUALIFIER. It said any red matching
a retired row is a recurrence — under which this PR's own R2 finding
reopens R2. Now: a match POSTDATING the retirement challenges the
disposition; a match predating it corroborates. That is not a
technicality. An occurrence scan reaches backwards by construction, so
most matches it finds are the earlier kind, and the old wording would
reopen every retired row the first time anyone scanned.
2. R5 AND R6 SAT UNDER "RETIRED ROWS" while declaring themselves live
and undiagnosed. I inserted them before R2's heading, which put them in
the wrong section — presentation contradicting classification in a file
whose whole job is classification. Moved under Live rows.
3. RECOVERY ANCHORS STILL DECLARED 12f2970, so the check accepted a
checkout lacking #216 and #217 while the same file described both as
complete. Advanced to db1bbe9, with the ancestry verified rather than
assumed.
4. THE HANDOFF'S DURABLE TREE FACTS STILL SAID IDS ARE OPAQUE AND
COMPARED BY EQUALITY — the contract ef99b64 deliberately narrowed. A §5
lesson explaining a correction does not fix a summary that still states
the uncorrected fact; the summary is what a new agent reads first.
Corrected there and in the header, which also still called Stage 2 in
flight and anchored main at f186253.
AND ONE FINDING FROM RE-EXERCISING THE RECOVERY PATH RATHER THAN
SWAPPING ITS SHA. This file claimed `git worktree add <path>
githubsucks/<branch>` fails with "fatal: invalid reference". It does
not fail. On git 2.55.0 it SUCCEEDS and leaves a detached HEAD — no
branch, no upstream.
Still use -b, but the reason is the opposite of the one recorded: the
hazard is not an error that stops you, it is that nothing stops you.
Work committed there sits on no branch and is not pushed by a bare
`git push` — the "uncommitted work does not travel" hazard wearing the
shape of committed work. A documented error message that never appears
is worse than no documentation, because the reader waits for a signal
that is not coming.
Verified: fmt, diff-check, --lib, listview 26/26. Recovery path re-run
from an empty directory at the new base; all four steps clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ledger requires a lane for every open PR. #171 drifted 153 commits
while invisible here and #215 had no lane until review caught it, so
the block goes in with the lane's own commits rather than after someone
asks for it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RULE 4 REMOVES A LANE AFTER MERGE, and two were overdue: the tree
primitive (#217) and macOS CI signal integrity (#215, #216).
The macOS arc could not simply be deleted. It owned R1 and R3, and a
lane removed while it still owns undone work does not close that work,
it hides it. R1 goes to a new async-runtime block; R3 goes to the
reap-ledger lane, which already parks every disposition change pending
exactly its question. Re-homing first is why this did not happen at
merge.
AN OCCURRENCE SCAN (last 25 main runs: 23 green, 2 red) found both reds
unrecorded, and I misattributed both on the first pass — by theme
rather than by required fragment, which is the exact error the registry
exists to prevent.
- Run 30710662474 is NOT R3. Same test, same EPERM, same
measured_group=unobservable(ESRCH...) — but R3 requires `leader=live`
and this reads `leader=exited(signal SIGUSR1)`, R2's exact fragment.
It is a second R2 occurrence, four days BEFORE R2's retirement, so it
corroborates the row rather than falsifying its disposition. It also
adds something: macOS luajit where R2's evidence was lua54, so the
mechanism was never flavor-specific. Filing it as R3 would have
attached a live possible product defect to an occurrence of a fixed
test race.
- Run 30555667095 is NOT R1. Different test, different module,
different assertion; they share only "supersede under a deadline on
macOS". Sharing a subject is not sharing a signature. Filed as R5,
undiagnosed.
R6 is the acc28 readiness timeout from #217's CI. Its scope is the
AUDIT, not the call site: three independently written readiness helpers
now exist and they disagree, with bottom_panel_stage1's carrying only
the zero-byte half of #216's hardening. R4's disposition predicted this
recurrence under a new selector. Patching acc28 alone repeats the
mistake this arc already made once, when the empty-file predicate was
fixed in one helper and left in its neighbour.
The registry carries a rate for the first time — a floor, not a
measurement: main only, 25 runs, readable reds only.
Four durable lessons to the handoff, two from the tree review: an
optional field the shape depends on is not optional, and a contract two
mechanisms must honour is only as strong as the weaker one.
Verified: fmt, diff-check, --lib, listview 26/26. Docs-only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#216 landed while this branch was open. Both conflicts are in the
ledger and the handoff, and both sides had independently written up the
same shared-CARGO_TARGET_DIR hazard.
THE TWO WRITE-UPS ARE NOT ABOUT THE SAME OCCURRENCE, and merging them
carelessly would have been a real error. Stage 2's is established: seven
failures against a clean baseline, failure text naming its own cause,
pgrep confirmation, and a dedicated-target-dir re-run at 41/41. This
lane's is the one whose signatures were destroyed before being read —
it has no captured text to match against Stage 2's, and it keeps two
non-causal hypotheses. A mechanism established in one occurrence is not
evidence about a different occurrence that was never characterized, so
the merged bullet says so explicitly rather than letting proximity
imply it.
The ledger records #216 merged and stops saying Stage 2 is in flight.
It does NOT retire the arc, though rule 4 now would: R1 belongs to the
async-runtime lane and R3 is an unresolved possible product defect for
the process-signal lane, and neither has a block yet. Re-homing them is
an absorption pass — not something to fold into a feature PR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It said "Unpushed while held" while githubsucks/tree-primitive-framing
sat at the same tip as HEAD. Held means no PR is open; it does not mean
the work is stranded locally, and the handoff's portability rule cares
about the latter.
The commit list also claimed to be exhaustive and then excluded the
commit that updated it — a list of that shape is wrong the moment it is
written. It is now the substantive arc, with `git log` named as the
place to get the complete one, and the remote ref rather than a pinned
SHA named as the authoritative tip: any edit to this lane advances past
whatever SHA the lane records, including this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FOUR REVIEW FINDINGS, and the first two were right about the tests.
1. THE SELECTION ACCEPTANCE WAS VACUOUS. `tr_1` toggles the selected
root, which sits on line 1 before and after collapsing — so it passes
unchanged under the line-based re-seating that id-keyed re-seating was
built to replace. It proves collapse hides descendants; it proves
nothing about selection. `tr_4` adds the case that discriminates: an
`on_refresh` inserts a row ABOVE the selected node, so the node moves,
and the assertion is that selection follows the NODE. Bitten by
restoring `seat_cursor(p, saved)`: `tr_4` fails with left " kid2",
right "sibling", while old `tr_1` passes — which is the finding,
reproduced.
The substantive assertion is deliberately ordered first. It was second
at one point, behind the fixture check that the node moved, and a
regression then reported as "the insert must move the selected node" —
reading like a broken fixture rather than a broken re-seat.
2. FLAT PANELS WERE NOT BEHAVIOUR-IDENTICAL. `bind_local_keymap` binds
TAB on every listview, so a depthless panel that previously fell
through to the global binding — and to Q#P3's read-only intercept —
began answering "listview: no node here". `listview.toggle` now
delegates to `buffer.tab` when no row carries an id, restoring the
prior path exactly; leaf feedback is kept for panels that really are
trees. `tr_3` asserts the absence of both tree messages rather than
merely that the panel still renders.
3 and 4 are documentation. The lane now lists 38e94dc, and no longer
says the PR is held "pending review of the documentation" that the same
commit supplied — it is held pending the decision to open it. §20 said
to BUILD the tree primitive while §14 already carried ◐; it now says
what actually remains, which is adoption: dired's `i` is the next
constraint source, DAP's variables view is why this was worth building
before them.
ONE RED, CLASSIFIED RATHER THAN RERUN AWAY. The crdt lib gate failed
`composition_overhead_under_ten_percent` at 30.7%. It is an incumbent
handoff hazard, and the branch cannot reach it — the diff versus main
touches no src/, no crate, no manifest. Alone it ran 5/5 green at
-0.6% to +0.2%; the next full run was green. Recorded in the handoff as
a MEASUREMENT, not a cause: five isolated greens establish that the
ratio is nowhere near the threshold when alone, not that contention is
what pushed it over. Not a registry row either — that file judges red
CI runs, and this was local.
Verified: fmt, clippy, diff-check, --lib 1896/0, --lib --features crdt
2081/0, listview 22/22, m4 150/0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three updates, one of which was a broken cross-reference of my own
making.
THE DAEMON-LEAK ENTRY POINTED AT A LANE THAT DID NOT EXIST. It said the
unclassified failure was "recorded in the tree lane below"; this branch
had no tree lane. A pointer to nothing is worse than no pointer — it
reads as though the record exists and sends the next reader looking. The
tree lane is added, and since it now sits ABOVE that entry the direction
is corrected too, with a second pointer to the framing §6a where the
occurrence is recorded in full.
The lane carries branch, base, framing revision, every commit, the gate
table with both sweeps and their exact reconciliations, the bite
verification for both behavioural claims, the held-PR state and the
recovery command. It also names what is NOT in scope, because the four
unadopted §14 consumers and dired's `i` will otherwise read as omissions
from this stage rather than as later ones.
COHERENCE.md §14: Tree moves ✗ -> ◐, implemented with ONE consumer. Not
✓, and the row says why: the LSP outline is the only adopter, dired's
`i` remains the deferral in its §13, and the other four named consumers
have not adopted. The organising fact goes in the section rather than
only in the framing — folding is LOCAL PROJECTION STATE, NOT A REFRESH
PROTOCOL, which is why a consumer with no `on_refresh` can fold at all.
The §0 scorecard row moves with the body; a grade table disagreeing with
its own section is the defect this document keeps correcting in others.
The framing's status moves from "approved" to implemented-and-gated,
held for PR review, and four durable facts go to the handoff §1: folding
as projection state; identity being consumer-supplied and compared by
equality, with `line:col` chosen because the `::` parent chain collides
on overloads; `has_children` having to read the full row array rather
than the rendered subset, since a collapsed node's children are absent
from it by construction and the bug would look like fold working and
unfold silently not; and that a bite which passes validates the pair
rather than the test.
The daemon leak stays a separate reap-ledger candidate. It is not tree
scope, it predates this work, and folding it in would make a lane
responsible for a leak it did not cause.
Verified: fmt, diff-check, listview 21/21, --lib 1896/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ACCEPTANCE 5, and it needed a real test rather than a weakened claim.
`listview_acceptance` says in its own header that the references panel
"needs a live LSP and is validated manually / via the m4 harness", so it
does not exercise `*references*` at all; the m4 hover test asserts
content PRESENCE, not exact output. Neither would notice a flat consumer
silently gaining an indent column — the regression a tree extension can
introduce. So the coverage is written against the real entry points
through the fake language server.
`*references*` is pinned EXACTLY: the row is the location string and
nothing else. `*lsp*` formats its own two-space indentation, so
"starts with a space" is not a violation there; what must hold is that
the primitive reproduces the consumer's text verbatim, matched as a
WHOLE LINE — a substring would still be found inside a further-indented
copy of itself. Volatile parts (pid, elapsed) are deliberately excluded,
the same normalization reasoning the CI registry uses.
THE FIRST BITE PASSED, AND THAT WAS THE FINDING. Injecting
`string.rep(" ", row.depth or 0)` did not fail the test — flat rows
carry no depth, so it added nothing. I had simulated a regression the
flat path is immune to and would have recorded the test as verified.
The regression this criterion actually guards is an UNCONDITIONAL
column, a fold gutter on every row; with that injected the test fails on
"the flat references row renders verbatim". A bite that passes validates
the pair, not the test — and injecting the wrong defect teaches nothing
while feeling like assurance.
A VERIFICATION RECORD, including one unclassified occurrence. The first
local crdt sweep of this branch reported 7 failures and its SIGNATURES
WERE DESTROYED before being read, piped through an aggregation that
emitted only totals. That is the failure the CI registry exists to
prevent, committed one lane after writing it, and it is why the cause
cannot now be established rather than merely being unknown.
It is recorded in this lane's own framing and deliberately NOT as a
registry row: that registry keys on a normalized signature, and an
occurrence with none would be granted a recognisability it cannot
support — the same reasoning that made the unevidenced incumbents audit
notes rather than rows.
Four re-runs are tabulated with what each supports. Two were not
isolated, including one where my own guard printed "aborting" and did
not abort. TWO GENUINELY ISOLATED RUNS ARE BOTH CLEAN, which supports
repeatability under isolation and establishes nothing about the cause.
Two mechanisms are recorded as NON-CAUSAL hypotheses, because both were
present and neither can now be tested: a shared CARGO_TARGET_DIR (whose
reciprocal case another lane observed independently, with `pgrep`
evidence and failing text that named its own cause), and ~40 resident
leaked daemons. Having two plausible mechanisms and no way to
discriminate IS the result; naming either would repeat the reasoning
this project has rejected — concluding something about an occurrence
from something that was not about that occurrence.
Both mechanisms are recorded as standing hazards in the handoff, and the
daemon leak gets its own candidate lane: 42 orphans, oldest four days,
reparented to systemd with deleted sockets, from
`gpu_invocation_acceptance`'s one-command tests, leaking 3-4 per sweep
as measured rather than estimated. It predates this work and belongs to
the reap-ledger family — a process outliving its supervisor with nothing
watching it — but the existing ledger arms only for `spec.group` and so
does not cover it.
Verified: fmt, diff-check, luajit sweep 3453/0 and crdt 3722/0, each
exactly +4 on its baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both heads that carry code are 14/14 green — `2d9c678` (the fixes and
the framing) and `668fc72` (this block). The tip row is explicit rather
than missing: a checkpoint table can never record the head that adds
the checkpoint, and #215's lane used the same convention. The branch
tip stays authoritative over any row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fills in what could only be filled in after the fact: PR #216, the
opening head's CI run, and the four-way sweep table. Both branch totals
are exactly +4 on their baseline — the four witnesses, and nothing else
moved.
**A shared `CARGO_TARGET_DIR` makes a local sweep unattributable, and it
bit this lane.** The branch `crdt` sweep first reported seven failures
in three suites while the baseline `crdt` sweep was clean. All three
spawn the REAL `pmacs` binary out of the target directory, and the
failure text named its own cause — "daemon does not advertise required
capabilities … start the daemon built with the `crdt` feature". A
concurrent `cargo test --workspace` in a DIFFERENT WORKTREE, at default
(non-`crdt`) features and the same `CARGO_TARGET_DIR`, had overwritten
`target/debug/pmacs` mid-sweep. Confirmed with `pgrep` while it was
happening, and discriminated by re-running the same three suites from
the same tree with a dedicated target directory: 41/41 green, then the
whole configuration swept again there. Recorded in the handoff's
standing hazards, beside the feature-blindness rule it rhymes with: a
feature-flavored binary is a shared mutable file, not an artifact
private to your invocation.
The clean re-sweep left ONE failure, and it is recorded rather than
rerun away: `lsp_dispatch_seams_acceptance acc33_...`. It is a new
incident by the registry's rules, and it is not attributable to this
branch on a STRUCTURAL argument rather than on its green rerun — the
only Rust change lives in `#[cfg(test)] mod tests`, which compiles into
the lib test target alone, so an integration-test binary linking the
non-`cfg(test)` lib is exactly what `main` builds. CI's `Test (crdt)`
job passed at the same head and runs that suite. The 15/15 repetition
set is the weakest of the five points, not the argument. No registry
row is opened: the registry judges red CI runs and keys on linked CI
occurrences.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stage 2 of `docs/macos-ci-signal-integrity-framing.md` revision 3
(acceptance 6–9). Two test races, both the same shape: the thing waited
for was weaker than the thing asserted, so the wait could return inside
the window the assertion needs closed.
R4 — `wait_for_file` returned as soon as `fs::read` succeeded, which
succeeds on a ZERO-BYTE FILE. The probe publishes with
`open(path,'wb').write(b'1')` and `open()` creates the file before
`write()` fills it, so the helper handed `[]` to a caller asserting
`== b"1"`. It now takes the expected bytes and waits while the file
holds a STRICT PREFIX of them — the states a write in flight can be
observed in — returning anything else immediately so the caller's
`assert_eq!` stays the discriminating assertion rather than becoming a
timeout inside a helper that does not know what was expected.
All four callers pass their expectation. `wait_for_published_file`, one
function away in the same suite, gated the real-TUI smoke's
`assert_eq!(…, b"1")` on the identical predicate and is fixed with it:
leaving it would have let R4 recur under a different selector, which the
registry would then have had to judge a new incident.
R2 — the USR1 fixture waited on `ProcessEventKind::Started`, emitted at
SPAWN, not when `/bin/sh` has parsed `trap '' USR1`. SIGUSR1's default
disposition is terminate, so a signal inside that window kills the
child. The child now publishes a marker AFTER the trap and the test
waits for that marker's CONTENT (the same zero-byte trap applies to a
shell's `>` redirection). `exec` replaces the forked `sleep`, so the
group holds exactly one process and the ignored disposition survives by
POSIX rather than by the shell's fork-suppression optimization — an
unstated dependency the old fixture had, since these signals are
group-directed and a forked `sleep` is an untrapped group member.
Four witnesses, each verified by REVERTING the fix and observing the
failure rather than by reasoning about it:
- `wait_for_file_does_not_return_a_zero_byte_readiness_file` fails
`left: []`, `right: [49]` — R4's two required fragments, verbatim;
- `wait_for_file_does_not_return_a_partial_write` fails on the torn read
a length check alone would admit;
- `wait_for_file_returns_divergent_content_rather_than_timing_out` fails
against an over-strict helper that waits for an exact match;
- `usr1_readiness_waits_for_the_trap_not_for_the_spawn` fails
`left: Some("SIGUSR1")`, `right: Some("SIGTERM")` with the readiness
wait removed. Its fixture sleeps before `trap` so the pre-trap window
is deliberate rather than load-dependent, and it proves survival by
the child's EXIT DISPOSITION rather than by an absence observed within
a window.
R1 is NOT touched — referred to the async-runtime lane (Q#MCI3), because
widening its budget would make it pass and measure nothing more. R3 is
NOT touched and remains UNRESOLVED, owned by the process-signal /
reap-ledger lanes.
`docs/ci-red-signatures.md` moves R2 and R4 to a "Retired rows" section
with their dispositions and adds the rule the file needed and lacked: a
red matching a retired row is a RECURRENCE that puts the retirement in
question, never a known flake. `docs/active-work.md` carries this lane
from its first commit rather than after review asks for it.
Repetition sets, not single runs: the two `--lib` process tests 15/15,
the whole `vterm_stage2_acceptance` suite 15/15 at default parallelism.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three corrections.
"NO OCCURRENCE WAS EVER OBSERVED" OVERSTATED WHAT THE AUDIT CAN SAY.
Someone may well have seen one of these fail and simply not recorded it;
what is established is the absence of a RECORD. The framing now says
"linked or captured" and states the distinction explicitly, because an
audit that claims to know what nobody saw is making the same kind of
unfounded assertion this lane exists to remove — one level up.
A missing closing quotation mark in acceptance 3.
THE LANE CHECKPOINTED THE REVIEWED HEAD, WHICH GOES STALE ON THE NEXT
PUSH — the exact mechanism by which #171 became invisible while it
drifted 153 commits. Recording one head is a snapshot; what a resume
ledger needs is the sequence. It is now a table of head -> CI run ->
result, newest last, with the rule stated: the branch tip is
authoritative over any row, verified by `git rev-parse`, because the
table is written by hand and the tip is not.
Verified: fmt, diff-check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five corrections, one blocking.
BLOCKING — the contract, not the implementation, was what needed
changing. Revision 2's acceptance 3 offered a binary: carry an incumbent
in with a signature and evidence, or remove it as never substantiated.
Stage 1 shipped a THIRD state because both incumbents are neither. The
framing is now revision 3 and names all three states, because a
governing criterion that says "two" while the branch does five is the
framing describing something that does not exist.
"MECHANISM NAMED" OVERSTATED THE EVIDENCE, and the phrase is retired.
The a33 audit proves an assertion string exists; the m6_8 audit proves a
test is timing-based. NEITHER ESTABLISHES A FAILURE MECHANISM — no
occurrence was ever observed, so nothing is known about how, or whether,
either fails. They are now AUDIT NOTES A1/A2 rather than registry rows,
with `R`-numbers reserved for signatures carrying linked evidence. The
distinction is not row-versus-weaker-row: a row says "this was seen,
here is the evidence", a note says "someone recorded a belief and no
occurrence backs it". Both remain unmatchable, so a red in either test
is a new incident.
R2's SIGNATURE WAS WEAKENED AND IS RESTORED. Splitting
`leader=exited(signal SIGUSR1)` into `leader=exited(` plus `SIGUSR1`
would match a child that exited by some OTHER disposition while SIGUSR1
appeared elsewhere in the output — precisely the name-style
over-matching this registry exists to refuse, reintroduced one level
down as fragment-style over-matching. It is one exact fragment again,
and the row says why.
THE HANDOFF STILL ISSUED LIVE IMPERATIVES. Its historical block opened
with "rerun isolated before treating a sweep failure as a regression"
and closed with "rerun the test alone before investigating", so the
supersession note I added sat between two instructions it contradicted.
Both are rewritten as record: the block now reports what that lane
OBSERVED, and the retired instruction is marked retired with its reason
— an isolated green reproduces nothing about a load-sensitive failure.
#215 HAD NO LANE, in the file that requires one for every open PR and
records why: #171 drifted 153 commits while invisible there. That is the
same defect, caught in review rather than 153 commits later. The lane
now carries branch, base, PR, reviewed head, what Stage 1 ships, the
verification, what Stage 2 owes, and the recovery worktree command. Both
snapshot headers are bumped.
Also narrowed a claim the PR body overstated: the registry is NOT
macOS-only. All four EVIDENCED rows are macOS, which is a property of
these occurrences; A1's job is `GPU Render (headless)`, which runs on
Ubuntu, and A2's job was never recorded. A future row from any job
belongs in the same table.
Verified: fmt, diff-check, clippy, --lib 1896, --lib --features crdt
2081.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stage 1 of the macOS CI signal-integrity lane. `docs/ci-red-signatures.md`
is now the single authority for judging a red CI run.
NOT NAMED "FLAKES", DELIBERATELY. One of its rows is a possible product
defect, and a filename calling it a flake would confer immunity the
evidence does not support.
A ROW MATCHES ON SIGNATURE, NEVER ON TEST NAME. All three of selector,
job/flavor and every required fragment must hold. Fragments are
normalized rather than pasted: PIDs, elapsed times and rendered
OS-error suffixes vary between runs, so a verbatim key would match
nothing, and where a fragment lists alternatives (ESRCH / "No such
process") those are one condition rendered differently by platform. A
failure in a listed test WITHOUT that row's fragments is a new incident.
The process test is why that rule exists: it produced TWO signatures
with different mechanisms and different causal status, and only one is a
test bug. Four incidents, three tests, four signatures — the registry
counts signatures.
THE RERUN RULE IS REPLACED, NOT SOFTENED. A green rerun establishes
INTERMITTENCE ONLY — never environmental cause, harmlessness, or
retirement. The same signature again is a second occurrence and stays
blocking pending investigation or a merge-base control. A different
signature is a new incident.
RETIREMENT IS CAUSAL. A test race retires by hardening that removes the
mechanism plus a discriminating witness; a measurement-design row by its
owning lane replacing or justifying the measurement; an unresolved row
by diagnosis and disposition. Main-branch greens accumulate as
occurrence evidence and retire nothing.
THE AUDIT FOUND A THIRD CATEGORY the framing's acceptance 3 did not
anticipate. It said each incumbent is either carried in with a signature
and evidence, or removed as never substantiated. Both incumbents are in
between: the tests are real and the mechanisms plausible — a33's "blue
pixels" is a genuine assertion string at pmacs-gpu/src/main.rs:17973,
and m6_8 exists and is timing-based — but NEITHER HAS A LINKED
OCCURRENCE. Deleting them would discard a real observation; carrying
them as peers of the evidenced rows would grant exactly the reputation
this lane exists to deny.
They are recorded as "mechanism named, no occurrence recorded", with it
stated that nothing there confers known-flaky status and that a red
matching one is a FIRST recorded occurrence to be investigated. R6 goes
further: with no signature ever captured it CANNOT BE MATCHED AT ALL, so
a red in that test is a new incident by default. That is the correct
outcome for an entry that never carried evidence.
LIVE POLICY IS CENTRALIZED; HISTORICAL EVIDENCE IS NOT MOVED. The
handoff's hazards rule becomes a pointer. Its landed-lesson block at
§5 keeps its own evidence and gains a note that the registry's rerun
rule supersedes "rerun isolated". active-work.md's two mentions are
verification records for the reap-ledger and bottom-panel lanes and are
left alone; only the CI-weakness block's triage half points at the
registry, because the job-cost question there is genuinely separate.
Verified: fmt, diff-check, clippy with and without crdt, --lib 1896,
--lib --features crdt 2081, pmacs-protocol 19, m4 149, required GPU 221.
All three tests the registry names pass locally on Linux, which is
consistent with every row being macOS-only and is not evidence about
any of them.
Docs only. Stage 2 (hardening) is a separate PR.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rule 4 removes a lane when its ARC is done AND its durable facts are in
docs/agent-handoff.md. Both hold: #213 merged, and §1 carries the five
facts Stage 3 produced. The lane goes; the distribution and CI-CRDT
lanes stay, rewritten, because each keeps named follow-ons.
The canonical base and the RECOVERY FLOOR both advance to f186253. The
floor moving with the base is this file's own rule, and the reason is
worth restating: a check that accepts an older commit than the declared
base passes on a tree the rest of the file does not describe. The three
places that named cfc1710 — the base line, the prose, and the `git log`
verification step — now name f186253 together, because a floor that
advances in one place and not the others is worse than one that does not
advance at all.
TWO CI WEAKNESSES STAGE 3 EXPOSED, recorded in the crdt lane that owns
that job rather than lost with the lane being retired:
* CI does not pass --no-fail-fast, so a multi-suite break reports as a
single-suite one. #213's first crdt failure showed ONE suite where a
local --no-fail-fast sweep of the same tree showed THIRTEEN. The
Stage 3 census hit the identical trap and recorded it; CI has it
too.
* The crdt job pairs the heaviest build — lavapipe, full workspace
with crdt, the largest test count — with real-PTY smokes on
5-second deadlines. #213 saw two different such suites fail on two
runs of the same commit; one was a real regression, the other was
load. That ambiguity is the actual problem: a job where noise and
signal look alike trains people to rerun rather than read.
Neither is a defect and neither is fixed here. They are decisions about
what the crdt job should cost, and they belong to whoever picks that
lane up.
Two ops lessons move to the handoff §5, both learned the expensive way
on #213:
* A LOCAL SWEEP IS BLIND TO WHICHEVER FEATURE CONFIGURATION IT DOES
NOT BUILD. Stage 3's census and every verification sweep ran
`--features luajit` without `crdt`, so compile_mode_crdt_acceptance
reached CI broken. Sweep both configurations before calling a corpus
green.
* "MY CHANGE MADE THIS FRAGILE" IS A DIFFERENT FINDING FROM "THIS WAS
ALWAYS FLAKY", and only one of them is yours to fix. Two CI runs on
one commit failed different PTY/GPU suites — the load-sensitivity
signature, on suites the flake list already names. The tell that it
was neither: the failures kept landing on GPU TERMINAL tests, and
terminal placement was what the PR changed. Ask which tests and why
those, before reaching for a rerun.
Docs only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Branch-plan step 6. The lane, the handoff, COHERENCE, and the framing all
still described Stage 3 as ahead.
COHERENCE §14's scorecard row and §20's Priority 5 both said the adopter
default flip was what remained on the panel. It is done, so the panel
half of P5 is closed and the TREE PRIMITIVE becomes the arc's successor —
§14 grades Tree ✗, and DAP's variables view is its next would-be
inventor. That sequencing is now stated where the next framing doc will
read it rather than living in a conversation.
The lane is RETAINED, not removed, because the PR has not landed; rule 4
removes it once Stage 3 merges and its facts are in the handoff. Both
conditions are half-met, and recording that honestly is cheaper than
discovering later which half.
Five durable facts move to the handoff §1, each of which cost a review
round or a defect to learn:
* A VISIT FROM A PANEL MUST NEVER USE THE RAW SWITCH.
`pmacs.window.switch_buffer` replaces the buffer in the ACTIVE
window, so from a panel it clobbers the panel itself. Q#BP11c is the
contract: after RET, `M-,` must FOCUS the still-present panel rather
than clone its buffer into the document — and an assertion on the
active buffer NAME alone cannot distinguish those two outcomes.
* AN OPT-OUT THAT DOES NOT SURVIVE REPLAY IS NOT AN OPT-OUT. Anything
that replays a stored invocation must store the escape hatch with
it, or the escape hatch is a one-shot that silently reverts.
* Compile's chords are PANEL-LOCAL, deliberately, and `M-x
compile.kill` still reaches the slot from anywhere. A global chord is
a command-surface decision framed separately.
* Two `q` mechanisms coexist by design — presentation history chains in
the side slot; `p.prev` prevents raw-switch and capability-fallback
loops. Neither supersedes the other.
* A CAPABILITY FALLBACK MUST STRIP THE QUIT ACTION TOO, not just the
side parameters. A quit action stranded on a document window makes a
later `q` try to restore a presentation that never happened.
The framing reaches revision 4 with the three defects the flip exposed
(§1.7a–c) and the census lesson (§1.6d): it counted FAILURES, not
CAUSES, and the two differed by an order of magnitude — thirteen listview
failures were one missing frame-geometry declaration. Read a census as
"how many assertions move", never "how many decisions are required".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review finding P1: Stage 3 was neither portable nor accurately
recoverable. `bottom-panel-stage3` had no upstream and no
`githubsucks/` ref, while docs/active-work.md still described Stage 3 as
"not started" — so a reader on another machine would have found a lane
claiming nothing existed, and no way to fetch the two commits that did.
That is precisely the failure this ledger records as its own rule:
"When a PR is opened, give it a lane", learned when #171 drifted 153
commits while invisible here. A branch without a PR is the same hazard
with less warning.
The lane now carries branch, base, the two completed steps with their
hashes, what remains, the verification at this head, and a recovery
command.
Review finding P2: the framing retained state the census superseded. It
still said "Nothing has been implemented or measured" and that the
blast-radius measurement "has not been taken", directly contradicting
§1.6b two screens below. Both bullets are rewritten to point at the
measurement rather than deny it, and the dired question is marked
DECIDED rather than left open.
Also records, in both documents, the thing step 2's original commit
message overstated: it is DEFAULT-PRESERVING WITH ONE INTENTIONAL
NORMALIZATION, not "behaviour-preserving". Every adopter kept its
default and the suite is byte-identical to baseline, but terminal's
invalid-input behaviour moved on purpose — and the sweep could not have
caught it, because every existing assertion passes an unknown STRING,
which takes the same path under both designs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two parts, one intent: record the release, and fix the user-facing
surfaces it falsified.
ABSORPTION. v1.1.0 shipped and nothing recorded it. docs/active-work.md
had no distribution lane, the handoff anchor still named c5f7501, and
COHERENCE.md — a REQUIRED framing input — still asserted "zero release
machinery exists" and graded journey step 1 as source-build-only. Both
are now false, and a framing doc written against them would have been
written against a lie.
* §0 scorecard: step 1 Partial -> Works; §17 Missing -> Partial; the
journey row moves off "steps 1, 11 and 12 remain the thin end" to
name 11 and 12 only.
* §17 ground truth rewritten: what Stage 1 shipped, and the nine
things it explicitly did not.
* §20 Priority 8: "State: zero" -> Stage 1 shipped, with the blocker
it named ("every other priority's value is invisible until this one
exists") recorded as LIFTED. Its next increment is a DECISION about
channels/update/signing, not a queued plan — worth stating so nobody
treats Stage 2 as pre-approved.
* A distribution lane in active-work.md, rewritten-not-removed because
the arc is not done.
Five durable facts move to the handoff §1, each of which cost something
to learn:
* a release build can produce FIVE binaries and three must never ship;
layer-2 exclusion is load-bearing, demonstrated when target/release
still held all three after building only two;
* `env!("CARGO_PKG_VERSION")` expands in the crate being COMPILED, and
three correct tests could not fail while two crates shared a number;
* pin release runners, and assert the glibc floor from the binary
rather than trusting the pin;
* a tag pushed before its workflow reaches the default branch does
nothing, SILENTLY;
* verify from the downloaded artifact, with a negative control — the
1,576 `loro` strings mean nothing without the control build's zero.
STALENESS, found by reading the surfaces a new downloader hits first.
`pmacs --help` claimed the TUI was "currently the only frontend;
reserved for the M4 GUI rollout, where `pmacs` will default to the GUI".
That is not merely dated — it is false in a release that SHIPS the GPU
frontend as a second binary. Rewritten to say what -nw actually does
(name the default explicitly for scripts and wrappers), and --gpu now
states its two real preconditions: a `crdt` build, and pmacs-gpu either
beside the binary or on PATH. Both are things a downloader can get
wrong and neither was documented where they would look.
Also in main.rs: the TLS attach line said "activation in v0.2" and four
doc comments dated themselves "v0.1" or "M4+" while describing behaviour
that is still current. The behaviour claims were accurate; only the
version labels lied, so the labels are gone rather than the sentences
rewritten. One comment gained a correction it needed regardless —
FrontendChoice is IN-PROCESS dispatch and the GPU frontend is not a
value of it, which the old text implied it eventually would be.
README status block: v1.0.0 -> v1.1.0, protocol v20 -> v21, and it
pointed at docs/roadmap-2026-07.md for "current direction" — a file that
opens by calling itself a historical snapshot and redirecting to the
handoff. It now points at COHERENCE.md and the handoff, and mentions the
arcs that landed since it was last touched.
Verified: fmt, diff-check, clippy with and without crdt, --lib 1896,
--lib --features crdt 2081, pmacs-protocol 19, m4 149, required GPU 221,
and `pmacs --help` rendered and read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two jobs: absorb the CI CRDT coverage lane now that #209 has merged, and
clear documentation drift that predates it and was found while reading
in on a new machine.
ABSORPTION. docs/active-work.md's lane is REWRITTEN, not removed —
rule 4 removes a lane when its ARC is done, and three named follow-ons
remain. It shrinks from 10,441 to 3,037 characters because rule 3 puts
durable architecture in the handoff and leaves only volatile state here.
The follow-ons, each with its reason:
* the macOS crdt leg, deferred pending first-run evidence that NOW
EXISTS and is favourable — the non-crdt macOS legs pass at 3,474,
thirteen fewer than ubuntu's 3,487 purely from cfg-compilation of
the Linux-gated process tests, and no crdt-specific failure appeared
anywhere;
* the --lib --features crdt flake, which did NOT reproduce in #209's
runs — but every run was --test-threads=1 and the trigger was seen
under PARALLEL load, so that is consistent with the drain_until
hypothesis rather than evidence against it. Its own PR: a
product-defect hypothesis, where all of #209 was configuration;
* the crdt_replica serde default, a third default mechanism that
disagrees with the Default impl in non-CRDT builds, untestable
without a self-describing format this crate does not depend on.
Five durable facts move to the handoff §1, each of which contradicted
something previously recorded: gpu-render runs a DIFFERENT PACKAGE than
the suites the old fix-shape wanted moved there; PMACS_REQUIRE_GPU is
absent from two of the four GPU suites; m10_10_perf is a CI-default
tripwire rather than a bench; --keep-going is what makes a clippy run an
inventory instead of a lower bound; and a feature can matter to a crate
a per-test census scores as unaffected.
DRIFT, all of it predating this lane and all found while reading in:
* COHERENCE.md §20's Priority 1 still read "runs to step 5; thin from
step 6" and called Stage 1b-3 "in flight", while §0's scorecard said
step 10 and 1b-3 had landed. §20 is a REQUIRED FRAMING INPUT per
CLAUDE.md, so a stale priority annotation there does not just sit
wrong — it misdirects the next framing doc written against it. Its
arc-cuts list carried the same two errors.
* docs/agent-handoff.md §1 described Discovery Stage 1 and Journey
1b-3 as "IMPLEMENTED, PR open" while §1a, twenty lines above,
correctly recorded both as merged. A file that contradicts itself is
worse than one that is merely behind.
* docs/side-quest-backlog.md, compiled 2026-07-14, listed #123 as an
open PR (merged 2026-07-21), #137 as "in review" (merged), and
HTML/CSS as an unshipped grammar (#146 shipped it AND its injection
consumers). It is the cross-cutting index, so a stale entry sends
someone to build something that already exists. Its north-star
section now says plainly that every original item has shipped and
that the file no longer sets direction — COHERENCE.md §20 and the
handoff §1a do, and both postdate it.
* docs/json-yaml-framing.md's status line advertised "PR #123 open and
awaiting review" for six weeks after that PR merged. Corrected in
the status line only; the revision-4 body is the as-reviewed text
and is left unedited.
No code, no workflow, no protocol change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Acceptance 8 in its strongest form. The ledger predicted the post-round-2
job totals from the census — 3,766 for Test (crdt) and 3,487 for
Test (ubuntu/luajit) — and run 30706324644 @ 71a1ebd reported exactly
those. Predicting the count before the run and matching it says more
than reconciling one afterwards: a vacuous leg would have landed near
the non-crdt job's figure.
Also records two readings that would otherwise be misread:
The macOS legs report 3,474, thirteen fewer than ubuntu's 3,487. That is
cfg-compilation, not lost coverage — the Linux-gated process tests
(setsid, the `bash -m` job-control corroboration) are not built there
rather than skipped.
And the head_sha trap bit again. Round 1's run (30705916037 @ 6519bc3)
was CANCELLED, not green: round 2's push superseded it, which is the
concurrency group working exactly as its comment in ci.yml describes. A
`gh pr checks` summary polled around that moment reported the PREVIOUS
run's results — with entirely plausible timings — and round 1 was
briefly reported as passing on that basis. This ledger already carried
the lesson from #178 ("verified by head_sha, not by the check summary").
It recurs because the wrong answer looks exactly like the right one, so
it is now recorded against this lane too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review finding (P2), and it is this lane's own defect class one level
down. Round 1 added a `-p pmacs-protocol --features crdt` step so that
crate compiles both ways. That step EXECUTED
`InstanceCapabilities::default` in both configurations and asserted
NOTHING about it: the crate's only use of that value is a transport
round-trip, and a round-trip is invariant to the values. An all-false
default — or one whose three fields disagreed with each other — encodes,
decodes, and passes identically in both builds.
Running code is not testing it. That is the same sentence this whole
lane is about, and round 1 committed the smaller version of it while
fixing the larger one.
Three tests now pin the defaults, and the split is the point:
* under `crdt`: multi_frontend, crdt_replica and semantic_render all
default true. Advertising false on a CRDT build would strand every
frontend in single-frontend mode.
* without `crdt`: all three default false. Advertising true would be
wire-protocol false advertising — those code paths are
conditionally compiled out.
* FrontendCapabilities::default is all-false in BOTH builds, and this
test is DELIBERATELY NOT feature-gated.
That third one pins an asymmetry nothing else did.
FrontendCapabilities derives Default and is feature-INVARIANT, while
InstanceCapabilities is feature-DEPENDENT. It is load-bearing rather
than an oversight: an instance advertises what it can do, a frontend
OPTS IN through the negotiation handshake, and a v1 frontend has no
local CRDT state regardless of how the crate it links was compiled.
Making the frontend side track the feature would have frontends
claiming support they do not have. A future edit that "makes them
consistent" now fails a test that says why not to.
All three fields are asserted separately rather than by comparing whole
structs, because they track one `cfg!` and a change flipping only some
of them is exactly the regression worth catching.
Bite-verified rather than assumed: mutating `multi_frontend` to a
literal false gives `FAILED. 18 passed; 1 failed` with the expected
assertion message; restoring returns 19/19. Both configurations now
report 19 tests, up from 17, with the correct cfg-gated test running in
each.
Left untested and recorded instead: `InstanceCapabilities::crdt_replica`
carries `#[serde(default = "default_true")]`, a THIRD default mechanism
that is unconditional and therefore disagrees with the `Default` impl in
a non-CRDT build. Exercising it needs a self-describing format and this
crate's only serde dependency is postcard, which is not one. Adding
serde_json as a dev-dependency to test a divergence this lane did not
introduce is scope creep.
Also corrects a note that round 1 made stale: the ledger's "do not
subtract the two jobs' totals" figures (3,485 and 3,747) were measured
BEFORE round 1 added the protocol step, and round 2 adds two tests to
that crate. Expected totals are now 3,487 and 3,766. The root-package
census is untouched at 3,467 / 3,746 — the new tests live in a sibling
crate, which is precisely the region scripts/feature-census cannot see.
Verified: fmt, diff-check, clippy on pmacs-protocol in both feature
configurations, and workspace clippy --features crdt --keep-going.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review finding (P2): docs/active-work.md and docs/agent-handoff.md both
still said "PR not yet open". This file's own rule is that an open PR
gets a lane recording its state — "When a PR is opened, give it a lane",
written after #171 drifted 153 commits while invisible here. Both now
carry PR #209, its six commits, and the first CI run.
That run is worth recording rather than merely citing, because it
discharges the one risk the framing could not close locally. ALL 14
CHECKS GREEN, including both new jobs (Test (crdt) 12m20s, M10 Perf
Gates (crdt) 5m40s) and the macOS/luajit leg that is the documented
flake surface. This was the first time in the project's history that
any of these tests executed in CI.
ACCEPTANCE 8 HOLDS AGAINST THE REAL RUN. Test (crdt) reported 3,717
passed / 0 failed / 30 ignored: the 3,746 all-targets census (with
basedpyright NOT skipped, as it is locally) plus one doc test, less the
30 ignored. The job demonstrably compiled and ran the crdt corpus rather
than reporting green over nothing — which was the whole point of writing
that criterion while the local sweep was already green.
A trap for the next reader is recorded with it: DO NOT SUBTRACT THE TWO
JOBS' TOTALS. Test (ubuntu/luajit) reports 3,485 and Test (crdt) 3,747,
a difference of 262 rather than 279, because the jobs run different
SETS — the non-crdt job adds pmacs-protocol's 17 tests. The dark count
is the all-targets comparison, 3,746 - 3,467. The wrong number looks
entirely plausible.
Chasing that discrepancy found a real gap, and one this lane's own tool
could never have surfaced: PMACS-PROTOCOL HAS ITS OWN `crdt` FEATURE.
It gates no tests, so a per-test census reports 17 either way and scores
the crate as unaffected — but it changes `cfg!(feature = "crdt")`
EXPRESSIONS inside InstanceCapabilities::default and
FrontendCapabilities::default, so those same 17 tests exercise different
runtime values under it. CI had only ever run the non-crdt ones. Closed
with an explicit `-p pmacs-protocol --all-targets --features crdt` step,
verified 17/17.
The blind spot is structural, not an omission, so it is documented at
the tool rather than fixed in it: scripts/feature-census censuses the
workspace DEFAULT MEMBER, because that is what a bare
`cargo test --all-targets` builds. Sibling crates are invisible to it no
matter what configs are passed. The durable lesson: A FEATURE CAN MATTER
TO A CRATE A PER-TEST CENSUS SCORES AS UNAFFECTED. Check siblings by
hand.
Verified: ci.yml parses at 10 jobs, script syntax clean, fmt,
diff-check, and the new protocol step green locally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Framing revision 3, plus the ledger and handoff updates the lane owes.
docs/active-work.md's "NEEDS A LANE" block is replaced rather than
annotated: the lane now has a branch, a framing, an implementation and
verification, so the old text's premise ("no branch, no framing yet")
was the part that needed removing. Its stale figures go with it — the
273-dark reading at 74301d1 and the seven-item clippy list, both
superseded and both explicitly labelled do-not-quote in the new block.
The corrections this lane produced, all now recorded where the next
reader will hit them:
* m10_10_perf is a CI-DEFAULT REGRESSION TRIPWIRE, not a bench. Its
bounds are deliberately generous "to catch catastrophic regressions,
not to verify a tight perf claim", so #[ignore]ing it to give it a
perf job would have reduced coverage inside a coverage lane. The
framing had this backwards through revision 1 and says so.
* gpu-render runs `cargo test -p pmacs-gpu` — a DIFFERENT PACKAGE from
the root-package GPU suites. The long-recorded fix-shape of "move
the GPU-requiring crdt suites onto gpu-render, it already has
lavapipe" does not work as written.
* PMACS_REQUIRE_GPU is not uniform: absent from
gpu_invocation_acceptance and gpu_initial_target_acceptance
entirely, so it cannot serve as blanket proof the GPU suites ran.
* The dark-test classification found THREE dispositions, not two:
benches awaiting a job, deliberately-manual operator tests, and
known-defect markers. Collapsing the second into the first gives a
CI job to tests whose #[ignore] reason says not to.
* §1.1's own target-column claim was wrong, and scripts/feature-census
caught it. Eleven targets run with zero tests under CI's flags;
eight of those gain tests under crdt and three are helper binaries
with no tests in either. The framing had merged two true statements.
Acceptance 9 is revised rather than quietly met at a lower bar. The
planned deliberately-broken-test bite is replaced by the --covers
coverage assertion plus the CI count reconciliation, and the doc states
what that trades: stronger on the claim specific to this lane (the old
job STRUCTURALLY cannot see these tests), and no longer proving that a
failing crdt test turns the job red — which is generic cargo and Actions
behavior, not anything this lane changes.
The handoff gains a census hazard beside the existing sweep note,
because every trap in it was hit while writing the script and one of
them survived two framing revisions: libtest prints `name: test` with no
space before the colon, a zero-test target prints only its Running line,
and both configurations need an --ignored pass or pre-existing ignores
get attributed to the feature.
The ledger also records what is NOT established, which is the lane's
whole remaining risk: the sweep is green SERIALIZED ON A DEVELOPER
MACHINE, and the failures this lane expects are hosted-runner timing and
concurrency. That green removes the "tests are wrong" explanation and
leaves the expected one untested. It must not be quoted as evidence the
CI leg will be green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nine PRs landed since the handoff's anchor (#199-#207) and it named
only four of them, so a fresh machine would not have learned that
ambient-root isolation exists, that the journey arc's 1b split
completed, or that the reap-ledger diagnostic landed. The ledger's
canonical base and recovery floor were five merges stale, and three
lanes described merged work as in flight.
Taken now because ZERO PRs are open. The ledger's own rule is never to
open a standalone refresh PR — because with several PRs open a lane
written on `main` re-conflicts at every merge — and this is the one
window where that cost is nil.
The handoff gains §1a, "Outstanding work — the whole board", which is
the point of the pass rather than a by-product: every arc against §20's
priority order with its next step, every open lane, every deferred item
attributed to the framing that parked it, and the standing hazards
someone running gates on a new machine needs — the three known flakes
by name, the basedpyright skip, the crdt sweep's build prerequisite,
and why a green a37 means nothing alone.
Two arcs completed, so per rule 4 their lanes are removed and their
facts are in the handoff: Journey Stage 1 and test ambient-root
isolation. Discovery and reap-ledger merged a stage each, so their
lanes are rewritten to the remaining plan rather than deleted — the
discovery lane now enumerates Stage 2 in dependency order and carries
the two Stage-1 facts a Stage-2 author would otherwise rediscover.
Base and floor advance together to `cfc1710`, per the file's own rule
that a floor accepting an older commit than the declared base passes on
a tree the document does not describe.
The recovery path was EXERCISED rather than asserted: from an empty
directory, clone, alias, fetch, floor check, and a lane worktree all
ran clean. The two-argument `git worktree add` still fails for a
remote-only branch, which is why every lane spells out the `-b` form.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
§7 said the classification comes first and alone, "its answer belongs in
review before any mechanical edit rides on it." The classification came
back at 342 sites across 66 of 97 files, and the whole-corpus migration
rode this PR anyway. That was a decision, not an oversight, and revision
5 records it as one so a later reader does not have to reconstruct it
from the diff.
The reasoning, in short: splitting would either leave 65 suites still
writing the developer's real data root while the seam sat unused, or ship
acceptance 12's ratchet with a ~65-file allowlist — and a ratchet
exempting most of the corpus records rather than ratchets. §7's ORDERING
is honoured (the census is the first commit); its implied SCOPING is not.
Revision 5 also records what review round 1 established about the shape
acceptance 12 needs: "a narrow, named allowlist" is not sufficient by
itself, because narrowness constrains which files are exempt and says
nothing about how far each exemption stretches. Exemptions carry counts.
`docs/active-work.md` picks up the same two facts and the PR number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Per §25 the audited claim this stage falsifies is updated on the
landing PR. §5's scorecard row and ground truth both said the
registration metadata was best-in-class with almost no way for a user
to reach it; eleven `help.*` commands now reach it.
The row names what is still missing rather than implying the concern is
closed: packages and workers have no discovery surface, `Command` still
has no title/category/aliases/flags, M-x rows are still bare name
strings, the predicate is still never evaluated, settings value
provenance is still absent, and there is still no help prefix key. The
original audit grade is kept inline for reference.
`docs/keybindings.md` gains the family table and records that the
commands are reachable by name and bound to nothing — the help-prefix
decision belongs to a later stage, for the reason #205 recorded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`docs/active-work.md`: the lane moves from FRAMING OPEN to
IMPLEMENTATION OPEN, with the implementation branch and worktree and a
recovery command that names them. The framing-only worktree is spent —
its doc is on `main` (#201).
`docs/agent-handoff.md` §3: a local full-suite run needs all FIVE
storage variables controlled, not four. `PMACS_STATE_HOME` outranks
`XDG_STATE_HOME`, so naming only the XDG four leaves a
higher-precedence state override live; and a run isolating only
`XDG_CONFIG_HOME` stops the `init.lua` reads while still writing through
the real data root — every local gate run in this repo before today had
that hole. `HOME` is deliberately excluded: it is the fallback the XDG
roots already cover once set, and it separately drives `~`-expansion,
which `find_file_acceptance` pins on purpose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Correct the M-x prompt census: six of the eleven canonical commands
need a second prompt, including describe-command. Make the apropos
substring negative discriminate against fuzzy matching with a concrete
non-contiguous fixture and a no-substring precondition. Update the help
index mutation to the twelfth canonical command, and carry all three
corrections into the active-work ledger.
No review findings at 1cc9d96; these are the two product choices the
framing deliberately left open.
Q#D2 resolves to `help.*` canonical with two forwarders. The family is
eleven commands under one prefix, so typing `help` at M-x surfaces all
of it — which is the discoverability win this arc exists for.
`editor.describe-command` and `editor.describe-setting` stay registered
as forwarders, so nothing in muscle memory or in
`docs/keybindings.md` breaks. Two forwarders are duplication of exactly
the kind §5 complains about; they are accepted as the bounded price of
not breaking documented names, and they carry a deprecation path. What
is not accepted is the split family revision 3 shipped.
Q#D3 resolves to substring. `fuzzy_score` is subsequence-based and
descriptions are long sentences, so a short query's letters almost
always appear in order — fuzzy would match nearly every command and
destroy the precision that makes apropos worth having. Acceptance 4 now
pins the decision rather than the intent: a subsequence that is not a
substring must find nothing.
Counts and pins follow the decision: the family is eleven throughout,
the `_show_help` counting stub expects eleven (the two renamed commands
are in it precisely because they were the pre-existing direct callers),
and the preservation pin now drives the forwarders through the real M-x
path — dropping them after the rename is the failure a user with muscle
memory would hit first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two factual corrections, both accepted and both verified in the code.
The custom completion source does not control display order. Revision 3
justified sorting the pool by claiming `Custom` candidates appear in
return order; `recompute_candidates` hands the pool straight to
`filter_and_sort`, which ranks by fuzzy score descending and breaks
ties lexically, so the source's order never reaches the user.
The sort is kept, for a reason that is actually true: `filter_and_sort`
applies `.take(CANDIDATE_LIMIT)` to the FILTERED iterator before
sorting, so when more settings match than the limit, pool order decides
which survive truncation. Registration order would make that vary with
an unrelated config edit; sorting makes it reproducible.
Read-only would not mitigate the foreign-`*help*` collision either.
Revision 3 implied it would. A buffer the user created and named
`*help*` carries no intercept of ours, so an intercept on the buffers
we create protects nothing — the renderer still matches on the name and
clears theirs. The missing guarantee is ownership identity: a private
table of buffers this module created, so found-by-name is not adoption.
`listview` carries it as `panels` and dired as its handle table; this
mechanism carries neither. Naming the wrong missing guarantee would
send a later fix at the wrong layer, which is why the correction is
worth its own paragraph rather than a word swap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two blocking, two major. All four accepted.
The ledger lane still said revision 1 and still carried the two claims
revision 2 refuted. The cause is worth recording: revision 2's ledger
edit was an assert-then-write block whose later assertion failed, so
nothing before it was written either — while the commit message
reported the whole change as done. This is the second time in this
project that shape has silently dropped edits. The lane is rewritten
from scratch and the result verified by re-reading the file rather than
inferred from an exit code.
`names_from` does not exist. Revision 2's completion source called a
helper nobody has written, over `pmacs.config.list()`'s descriptor
tables where `CompletionSource::Custom` consumes a sequence of strings.
Opening the prompt would have raised on an undefined global. The mapper
is now specified, and sorted — `Custom` presents candidates in the
order returned, and registration order is neither stable nor useful.
`*help*` has no read-only intercept. Revision 2 claimed one while
`show_help_text` writes with plain delete/insert, and #205 had already
recorded that this mechanism has not adopted the generated-buffer write
invariant. §3.4 now names the policies that really are shared —
reuse-by-name, wholesale replacement, the `q` binding, and the
foreign-buffer hazard — and notes that the last is precisely what a
read-only intercept would have mitigated and does not.
The naming was underspecified. With no `help.describe-command` in the
table, calling the existing `editor.describe-*` commands
"aliases-by-retention" was wrong on both halves: nothing forwards to
them and there was nothing for them to alias. They are now explicit
exceptions, the resulting split surface is named as a wart, and Q#D2 is
sharpened to the two ways out — forward the two under `help.*`, or keep
the family `editor.*` throughout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two blocking, two major. All four accepted and verified in the code
before the doc changed.
Completion does not close the free-text hole, and revision 1 said it
did. `resolve_accepted_value` returns the literal typed text whenever
no candidate is selected, so a non-matching typo still reaches
`on_accept` and the existing error path; separately, a fuzzy near-miss
can silently describe a DIFFERENT setting, which is a new failure mode
revision 1 did not name. Completion is reframed as assistance, the
acceptance pins both outcomes, and closed-set acceptance semantics are
named as Rust work rather than smuggled in as a side effect of adding
a source.
`invoke_interactive` is not the M-x path — the exact error #205
corrected one PR earlier. It rotates the interactive-command boundary
and calls the body; it opens no palette. The acceptance now states the
real path once and drives it: dispatch M-x, type the name, assert the
selected candidate BEFORE RET (accept does `session.take()`), accept,
and — for the five commands that take an argument — drive the second
prompt too. A pin that stops after the first RET has tested the
palette, not the command.
The `_show_help` seam is an output sink, not a migration seam.
`src/help.rs` has semantic renderers for command/key/buffer/mode/hook/
view and none for settings, lists or apropos, and the seam takes
already-flattened text, so a later migration still has to change each
command's subject-specific logic. The claim is narrowed to what is true
— one owner for Lua `*help*` writes — and paired with a per-subject
renderer function so the future Rust work is enumerated rather than
discovered.
Ground truth corrected: the missing-command list was eight and omitted
`list-settings` while §3.1 said nine; the site count said ten where
nine additions make eleven; `pmacs.keymap.lookup` does not return
`description` (it passes `cmd = None`); and the predicate sites cited
were `MenuItem` fields, not `Command.predicate`. The predicate
conclusion survives on the correct evidence — `src/help.rs:76` and one
assertion past `#[cfg(test)]`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`COHERENCE.md` §20 Priority 4, which §5 grades "substrate without
surface — the sharpest instance of §1.1" and calls the best
payoff-per-effort in the document. Nine describe/list commands over
introspection that already exists, picking up the family root `M-x
help` that #205 landed and documented for exactly this.
The stage adds no Rust. `pmacs.describe.*`, `pmacs.keymap.list()`,
`pmacs.command.list()` and `pmacs.config.list()` already return
everything the commands render, and `parse_completion_source` accepts a
Lua function as a `CompletionSource::Custom` — so `describe-setting`'s
free-text prompt, which today turns a typo into a status-line error
while `describe-command` has had completion all along, closes with no
new Rust either.
The consequential decision is a single rendering seam. `src/help.rs`'s
cross-referenced renderer is orphaned and the reachable Lua
`show_help_text` renders less, so nine new commands calling it directly
would turn a two-site migration into a ten-site one. Everything routes
through `pmacs.editor._show_help`, and an acceptance pin replaces that
seam with a counting stub — funnelling asserted, not merely intended.
Records that `Command.predicate` is stored and exposed but evaluated
nowhere, and deliberately does not start evaluating it: that makes
commands stop being invocable, which needs its own decision about what
"unavailable" means at each call site. A preservation pin registers a
raising predicate and asserts the command still runs, so a later stage
has to change that pin knowingly.
Adds no keybindings at all — the help-prefix question is taken once,
for the whole family, by the stage that can weigh it against the
constraint #205 recorded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Review round 2 on #205, one finding, accepted. #204 is this PR's base,
so three places still describing it as open were simply false.
- `COHERENCE.md` §2's step-6 row: "(PR open) ends the silence" ->
"(#204) ended the silence", and the "flips only on merge" clause is
replaced by the reason the row is still Partial at all — a server
that starts and then crashes is unsurfaced, which that landing did
not touch.
- `docs/agent-handoff.md`: the 1b-2 bullet says LANDED (#204), with the
same substituted reason.
- `docs/active-work.md`: 1b-2's per-stage block is removed rather than
relabelled, per rule 4 — it has merged and its durable facts are in
the handoff, which is the precondition for removal.
That last one exposed a structural problem the merge had created: 1b-2's
old lane body had come in *underneath* the unified journey header, so
the file carried two journey sections and the first one's text
described the second's predecessor. There is now one arc lane with
1b-3 nested under it as the only open stage.
Also corrects the reap-ledger lane header, which still read
"IMPLEMENTED, PR OPEN" after #202 merged. Same class of staleness,
noticed while editing the same file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Review round 1 on #205, two findings, both accepted.
The greeting was written straight into the registry without calling
`notify_buffer_edit`. The window's `TextView` had been indexed while
`*scratch*` was empty, and newlines are zero-width to a painter working
from a stale line index — so the first TUI frame collapsed the whole
three-line greeting onto row 0. Every buffer-text assertion passed
because the buffer content was correct; only the rendering was wrong.
The edit is now captured, the registry borrow released, and the core
notified.
The pin that would have caught it paints a real frame and asserts the
second line occupies its own row AND that row 0 does not contain it —
both directions, because a one-direction check passes when everything
collapses upward. Bitten by dropping the notify call: row 1 comes back
empty with row 0 holding the lot, and it is the only pin that fails.
Second: the project docs still described the arc as it was two PRs ago.
`COHERENCE.md` §20 called 1b-2 in flight and the welcome buffer
unstarted; its arc list said 1b-3 remained; and the ledger's journey
lane header still read "1b-2 PR OPEN" while the 1b-3 block carried a
mangled "Framing only; no code" line left by an earlier edit. All now
describe the PR-head state per §25.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
The journey suite conflicted additively — step 4 from this lane, step 6
from #204 — and both are kept: 44 pins now cover steps 2, 3, 4, 5, 6
and 9.
Per §25 the audited claims this stage falsifies are updated on the
landing PR rather than deferred: the scorecard's row 18 and §18's
ground truth both read "Missing" / "missing entirely", and a welcome
buffer plus a reachable cheat sheet makes both false. They move to
Partial. §2's step-4 row stays Partial, because `C-h` still deletes a
word and there is no tutorial.
§18's ground truth now records WHY `C-h` stays as it is, so the
help-prefix question reaches the discovery arc as a stated trade rather
than an oversight: non-kitty terminals cannot disambiguate
Ctrl+Backspace from Ctrl+H, so rebinding it would break Ctrl+Backspace
on every legacy terminal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two findings, both accepted.
The pinned API was crate-private while the pin was external.
`tests/journey_acceptance.rs` is a separate integration crate and
cannot call a `pub(crate)` function or match a `pub(crate)` enum, so
revision 3's acceptance 1 could not have compiled. `prepare_startup`
and `Startup` are now `pub`, which is consistent rather than expedient:
`run`, `EditorState::new`, `EditorState::open`, `install_state_dirs`
and `restore_desktop_if_armed` are already public, so the startup
sequence is public surface and this was the piece missing from it. The
alternative — keep it private and move the pin into `src/editor.rs`'s
unit tests — was rejected because §19 wants the journey row in the
journey suite.
The isolation that pin needs is now stated rather than left implicit.
`prepare_startup` deliberately calls `install_state_dirs`, which
resolves `PMACS_STATE_HOME` and XDG roots that tests cannot override
(`set_var` is unsafe and forbidden), so the pin inherits the standing
five-variable ambient-root requirement, asserts buffer content only,
and must assert that desktop restore was unarmed — a developer whose
real `init.lua` calls `desktop_mode(true)` would otherwise get a
restored scratch buffer and a silently different result.
The M-x acceptance still left its key assertion as an "or", and half of
it was impossible: `Minibuffer::accept` does `session.take()` and
resolves against the selected candidate, so after RET neither the
session nor the typed contents survive to be asserted. The framing now
specifies one observable, available before RET:
`pmacs.minibuffer.selected()` must equal exactly "help", then accept.
If the completion source selected something else the pin fails there,
naming what was actually selected, instead of passing on a help buffer
some other command produced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two acceptance holes and one doc correction, all accepted.
The real startup wiring was still unpinned, and revision 2 knew it: it
named the gap and then accepted it as residual, which is worse than
missing it. Deleting the sole `run()` call to `finalize_local_launch`
would have left every proposed pin green while shipping no welcome —
pin 1 called the seam by hand and pin 10 only proved constructors were
blank. That is the shape of a guard with no production caller passing
every direct-call test.
The fix is extraction, not disclaimer. Everything in `run()` from
`install_panic_hook` through the end of the attach-dispatch match is
terminal-free; `Frontend::new()` is where takeover begins. That prefix
becomes `prepare_startup`, which `run()` delegates to and which the
welcome pin now drives. Deleting the finalize call inside it turns the
pin red. What stays outside a test is `Frontend::new()` and the event
loop, which is where terminal takeover genuinely lives.
Acceptance 4 was not the M-x path. `pmacs.command.invoke` is the
programmatic API; M-x is `editor.execute-command`, which opens a
minibuffer with the `commands` completion source and calls
`invoke_interactive` only on accept. The pin now dispatches the chord,
enters the name, and accepts — and names the hazard that makes that
non-trivial: a selected candidate shadows typed text, so the pin has to
assert which command ran rather than trusting that some help buffer
appeared. dired refused a completion source on its own prompt for
exactly this reason.
Also corrects the #204 references: revision 2's history said it had
landed while the framing footer and ledger still called it open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Four findings, all accepted and verified in the tree first. The three
open questions are answered and folded into the design: three lines,
name `C-c c` and `C-c t` verified from structured entries, and the
command is `help`.
The startup seam was wrong, and every acceptance criterion rested on
it. `EditorState::new()` is not the no-argument entry point:
`EditorState::open` calls it before resolving the target, the daemon
constructs one too, user config runs inside it, and desktop restore
happens much later inside `run()`'s `RunLocal` arm. Greeting from
`new()` would greet a daemon, greet before a file argument replaced the
buffer, and precede anything config or a restored session put in
`*scratch*`. The stage now adds a launch-finalization seam called right
after `restore_desktop_if_armed`, using the `had_file` signal already
threaded to that point. Its untestable part — that `run()` calls it —
is stated rather than papered over, and bracketed by pins proving the
seam works and that no constructor greets on its own.
The step-2 pin amendment is withdrawn. Revision 1 analysed a
status-line welcome and then chose `*scratch*` but kept the amendment,
which was an internal contradiction. The status stays empty so the pin
stays true, and "no error text" has no defined predicate over an
unstructured status string — replacing an exact check with a fuzzy one
would weaken the ratchet to buy nothing.
The claim that this stage carried no §25 obligation was false. The
scorecard's row 18 and §18's ground truth both read Missing, and a
landed welcome plus reachable cheat sheet falsifies both: they move to
Partial on merge while §2's step-4 row stays Partial. A stage can be
too small to flip its journey step and still falsify a "missing
entirely" grade.
Acceptance 2 could not have been implemented as written. Scraping key
sequences out of prose is ambiguous — `M-x help` mixes a chord with a
command name, and `C-c c` is two chords whose boundary the text does
not mark. One structured entry list now both renders the text and
drives the `pmacs.keymap.lookup` checks, and `M-x help` is exercised
through the real palette path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
The last of the 1b split. Journey step 4 and `COHERENCE.md` §18: a
fresh `pmacs` greets the user with an empty buffer, an empty status
line, and no indication that `M-x` exists — the sole discovery
affordance in the product is knowing to press it.
Three findings shape the design:
- The existing step-2 ratchet pin collides with any welcome. It asserts
`status.is_empty()` while its own message says "reports no error" —
the same predicate only while nothing writes a non-error status at
startup. The framing corrects the assertion to its message's claim
and names that as the one existing assertion the stage changes,
rather than burying it in a diff.
- `C-h` is not free. It is bound to word-delete because non-kitty
terminals cannot disambiguate Ctrl+Backspace from Ctrl+H — both
produce byte 0x08 — so rebinding it to a help prefix would break
Ctrl+Backspace on every legacy terminal. §2's step-4 row calls it an
oversight; it is a deliberate trade with a stated rationale. The
help-prefix decision is deferred to the discovery arc with the
constraint recorded so it is inherited rather than rediscovered.
- A `*help*` buffer mechanism already exists over `src/help.rs`, so the
welcome can point at something real. Its two gaps are recorded rather
than inherited silently: it writes with delete/insert instead of
`set_generated_contents`, and it is found by name.
The welcome deliberately does NOT use `set_generated_contents`: that
lifts read-only, discards history and marks the buffer generated, all
wrong for the buffer step 5 requires the user to type into immediately.
Step 4 stays Partial — the row names a welcome, a cheat sheet and
`C-h`, and this closes the first plus a minimal second — so this is the
first 1b stage with no landed-evidence obligation on merge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Stage 1b-1 landed as #203, so the four places it deliberately left
saying "in flight" are flipped here rather than in a standalone docs
PR: this branch already touches all three files, and a separate PR
would re-conflict on every merge.
- `COHERENCE.md` §2's step-9 row: Partial -> **Works**.
- §2's keybinding-inversion paragraph: all three examples answered. The
quote itself is deliberately unchanged — it names a bias, and three
fixes do not retire a bias.
- §20 Priority 1 and the arc list: 1b-1 landed, 1b-2 in flight, 1b-3
remaining.
- `docs/agent-handoff.md` §1: IMPLEMENTED -> LANDED.
Conflicts were additive on both sides and are resolved keeping both:
the journey suite carries step 9 and step 6 (34 pins), and §24 keeps
both drift entries — the `ProjectKind::Cargo` naming error and §1.2's
wrong frequency note.
The two journey lanes are unified into one arc lane rather than one
being deleted. Rule 4 removes a lane when its ARC is done, and the
journey arc is not: 1b-2 is in flight and 1b-3 is unframed. Stage 1a
and 1b-1 are summarized there with their facts in the handoff, which is
rule 4's precondition satisfied rather than deferred.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Implements `docs/journey-stage1b2-lsp-guidance-framing.md` (approved at
revision 4, after three review rounds). Lua, tests and docs; no Rust
change and no protocol change.
`COHERENCE.md` §1.2's canonical silence: a preconfigured server that is
not installed failed with no status message, no record and no modeline
marker, while tree-sitter highlighting kept working and masked it. Now
the status line names the command, the language and the errno; the
modeline reads `LSP:!` instead of nothing; and `M-x lsp.status` renders
a durable `*lsp*` panel.
Half of this was already built. `status_buffer_text()` and
`last_error()` have existed since M4.8, exposed to Lua and tested, with
no production caller and no buffer to render into — several doc
comments already referred to "the `*lsp*` buffer" as though it existed.
The reporting shape was likewise already adopted twice inside
`lsp.lua`; the canonical case was silent because nobody had converted
it.
Three tables with three lifetimes, because one cannot do the job:
`reported` is never cleared and includes the command, so repointing at
another missing executable reports again; `failures` is cleared by a
successful spawn so the panel goes quiet on recovery; and a
buffer-keyed projection feeds the modeline, because that provider runs
for every window on every paint and deriving an affinity key inside it
would invoke root resolvers during painting.
The memo is on the report, not the failure: the spawn is still
attempted on every file open, so installing the binary mid-session
recovers with nothing to invalidate.
Adds `tests/lsp_spawn_guidance_acceptance.rs` (16 pins) and a step-6
row to the journey ratchet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
One blocking lifecycle gap, accepted. The registry's dispatch order was
read before specifying the fix rather than assumed.
`failed_attachments` is keyed by `tostring(buf)` and revision 3 never
said who deletes an entry, so killing a failed buffer leaked its
projection for the session — and that also made revision 3's sweep
bound ("at most the number of open buffers") false, since the table
could exceed the number of buffers that exist.
Nothing existing would have cleaned it incidentally. The LSP resource
reconciliation finds its work through `attachments_under`, which
iterates `attachments`, and a failed buffer has no attachment by
construction — that is the whole reason the projection exists. The gap
needed its own registration.
Revision 4 specifies:
- `pmacs.buffer.on_removed` registered once per projection, never per
failed attempt, since `attach_buffer` is reachable more than once for
the same buffer and per-attempt registration would stack callbacks —
the same unbounded-registrar shape with the leak moved rather than
fixed.
- The handle released on the success sweep, where the buffer is still
alive, but not from inside the removal callback: dispatch does
`callbacks.take(id)` and then iterates a local vector, so the entry is
already gone.
- Rename and delete CLEAR the projection rather than re-keying it.
The projection asserts "this buffer's server failed for affinity K",
and after a rename that is no longer known to hold — the new path may
be in a different project or none. Re-keying would assert a failure at
a location where none was observed, which is the error shape this arc
has been correcting throughout.
- The sweep bound now follows from the cleanup instead of sitting beside
it as an independent claim.
Adds acceptance 14-16 for kill, rename and delete, each asserting the
chosen behaviour rather than merely the absence of the forbidden one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two blocking, two cleanups. All four accepted, and both blockers
verified by running Lua rather than by reading it.
Recovery was inconsistent across buffers sharing an affinity. Revision
2 cleared `failures[K]` on a successful spawn but cleared only the
succeeding buffer's projection, so: buffer A fails, buffer B succeeds
for the same key, `M-x lsp.status` reports nothing wrong, and A's
modeline still reads `LSP:!`. Revision 2's claim that the two tables
are "written and cleared at the same moment" was false for exactly the
cross-buffer case, which is the normal case for a project with more
than one file. Each projection now carries its affinity key and a
success sweeps every projection holding it. The new pin asserts on A,
not B — a version checking B passes on the broken implementation.
The markerless key had no Lua representation. `key_uri` is deliberately
nil and `t[nil] = v` raises "table index is nil" — confirmed under both
LuaJIT and Lua 5.4 — so the central markerless criterion was literally
unimplementable as written, and leaving it to implementation would have
produced two ad-hoc encodings for the two tables. The framing now
prescribes one key function used by both, with a `u`/`n` discriminator
that cannot collide with any URI and a NUL separator (Lua strings are
8-bit clean, checked).
Acceptance 10 could not have observed what it claimed: making the
command resolvable changes no state by itself, since `failures` is
cleared by a successful spawn. The pin now reattaches before pressing
`g`, and says why that step is load-bearing.
Also corrects the ledger heading, which still said revision 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Review round 1 on #203, two blocking findings, both accepted.
The compile-directory assertions used the suite's lexical `canon()`,
but `pmacs.project.detect` canonicalizes before walking
(`canonicalize_or_passthrough`, `src/project.rs:509-511`), so the
compile cwd is filesystem-canonical. On macOS `/var` is a symlink to
`/private/var` and the two spellings disagree — both macOS legs failed
while Ubuntu, where `/tmp` is not a symlink, stayed green.
Fixed with a `detected_root()` expectation, and pinned by a fixture
that launches through an explicit **symlink** so lexical and canonical
paths disagree on every platform. That matters more than the fix: the
original bite ran only on Linux, where nothing could make the two
differ, so no amount of local mutation testing would have caught this.
The new pin is the only one that goes red when the lexical expectation
is restored.
Second: `COHERENCE.md` §2's step-9 row was flipped to **Works** and the
handoff said Stage 1b-1 was **LANDED**, while this PR is open. §25 is
explicit that grades change only with landed evidence, never
aspirationally. Both now describe the real state — the row stays
Partial and names #203 as the open PR that closes it, §20 and the arc
list say "in flight", and the handoff says IMPLEMENTED with the PR
number.
The flip is not dropped, it is owned: the active-work lane records the
four places to change on merge, because an unowned doc flip is exactly
how this ledger's drift starts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two blocking, three major, one minor. All six accepted and verified in
the code before the doc changed.
The affinity key was misstated. `ensure_server` sets `key_uri` only
when the root came from config or a marker walk, so a markerless file
keys on nil and loose files across unrelated directories deliberately
share one server per language. Revision 1's "(root, language)" would
have split what the runtime shares and re-reported one failure once per
directory. The memo now uses the real key and the stage changes no
reuse behaviour.
Dedupe and current-failure state were conflated into one record, and
revision 1 said nothing about recovery. One record cannot do both jobs:
keep it and `*lsp*` shows a failure the user has already fixed; clear
it and the message returns on the next file open. They are now two
records with different lifetimes, the reported identity includes the
command so repointing config at a different missing executable reports
again, and recovery is pinned in both surfaces.
The modeline provider is a pure per-buffer projection by design — it
runs for every window on every paint. Revision 1's "read the failure
table" would have made it derive an affinity key, invoking user root
resolvers and project detection inside painting. The failure is now
projected per buffer at attach time and the segment stays one map
lookup, with a preservation pin that counts resolver invocations.
"Adopt listview's idiom" is replaced by requiring `pmacs.listview.open`
and naming what it guarantees, including `on_refresh` — without which
`listview.refresh` early-returns and `g` is a bound key that silently
does nothing.
The acceptance had no journey ratchet row despite the stage making step
6 real; `tests/journey_acceptance.rs` states that steps 6-12 join as
later stages make them real, and 1b-1 added step 9 the same way. The
end-to-end row is now acceptance 1 and the M4 pins stay beneath it.
Also refreshes the ledger's canonical-base anchor and recovery floor
from `7586905` to `fbcf235`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`COHERENCE.md` §1.2's canonical silence, and journey step 6: a
preconfigured-but-missing language server fails with no status message,
no record, and no modeline marker, while tree-sitter highlighting keeps
working and masks it.
Three findings shape the design, all verified in the tree:
- The reporting pattern is already adopted twice inside `lsp.lua`
itself — root-resolver failures and subscriber failures both report
through `pmacs.editor.set_status` with the `pmacs.error` arm riding
along. The canonical spawn failure at `:658-674` was simply never
converted, so this stage finishes an adoption rather than starting
one.
- Half the stage is already built and unwired.
`LspManager::status_buffer_text()` renders "the `*lsp*` status
buffer", `last_error(sid)` exists, and both are exposed to Lua and
tested — with no production caller, no `*lsp*` buffer and no command.
Several doc comments already refer to that buffer as though it
exists.
- COHERENCE's frequency note is wrong, and the correction decides the
hardest question. It records the failure as firing once per project
root; `LspManager::spawn` returns early *before* both
`status_tracker.ensure` and `clients.insert`, so a failed spawn
leaves no record, the affinity loop cannot see it, and the real rate
is once per file open. Hence the rule the stage adopts: memoize the
report, not the failure — the spawn is still retried, so installing
the binary mid-session recovers with nothing to invalidate.
Records one limitation rather than hiding it: `status_buffer_text`
renders from `self.clients`, which a failed spawn never enters, so the
durable surface cannot yet show the failure natively. The record stays
in Lua for this stage.
Also logs four stale `COHERENCE.md` §1.2 citations for correction when
the stage lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Implements `docs/journey-stage1b1-compile-defaults-framing.md`
(approved at revision 2). Lua, tests and docs; no Rust change and no
protocol change.
`C-c c` now runs `compile.run`, and the first prompt is prefilled from
the detected project kind through `pmacs.compile.defaults` — seeded
`rust = "cargo build"` and extensible from `init.lua`. `_last` still
wins, so a session that has compiled keeps its own command.
The prompt CAPTURES its directory rather than re-resolving it. Sharing
one resolver between the prompt and the run is necessary and not
sufficient: `pmacs.minibuffer.read` is asynchronous and nothing freezes
the active window while a prompt is open, so two calls to the same
resolver at two different moments are still two different answers — the
user could be offered `cargo build` for A and handed a run in B by
clicking away mid-prompt. This is Journey Stage 1a's `commit_to`
discipline on a smaller seam.
`pmacs.compile.defaults` is public and assignable, so the lookup is
guarded: a throwing `__index`, a non-string entry and a non-table
replacement all degrade to the pre-stage empty prompt and never
prevent compiling.
Only `rust` ships seeded. Rust has one answer; npm/yarn/pnpm,
make/cmake, and `go build` versus `go test` do not, and a wrong prefill
costs more than an empty one.
Adds eight step-9 rows to the journey ratchet and five module pins to
the compile suite. Corrects `COHERENCE.md`, which named a
`ProjectKind::Cargo` that does not exist — the variant is `Rust`, line
77 is its doc comment, and Lua only ever sees the tag string "rust".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Review round 1, one finding, accepted.
"A failed escalation is never retried by anything" was false.
`shutdown()`'s force-kill loop iterates the reap ledger with **no**
`!entry.killed` guard, so it does re-kill an entry the escalation arm
gave up on. The accurate claim is that no later *tick* retries it —
`tick_reap_ledger`'s escalation is guarded by `!entry.killed` and never
fires again for that group.
The overclaim collapsed two failure modes that this lane exists to keep
distinct: a failed escalation leaks the group until editor exit, where
one more attempt is made, while a failed `shutdown()` force-kill leaks
it past exit with nothing left to try. Narrowed in the framing, the
handoff, the active-work ledger and the test commentary.
The corrected claim was asserted in three documents and pinned by
nothing, so it gets a pin: a failed escalation marks the entry, the
survivor stays alive across ticks, and `shutdown()` — with no fault
planned, so its force-kill really lands — still reaps it. Bitten by
adding the missing `!entry.killed` guard to that loop: the new pin
fails and the other five stay green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Not a new design round. Every bet resolved, and one acceptance turned
out to be satisfiable vacuously.
Acceptance 2's in-drain clause said "the live descendant's named late
output absent" without saying how the descendant stays live. It does
not, by default: `poll_one` TERMs the whole group on leader exit. The
first fixture's marker was therefore absent on both paths, and the pin
would have stayed green with the collapse fixed.
Bets 1 and 2 hold — every site took a directed outcome without
restructuring, and every consequence is reachable, so the lane does not
shrink. Bet 3 resolves the shutdown coupling as real and measured. Bet
4 is falsified exactly as its own clause anticipated: `ProcessEvent` is
keyed by `ProcessId` while the ledger is keyed by pgid and is
deliberately independent of managed records, every production consumer
polls per known id, and `take_all_events` has no production consumer at
all. Q#RL3 is answered accordingly and reporting is parked as its own
lane.
Also records the four sites and the two reusable lessons in
`docs/agent-handoff.md` — that a seam for a background loop has to be
directed, and that an absence assertion is only as good as its
fixture's ability to produce the thing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two blocking, two major. All four accepted and verified in the code
before the doc changed.
Sharing one cwd resolver between the prompt and the run does not
prevent drift. `pmacs.minibuffer.read` is asynchronous, nothing freezes
the active window while a prompt is open, and `run` re-resolved at
accept time — so the prompt could offer `cargo build` for A and execute
in B. Two calls to the same resolver at two different moments are still
two different answers. The interactive command now captures
`context()` and passes its `cwd` through to `run`. This is Journey
Stage 1a's `commit_to` discipline on a smaller seam: capture at request
time, never re-derive from whatever is ambient when the async work
lands.
No pin crossed the accept boundary. N1-N5 compared values the prompt
and the resolver had already agreed on, so a wrong directory inside
`on_accept` — exactly the defect above — passed every one of them. Two
pins now accept: N3 switches windows between prompt and RET and reads
the directory back two ways, and N4 accepts `cargo build` unedited in a
real Cargo fixture and asserts a clean exit.
N3's old falsifier was not discriminating: `project_root_of_active()`
already returns the innermost root, so re-detecting from it yields the
same kind and the pin would have stayed green. The mixed-fixture pin is
now N5 with a falsifier it actually catches, and says what it does not.
And §6.1 contradicted §3.1 — a Cargo workspace subdirectory holds no
`Cargo.toml` and correctly receives `cargo build`. The predicate is a
detected project, not a file in that one directory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`COHERENCE.md` §20 Priority 1 names Journey Stage 1b as the golden
journey's remainder and it had no branch, no framing, and no lane —
the only §20 priority with nothing in flight. This frames the first
third of it: journey step 9, build or test the project.
Scope is a global `C-c c` for `compile.run` plus a first prompt
prefilled from the detected project kind instead of empty. Lua, tests
and docs; no Rust change, no protocol change. LSP spawn guidance and
the welcome buffer are separate stages (§7).
Three ground-truth findings the framing rests on:
- `ProjectKind::Cargo` does not exist. COHERENCE names it twice; the
variant is `ProjectKind::Rust` and line 77 is its doc comment. Lua
matches the tag string `pmacs.project.detect` already returns, so
the stage needs no new Rust primitive.
- The compile cwd is resolved inside `run`, after the prompt closes,
so a suggestion built in the command's `fn` would obey a different
rule than the run. The stage extracts one resolution and has both
consume it.
- The last-resort cwd is `std::env::current_dir()` at call time, which
in tests is the pmacs repo root — itself a Cargo project. Negative
pins are built so that fallback is never reached.
Records one limitation rather than fixing it: after `pmacs <dir>` the
active buffer is dired's and pathless, so the cwd falls through to the
process cwd. That needs COHERENCE §8's execution-location model, not a
reach into dired's private handle table.
Adds the journey lane to the active-work ledger.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Refine the framing's in-drain test seam: a one-shot probe error cannot
survive the 50 ms quiescence interval, so the test-only override lasts
for one drain context and remains per-supervisor. Name the in-drain
SIGKILL as a non-observable local fact rather than adding an untestable
seam, and synchronize the revision-three ledger record.
The revision-2 commit shipped the framing alone. Its ledger edit was
generated by a script that aborted on a line-wrap mismatch before
writing, so `b881bd4` left the lane entry describing revision 1 — three
silent failures instead of four, no in-drain twin, no site-directed
seam, and the withdrawn "Bet 1 ships alone" staging.
Synchronized now: four sites including `final_drain_runtime` and its
distinct reader-cancellation consequence; journey step 9 rather than 8;
the `any_running()` precondition on the shutdown coupling; and the
first PR scoped as seam plus tests.
The failure mode is worth naming, because a partial write is exactly the
shape that leaves a document confidently wrong: the commit succeeded and
reported one file changed, which is easy to read as success when two
were intended.
Unparked from PR #200's §5, which retired the premise justifying the
ledger's leniency and deliberately changed no disposition.
Scouting found three silent failures where #200 named two. A probe error
of any errno drops the entry and cancels escalation; a failed escalating
SIGKILL is recorded as a success and never retried; and `shutdown()`
discards its own force-kill result identically — on the path written
specifically to stop a leak at editor exit.
Two facts shape the lane. `shutdown()`'s final loop terminates when the
ledger empties, which happens through the same silent drop, so the probe
error that hides a leak can also end the cleanup loop early — the probe
cannot be made strict in isolation. And all three paths are untestable
today: `tick_reap_ledger` and `shutdown()` call nix directly and consult
no injection seam, unlike `signal()`'s `forced_kill_errno`, so every one
of the five existing ledger tests exercises only the success path.
Diagnosis first; no disposition change is proposed. Stage A of the signal
lane had three tolerance rules rejected across three revisions, each for
concluding something about one entity from something about another, on
this same data structure.
The framing states plainly that none of the three has been observed:
#200 saw an explicit SIGTERM fail in `signal()`, not a ledger call.
Framing only. No code.
`docs/active-work.md` auto-merged without conflict — this branch's lane
entry sits above the folding lane, clear of the six blocks #199 removed.
Verified rather than assumed: all lane headers checked afterwards.
The merge surfaced a stale statement on `main`. The process-signal lane
still read "PR #200 open, four review rounds closed, held for review",
which stopped being true when #200 merged as `a2a92bb`. Its arc is
complete — Stage A #176 and Stage B #200 both landed, and the items its
§5 parked become their own lanes — so under the rule #199 established
(a lane retires when its ARC is done, not when a PR merges) it is
removed and recorded under "Closed since the last snapshot".
Doing that here rather than deferring it: this PR already edits the
file, and leaving a known-false statement on `main` to keep a PR
single-purpose is the wrong trade. The alternative was letting it stand
until the reap-ledger lane, which is scoped next and inherits from the
very §5 list being retired.
Review round 4: three blocking, one major. All accepted.
**`measured_group` was sampled after the failure.** It ran inside
`signal_failure_report`, after the `kill` and after `observe_leader`,
while the framing and the function's own doc both said before. A
concurrent group change would have made the diagnostic report
post-failure state as evidence about the attempted target. It is now
sampled in `signal` before the kill and passed into the report, so the
field describes the target that was attempted rather than the state the
failure left behind.
**The Linux corroboration did not exercise the production lookup.** Its
helper read `portable_pty::process_group_leader` — the accessor this
lane stopped using on the signal path — so `pty_foreground_group` could
have fallen back on every call with every test still green. Forcing it
to always fall back demonstrates the hole exactly: the corroboration
fails, and the injected pin PASSES, because the injected tests supply
the group themselves and structurally cannot detect a broken lookup.
The helper now calls the production lookup, and the corroboration forces
only the kill so the report is built from a real terminal read. The
residual limitation is recorded rather than left to the green: on macOS
`pty_foreground_group` has no end-to-end coverage, because the platform
cannot produce the precondition.
**The framing did not update its own acceptance contract.** Revision 5
recorded Bet 1's falsification in the revision history and in the bet,
and left the normative criterion demanding the real-shell rewrite — the
"implementation quietly diverges from the contract" shape this project
already recorded as a lesson on #191/#188. Acceptance 1 now describes
the injected pin, 1a adds the corroboration and its macOS limitation,
and 4 states the sampling order. The ledger is synchronized: revision 6,
four commits, 4,471 tests, bash armed on Linux only.
**`TargetSource`'s doc had the wrong classification.** Two of the four
variants target the leader pid, not one, and the pid-versus-group split
does not line up with PTY-versus-pipe — which is why the fallback needed
its own variant rather than reusing `LeaderPid`.
Gates: 11 gates, 4,471 tests, zero failures.
One blocking and two major findings, all accepted.
- The isolated gate still inherited `PMACS_STATE_HOME`, which wins over
`XDG_STATE_HOME` in `user_state_dir` (`src/state.rs:47-68`), so
controlling the four XDG storage variables did not isolate state on a
machine that exports it. The contract now names the exact five
variables rather than counting roots.
- Q#TI5 still called self-spawn the durable regression guard after
revision 3 had accepted that it proves behaviour, not continued
adoption. It now names both guards and their different jobs: self-spawn
is the behavioural proof, the checked source inventory is the adoption
proof.
- The active-work lane still said revision 2 and still prescribed an
isolated `XDG_CONFIG_HOME` alone; both are synchronized, and the stale
config-only gating note elsewhere in the ledger is corrected to say it
isolates the observed symptom rather than the gate.
Framing only. No code.
`docs/active-work.md`'s snapshot header was the only conflict; main's
absorption version supersedes this branch's. `docs/agent-handoff.md`
auto-merged, and both sides were verified present afterwards rather than
assumed: the absorption's `4cd4a7b` anchor and its new landed-lane
entries, and this branch's macOS occurrence with its run link.
This lane's ledger entry is updated from "revision 4 awaiting review" to
its actual state — implemented, gated, PR open — since the framing was
approved and both implementation commits have landed on the branch.
The full gate suite is re-run on the merged tree rather than inherited
from the pre-merge head. The absorption is docs-only, so a clean merge
is very likely correct — which is exactly when inheriting a green would
be most tempting and least justified.
Three blocking and two major findings, all accepted. Two were verified
by running the thing rather than reading it.
- **Journey isolation had no executable mechanism.** Cargo launches each
integration-test binary with the caller's environment, and a binary
cannot re-point its own roots before its tests run — the `set_var`
prohibition applies to itself. Revision 2's "isolated by its launched
environment" therefore assumed the external wrapper this lane exists
to delete. Now named and pinned: each journey test re-execs
`current_exe()` with `--exact`, a marker, and controlled roots; the
child runs the real body against the ambient `EditorState::open`, so
the production-entry-point ratchet is untouched while the roots are
contained. The same helper serves the hostile-environment check.
- **One self-spawning test is not a ratchet.** It proves the seam works
and cannot notice a raw `EditorState::new()` added to another binary
later. A checked source inventory, falsifiable by adding an ambient
constructor, is now acceptance 12.
- **The root list and the gates disagreed with the audit.** The scope is
now explicit — bootstrap STORAGE roots only (config, data, state,
cache). `HOME`'s non-storage semantics are excluded by decision, not
omission: `expand_tilde` resolves user-entered `~` and
`find_file_acceptance` pins it deliberately, so redirecting `HOME`
would retarget a user-facing feature. `XDG_RUNTIME_DIR` addresses
sockets, not stored data.
- **The gate instruction itself was insufficient**, and this is the
finding with immediate consequences: isolating only `XDG_CONFIG_HOME`
stops the reads and leaves the write path open. Every local gate run
in this repository today had that hole.
- **The count was one high and the ledger overstated it further.** 65
files call the constructor; 66 mention it. The 66th, `m5_6_acceptance`,
mentions it only to say it deliberately does not use it — making it
the third place in the suite documenting the `cfg(test)` gap. The
ledger's "all 96 test files load the real config" was false.
- **The recovery command did not work**, verified by running it:
`git worktree add <path> <remote-only-branch>` fails with `fatal:
invalid reference` after a bare fetch. Replaced with the explicit
tracking-branch form.
Framing only. No code.
Four blocking and two major findings, all accepted, all verified in the
code before acceptance. The lane's scope changes: it is about ambient
roots, not about init.lua.
- The read-only assumption was already false. `EditorState::new`
materializes bundled packages unconditionally and before config
loading, into `XDG_DATA_HOME` or `$HOME/.local/share`, and
`materialize_all` creates directories. Confirmed on the development
machine: `~/.local/share/pmacs/builtin-packages/` exists with v0.1.0
and v1.0.0. This upgrades the lane from "local gates lie" to "tests
write into real user data".
- The population count was wrong and its stated method did not match the
command that produced it: 18 from a grep for `Editor::new`, which does
not match the real constructor `EditorState::new`. 66 of 96 files
construct an editor directly.
- File-level classification cannot work: 5 files are both in-process and
spawned. Classification moves to construction sites.
- The seam must cover `EditorState::open`, which calls `Self::new()`
directly, while `journey_acceptance` requires that exact public entry
point to avoid a dead-production-path test. Resolved by isolating
journey through its launched environment rather than a different call.
- Isolated construction must still return `is_init_complete() == true`.
Config loading and `set_init_complete()` share one block, and
`m8_2_acceptance.rs:75` documents its dependence on integration-test
construction being init-complete — the `cfg(test)` gap is load-bearing
in that one respect.
- Revision 1 both proposed and parked a hostile-config CI leg. Resolved
in favour of a test-binary self-spawn, which travels with the test
rather than the workflow file, and which now also asserts the hostile
root is unmodified afterwards.
- The lane is recorded in `docs/active-work.md`, which revision 1 omitted
despite the volatile-work protocol requiring it.
Also documents six ambient roots where the shared daemon harness sets
two, and why setting HOME only isolates a root whose XDG variable is
unset — the harness's apparent adequacy is a property of one developer's
environment.
Framing only. No code.
Eight PRs merged on 2026-07-29/30 (#188, #190, #191, #194, #195, #196,
#197, #198) and `docs/active-work.md` had drifted to 1,854 lines, most of
it describing work already on `main`. Rule 4 permits removal once a
lane's facts reach `docs/agent-handoff.md`; this does both halves in one
commit so the precondition is satisfied rather than deferred.
Handoff §1: the head-of-`main` anchor moves from `6c9e765` to `4cd4a7b`
and its date from 2026-07-28 to 2026-07-30. New entries for the three
implementation PRs that had no record — #190's pre-filesystem delete
refusal, #191's Stage 1 adoption of the generated-buffer contract, and
#196's dired Stage 2a — plus #188 recorded as landed rather than
proposed.
Ledger: six merged lanes removed (781 lines). Two lanes are NOT removed
and are rewritten to their remaining plan instead, because their arcs are
unfinished: generated-buffer immutability (Stage 1 merged, Stage 2 not
started) and bottom-panel (Stage 2 complete, Stage 3 ahead). The
bottom-panel block said so in its own text — "this lane is not removed at
2B-3's merge" — so a removal keyed on "the PR merged" would have
discarded live planning. The header now states the rule as ARC-done
rather than PR-merged.
Two method notes are recorded in the ledger because they nearly cost
real work:
- A census by PR number is a proxy, not a measurement. Counting `#NNN`
in the handoff reported that five of these lanes had no record at all.
Counting by content found most already documented, with the real gap
being the implementation PRs while their framings were recorded. The
absorption written from the first count would have duplicated existing
entries.
- #194 and #195 keep their lessons in §3 and §5 rather than §1, which is
why a number search finds them once each. Sufficient under rule 3:
durable knowledge needs a home, not a required section.
Docs only. No code, no protocol change.
Integrate canonical main, restore PTY errno capture through the safe
filedescriptor duplication bridge, narrow the macOS occurrence to what
it actually measured, and make the job-control fixture foreground and
discriminating.
Record the portable framing lane and exact recovery commands in the
active-work ledger.
`docs/active-work.md` was the only conflicting file, in the same shape
as #191's: `main` inserted the generated-buffer Stage 1 lane immediately
above the bottom-panel header this branch had rewritten. The resolution
keeps both.
Each side's newer text wins where that side owns the fact: `main` carries
the corrected #188 status (MERGED/APPROVED, replacing "OPEN, PROPOSED —
do not implement"), and this branch carries the bottom-panel lane's 2B-3
state and the newer snapshot date, replacing main's "2B-2 MERGED; 2B-3 IS
NEXT" and its 2B-2 boundary paragraphs.
Verified: no conflict markers; every line absent from either parent is a
deliberate supersession by the other, enumerated and checked one by one
rather than counted; all three lane headers present exactly once.
`docs/active-work.md` was the only conflicting file. #196 added the dired
Stage 2a lane at the position this branch had used to relabel the #188
framing lane header; the resolution keeps both, changing neither side's
wording.
`src/editor_core.rs` auto-merged. Both lanes touch it, so a clean
textual merge is not evidence of a clean semantic one — the gate suite
is re-run in full on the merged tree rather than inherited from the
pre-merge head.
Resolution verified for line loss in both directions: the resolved file
differs from `main` only by this branch's own authored edits, and
differs from this branch only by additions taken from `main`.
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
Integrated main (#195) first so the lane block is written on top of it
rather than conflicting with it — the one conflict this file always has,
paid at the merge that was happening anyway.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
The four findings were one shape — a failure that left state wrong and
told nobody — so the lane records them as one lesson rather than four
bugs: every one was a `pcall` or a discarded return value, and each
looked like defensive coding.
Also records the round-1 pin that passed with its own bug restored
(acceptance 53's attribution assertion was satisfied by the deleted
path's basename appearing elsewhere in the same message), the refreshed
gate numbers, and that `main` was re-measured after the round and had
not moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
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
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
Handoff section 1 gains the 2B-3 bullet and the protocol anchor moves to
v21 with the rule that matters stated once: advertise the baseline,
negotiate up from the frontend's AttachRequest, and reserve moving the
advertised version for a change that cannot be expressed additively at
all. 2B-1's forward-looking constraint is marked discharged rather than
deleted, because its acceptance still passes unchanged and that is the
evidence.
COHERENCE section 14 grades the bottom/side panel primitive as complete on
BOTH frontends rather than 'Stage 2 pending its own framing', section 20
P5 follows it, and the section-19 protocol bullet records that the v21
family is live in production without an incompatible handshake change.
The active-work lane is rewritten to the shipped slice, including the
rejected activation alternatives and why the server-first shape forces
each one out, and the one-way compatibility window it leaves open.
The 2B-2 acceptance suite's header said production keeps panel_capable
false for every semantic session. That is no longer true, and its
assertions did not change — which is the point, so the header now says so
rather than being quietly left stale.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Rides this branch rather than a standalone ledger PR: with several PRs
open, a lane written on `main` for work that lands elsewhere
re-conflicts on every merge.
Records the measured merge-base as pasted output, what 2b and 2c still
owe so the split boundary is auditable, the two re-pinned m4 rows, the
one framing claim found wrong, the two bites that were vacuous as
specified and why, the gate numbers, and the §16 ownership warning
against starting Journey Stage 1b while this is open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
One conflict, `docs/active-work.md`, with three strands rather than the
usual one: main gained #190's lane, this branch carries its own Stage 1
lane and a relabel of the #188 framing lane, and main had removed the
documentation lane while this branch still had it.
Resolved by construction. Main's file taken whole; this branch's Stage
1 lane reinserted at its own position ahead of the bottom-panel lane;
this branch's relabelled framing lane ("MERGED AS PR #188") kept in
place of main's stale "OPEN, PROPOSED" version; main's removal of the
documentation lane preserved.
Verified against both parents rather than by inspection: the Stage 1
block is byte-identical to this branch's, the documentation lane is
gone, no conflict markers survive, and the update-protocol rule 6 seam
check finds no double blanks.
Note for whoever absorbs next: main now carries three lanes describing
merged PRs (#190, #188, #194). This merge keeps this branch's more
accurate labelling of the #188 one but does not remove any of them ---
rule 4 permits removal only once durable facts reach
`docs/agent-handoff.md`, and none of those three PRs touched it.
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.
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.
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.
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.
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.
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.
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.
Bring the approved generated-buffer immutability framing onto the Stage
1 branch and update both active-work lanes to the landed #188 state.
Main @ 27b1185 changes documentation only relative to the prior base.
Record the revision-7 selection and acceptance reconciliation, the
exact code checkpoint, the non-vacuous fan-out bite, and the final gate
results. Keep PR 188's proposed status and merge ordering explicit.
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.
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.
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.
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.
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.
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.
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.
The previous commit pasted `git rev-parse`/`git log`/`merge-base` output
showing `main` at `64883eb` and "main IS integrated". #193 merged between
writing that and pushing it, so the pasted output was false in the tree
that carried it --- and "main is integrated" is exactly the kind of claim
this review round was about: the tree asserting something the record does
not support.
Re-measured and re-integrated, and the bullet now says the thing the
first attempt got wrong. Pasting command output is necessary and NOT
sufficient: a base measured before the last push is a claim about a tree
nobody has, so it has to be re-measured at push time and treated as
expired on sight. `main` moved three times inside this one lane
(#187 -> #192 -> #193).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
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.
The lane itself carried two instances of the defect the review found in
the tests, so it is rewritten rather than appended to.
- Its "two framing criteria were wrong and the tests say so" bullet was
a restated contract in the ledger, which is the same shape as the one
in the test comments. Replaced by a statement of the boundary --- #188
owns the acceptance contract, this lane adopts it and files findings
there --- and by what each finding actually was.
- Its bite list said "five criteria are falsified by revert" over six
enumerated items, and counted the disambiguated-panel criterion among
them. That criterion fails on `main` only at its fixture premise, so
it is a mutation bite. Both errors corrected, and the list is now
split by falsification method rather than by a single count.
The base is measured in the lane with the command output pasted, and
labelled a reading rather than a constant: `main` moved twice while this
lane was open, so a SHA quoted forward from an earlier message would
already be wrong.
Adds the sweep result (7 sites, 4 named by the review, 3 found by the
sweep), the re-run bite results under `scripts/bite`'s new positive
control, and the one deliberately-recorded VACUOUS result --- criterion
7's listview half, which passes under the criterion's own mutation
because `window.switch_buffer` rebuilds the `TextView`. That is measured,
not inferred: the same mutation reports VACUOUS against the listview half
alone and BITES against the dired half.
Also records that the dired 200 ms perf test is load-sensitive rather
than regressed, with the pre-image comparison: 0.09 s either way over
five runs each, so the whole-buffer conversion costs nothing measurable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
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
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.
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.
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.
An open PR is exactly the volatile work this file records, so it gets a
lane the moment it is opened — and the lane rides on its own branch, not
on `main`, because with several PRs open a lane written there
re-conflicts on every merge.
Records the branch and its base (`githubsucks/main` @ `300cbc4`), the
code checkpoint the verification numbers describe, what Stage 1 ships,
what Stage 2 still owes, the two framing criteria that turned out to be
unimplementable as written and what replaced them, the bite result for
every criterion, and the recovery commands.
The canonical-base line above is deliberately left at `7586905`: this
lane names its own base, which is the case that paragraph already covers
("lanes below that name an older base have not been re-based"), and
editing the shared snapshot line while other PRs are open is the
contention this file warns about.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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
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
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
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
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
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
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.
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
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.
#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.
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.
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.
Capture the follow-up review finding, the evidence-driven completion
contract, the exact corrected CAT duration, and the proportional green
gate matrix at 9c79ce1.
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.
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.
Record the complete post-#183 gate matrix for bottom-panel Stage 2B-1
at c8895a8 and mark the lane ready to open for review.
Retain the required-GPU first-pass classification: one unrelated math
render assertion failed, passed immediately in isolated single-threaded
execution, and the mandatory complete 202-test rerun passed.
Integrate main through the Journey/GPU directory-target ratchet (#183).
The public managed-GPU probe additions compose with the v21 wire layer.
Resolve the volatile ledgers to the landed state: advance the canonical
anchor to 7fd646d, remove the completed ratchet lane, preserve the
approved Stage 2 revision-5 recovery facts, and mark the integrated
2B-1 tree as awaiting its from-start full gate rerun.
Drive pmacs --gpu . through the root broker and real managed GPU
connector, keep the session alive through the asynchronous dired
replacement, and assert its canonical listing before daemon reuse.
Expose snapshot count and materialized text through the private
display-less acceptance probe so the public path is observable.
Consume the directory session's later replacement snapshot and assert
the canonical dired header plus a known listing entry before checking
daemon reuse.
Correct the bottom-panel revision-5 recovery branch and advance the
durable handoff to the Journey Stage 1a main anchor.
Capture the complete green gate matrix and retain the diagnosed setup
and transient full-sweep failures so the lane remains recoverable and
the evidence is not flattened into an unexplained rerun.
Replace the stale directory-negative in the GPU initial-target suite
with an explicit snapshot-first readiness path, while retaining all
genuinely malformed and unloadable failure cases. Record the portable
side-quest and bottom-panel dependency state.
Make the statusline and Vterm Stage 3 acceptance suites track the
bottom-panel v21 bump, including the real daemon and headless GPU probe.
Record the full gate result and the unrelated stale directory-target
assertion reproduced on canonical main.
Integrate the Journey Stage 1a merge without rewriting the already
reviewed protocol branch. Record the approved three-way Stage 2B split,
advance the canonical recovery anchor, remove the landed Journey lane,
and put 2B-1 into its full-gating state.
The ledger preamble already says Lean 4's merged lane was removed, and
the durable Stage 4b facts already live in the handoff. Remove the stale
section that still called Stage 4b in review so PR #182's post-merge
state is internally consistent.
Two open documentation PRs editing the same three ledger files
re-conflict on every merge to `main` — the treadmill #169 and #176 each
paid three resolution rounds for. #180 was docs-only and already
CONFLICTING against `main`; folding it here retires one of the two
writers instead of resolving the same file twice.
Also integrates `githubsucks/main` @ `42025e4` (Lean 4 Stage 4b #181).
Conflicts resolved, and three claims corrected rather than picked:
* **The head-of-`main` anchor.** Both sides named `74301d1`, which is no
longer true — `#181` has landed. The anchor now reads `42025e4` and
leads with it, in `docs/agent-handoff.md` §1 and the active-work
canonical-base line, and **the recovery floor moved with it**: the
`git log` check requires `42025e4` or newer, per that section's own
rule that a floor accepting an older commit than the declared base
passes on a tree the file does not describe.
* **The Lean 4 bullet contradicted the anchor I had just written.** Its
header still said "stages 1, 2, 3a, 3b LANDED" and its Stage 4b
sub-bullet still said "implemented and in review", while the anchor
above it now records #181 as merged. Both corrected: all six stages
landed, #181 named.
* **The COHERENCE §2 grade.** #180's side still carried "broken at step
3" / "Broken at entry" in both the ground truth and the §0 scorecard;
this branch's corrections win, since they are what the code change in
this PR makes true.
The removal note at the top of `docs/active-work.md` is reattributed:
the terminal config + copy mode lanes were #180's removal, and it now
says so and says why the fold happened.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A count is a reading, not a constant. Recorded as of rev 8 with an
explicit instruction to re-read rather than quote the line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 1 of PR #182. One implementation gap and two stale claims.
**The scope pins the frontend; it does not pin the window.** Framing
§4.4 specified `display{ window = dest:window() }`, but dired's commit
still ended in `pmacs.window.switch_buffer`, which targets whatever
window the scoped frontend has selected. A split or panel that took
focus while `read_dir` was pending therefore received the listing, and
`prev` was captured from it too — with every preflight check passing,
because the captured window was still live and still held its captured
buffer. Both sites now read the captured window: `display` routes to it
with `select = true` (the later `seat_cursor` acts on the active
window), and the `prev` read asks it directly.
N4c pins both halves. The suite's existing routing pins all varied
*frontend* identity; none varied the selected window within one
frontend, which is exactly why 23 green pins missed this. Bite: dired's
`display` back to `switch_buffer` fails N4c alone; `prev` read from the
ambient window fails N4c alone.
Two stale documentation claims, both of which this PR was supposed to
have already fixed:
* **The §0 scorecard still graded §2 "Broken at entry"** while §2's own
ground truth had been rewritten. The scorecard is a second copy of the
same claim and §25's protocol covers both. §19's row and ground truth
were stale the same way — this PR creates the first cross-subsystem
suite, which §19 says should exist and grades as missing — and are
corrected too.
* **P4 still read "leaves exactly one buffer"**, the exact claim rev 6
corrected as false everywhere else in the framing. Restated to what it
actually pins: the file is in the *active window*. The test was
already written correctly; only the framing lied.
Framing rev 8.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes Journey Stage 1a: the `commit_to` acceptance suite (framing
§6 N4, N6, N6b, N6c, P1, P2, P3) plus the documentation updates
COHERENCE §25 requires the PR to carry.
Bite-testing the new pins found a real gap. Deleting the
`ScopedFrontend` arm from `acting_frontend` left N4 green, because
`ScopedFrontend::enter` also swaps `core.active_frontend` and the
ambient fallback then answers correctly on its own. The arm is
load-bearing in exactly one case — a commit reached from inside an
interactive command, where the origin sits between the override and the
ambient value — and nothing pinned it. N4b is added, driven through
`dispatch_key` because that is the only thing that establishes an
interactive origin, and the mutation now bites it.
Two smaller corrections found the same way:
* `commit_to`'s forged-destination message was unreachable. Typed as
`AnyUserData`, mlua rejected a table during argument conversion, so a
caller got "error converting Lua table to userdata" — true, but naming
neither the rule nor the remedy. The parameter is now `mlua::Value`
and the pointed message fires.
* P1 and P2 also fail on full revert, since `commit_to` does not exist
on the pre-image, so §6.0's "legitimately green on the pre-image" does
not describe them. They stay in the P list because their
discriminating falsifier is the named mutation — a revert-only check
cannot distinguish "validates" from "validates in time" — and each pin
now says so at its own site rather than being silently mislabelled.
Bite results, each run against the whole suite:
scope stops swapping `core.active_frontend` -> N6a, P3 fail; nothing else
preflight moved after the callback -> P1, P2 fail; nothing else
drop the `ScopedFrontend` arm -> N4b fails; nothing else
Docs: COHERENCE §2 grade + step-3 verdict row, §20 Priority 1 and the
arc list; the GPU initial-target framing's Q#GT6 and acceptance 10,
whose directory case this stage deliberately supersedes; handoff §1;
the active-work ledger; framing rev 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 11 put the nesting count in the expander, which is optional. A
consumer at a lower priority can CLAIM and stop the chain before the
expander runs, while that fan-out's deferred-expansion subscriber still
runs — so the nested pass went uncounted, looked like the outermost
one, expanded early, and outer pairing resumed with a record the
replace had invalidated. `\alp(` gave `α(` again.
The count now comes from a no-op consumer registered at the minimum
priority, which runs first in every chain invocation that reaches any
consumer at all. Its guarantee is exactly the ordering contract the
chain already rests on, and it degrades safely: the only thing that can
skip it is a claim ahead of it, which skips the expander too, so
nothing is queued in that fan-out either.
The other plausible home does not work and the comment now says why: a
subscriber registered beside `run_deferred` is too late, because the
whole nested fan-out completes inside the OUTER chain's subscriber,
before either of them runs.
Acceptance 45o pins the short-circuit path — a consumer at 25 that
claims when the record is nil, so the nested pass never reaches the
expander. 45n passes against this bug, which is why both exist.
Counting in the expander fails 45o and nothing else.
Framing rev 12 also names the shape rounds 10–12 share: each fix was
correct about the failure it was shown and wrong about the boundary of
the mechanism it leaned on — the chain's copy semantics, then its
re-entrancy, then its short-circuit. A queue that outlives the thing
that filled it has to name that thing, not approximate it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
`buffer.after-edit` fan-outs NEST — the typed-edit contract supports a
consumer calling `pmacs.hook.run`, and typed_edit.lua's header says so
in its second paragraph. A nested run re-enters every subscriber,
including the deferred expansion's, while the OUTER chain is still
walking its consumer list and pairing has not yet seen the terminator.
So a consumer registered at priority 75 — between the expander at 50
and pairing at 100 — that runs one nested fan-out made `\alp(` yield
`α(` again: the nested pass consumed the queued expansion and edited,
and outer pairing then resumed holding a record the replace had
invalidated. That is round 10's failure reached through the chain's
documented re-entrancy seam rather than through claiming, which is why
deferring alone did not close it.
Deferring work past a fan-out means owning WHICH fan-out it belongs to.
The chain's subscriber and this module's each run exactly once per
fan-out, in that order, so counting invocations of the first and
matching them off in the second identifies the nesting level. Only the
outermost pass expands; a nested one leaves the expansion queued. No
new seam in typed_edit.lua, which is merged Stage 4a substrate.
Both halves bite: removing the level check and never counting
invocations each fail the new acceptance 45n.
Also fixes a test comment that still described the span design round 10
discarded — it claimed the expansion replaces the span "INCLUDING the
terminator". The behaviour asserted was right; the explanation was
stale. Framing rev 11.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
Three defects, all about what happens AROUND the expansion rather than
about resolving an abbreviation.
A pair character that TERMINATES an abbreviation never reached
auto-pairing: `\alp(` gave `α(`. Q#LN22 already said the terminator is
not claimed and the implementation claimed it whenever an expansion
succeeded. Merely declining is not enough either — the chain hands each
consumer a copy of the record made before any consumer ran, so
expanding inside the chain invalidates the copy pairing is holding and
the closer is silently lost. Verified by mutation rather than assumed:
expand-then-decline reproduces `α(` exactly.
The expansion therefore runs on its OWN `buffer.after-edit` subscriber,
registered after typed_edit.lua's and before lsp.lua's. A claim stops
the chain but not a separate subscriber, which is the point: pairing
claims the terminator it reacts to. The replaced span now covers only
the leader and the typed text, so pairing's closer lands outside it and
survives. One undo restores the same text either way, because the
terminator was always its own insert.
That second subscriber is a new instance of Q#AP7 — lsp.lua flushes
didChange synchronously on the signature-trigger path, and `(` is a
trigger — so acceptance 45m pins it with the sighelp fake server: no
didChange may ever carry the unexpanded text.
The relevance check is now three-part, as pairing's has been since
#110: buffer, window, AND `ed.cursor() == rec.post_cursor`. A redefined
self-insert can insert the completing character and then move the
point, and expanding over a span the user has left teleports them back
into it.
Cursor placement after the replace is context-guarded, as
`repair_cursor` is. A buffer intercept may switch buffers while
`buf:replace` runs; the unguarded `goto_byte` then translated the Lean
buffer's pre-edit point through the Lean buffer's edit and applied it
to whatever was ambient.
Q#LN22, criterion 38's span wording, and the ledger are corrected to
describe the deferred design rather than the one that shipped — the
rationale's source, not only the sites quoting it. Acceptance 45j/45k/
45l/45m added; framing rev 10.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
Round 6. Documentation only; no code, no protocol change.
1. **The four-writer table mapped the wrong buffers to `ensure_slot`.**
Verified at the call sites: `ensure_slot` has exactly two callers,
`*compilation*` (`compile.lua:1090`) and `*shell-command*` (`:1125`).
`*search-results*` is an **independent panel** in
`builtin/commands/default.lua` with its own intercept (`:869`),
round-trip mark and writes; `compile.lua` declares its name only to
answer a predicate (`:216`), which is what made it look like a third
slot. Round 5 fixed an undercount and introduced a misattribution in
the same paragraph — the count was right, the mechanism was not.
The table is now keyed by **writer**, not by buffer, so the mapping
cannot silently drift again: four mechanisms, five buffers. It carries
an explicit "do not read `ensure_slot` as covering the search panel"
note, because that is the specific wrong inference.
Corrected identically in `COHERENCE.md` §14 and the framing's
deferred-lane text, which both carried the error.
The scope claim is narrowed with it. "Every generated buffer outside
copy mode" was too wide: `*workers*`, `*help*` and `*buffer-list*` are
generated but do not use this idiom, and the REPL package's intercept
(`packages/repl/init.lua:187`) is an op-filtering editing policy
rather than a read-only panel. The claim is now "every remaining
intercept-protected writer", and the two excluded groups are named so
the next reader does not have to re-derive the boundary.
2. **The recovery floor contradicted itself.** The canonical-base line
said the check accepts `a27f646` or anything newer while the check
below required `74301d1`. The floor genuinely advanced; the prose now
says so outright — a tree at `a27f646` no longer passes — and states
why the floor must move with the base rather than trailing it.
3. **Two anchors survived the integration.** Lean 4 Stage 4a said it was
part of "the `fe8b8ba` anchor above" when the anchor had become
`74301d1`; it now refers to the anchor rather than restating a commit,
which is what let it go stale. And #168's closed entry called its own
`fe8b8ba` figure "the live figure" — it is a reading taken at
`1b6a084`, kept as history, and now says so and points at the
coverage lane as the single authority with an explicit "do not quote
this one forward".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
Rev 5 is approved and the branch is cut, so the ledger's "no branch,
commit, or PR exists yet" line no longer describes reality. Records the
recovery command, notes that PR #177 has merged and therefore unblocks
implementation, and carries the standing obligation that dired Stage 2
re-scouts around commit_to before its branch is cut.
Two doc conflicts, both in favour of `main`, and both are this PR's own
findings arriving from the other side:
- **The dired lane.** #169 did exactly what round 5 finding 5 said it
would: absorbed dired Stage 1 into handoff §1 and removed the ledger
lane per rule 4. This branch carried a placeholder saying #169 owned
that text; the placeholder is dropped and #169's version taken whole.
This is the rule working — one PR, one authority per paragraph.
- **The canonical base.** Both sides edited it. Resolved to `74301d1`,
now naming #169 and #176, keeping `main`'s added guidance that a
recovery threshold must move with the base it declares canonical (a
check that accepts an older commit passes on a tree the file does not
describe). The threshold moved with it.
Consequences of the merge, folded in:
- **The census is re-measured on the merged tree**: 3,176 vs 3,449 —
still **273 dark**, 185 of them in the library. #176's six new tests
are not `crdt`-gated, so both totals moved and the gap did not. The
per-target table is unchanged.
- **The `crdt` Clippy failure is re-measured too, and the previous count
was wrong** — it was read off a different checkout. At `74301d1` it is
seven errors before the build aborts: four in `src/daemon.rs`, three in
`tests/vterm_stage3_acceptance.rs`. Recorded as a **lower bound**,
because Clippy abandons the remaining targets once one fails, and an
older tree showed a further error in
`tests/auto_indent_crdt_acceptance.rs` this run never reached.
- **The PTY-terminate lane (#176) is marked MERGED and retained**, with a
note at its head saying why it survives rule 4: no landed-doc PR owns
moving its facts to the handoff, so removing it now would delete the
record rather than move it. The ledger preamble's stale-on-purpose
paragraph is rewritten around that one lane; the Lean 4 and
GPU-terminal-input headers it used to disclaim no longer exist.
- The flake note's "`src/process.rs` last changed by the Darwin PTY
signal-name fix" is dropped — #176 changed it. The load-bearing half
(#178 did not touch that file at all) is what remains.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
Reconciles the handoff and ledger against a main that advanced past
this branch's base: the header, `main` anchor, and canonical-base
description take main's richer versions restamped to 74301d1, main's
new PTY-terminate lane is kept alongside the Lean lane, and main's
Stage 4a/rev-8 lane history is dropped in favour of the Stage 4b lane
that supersedes it — per this ledger's own rule to remove entries when
their PR merges.
Also fixes the coherence census's second count, which still said eight
settings three paragraphs below the nine it now lists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
Documentation only; no code, no protocol change. All five findings
reproduced before fixing.
1. The CRDT-dark census was stale. Re-measured at `fe8b8ba` under CI's
exact flags versus the same flags plus `crdt`: 3,170 vs 3,443 —
**273 dark, 185 in the library**, not the 264/177 #168 measured at
`1b6a084`. The per-target table is regenerated (it gains a
`terminal_copy_mode_acceptance` row, acc16e's, from this very arc),
the rows are stated to sum to the total, and the lane now says the
number moves with every merge and must be re-measured rather than
quoted. #168's figure is kept as a dated historical reading with a
pointer to the live one.
2. The generated-buffer non-adopter inventory was short by half. It is
**four writers, not two**: listview panels (`listview.lua:60-61`),
`*compilation*` **and** `*search-results*` — both through
`compile.lua`'s shared `ensure_slot`, so naming only the first
undercounts a mechanism rather than a buffer — and dired
(`dired.lua:371`). All four pair an erroring intercept with
`bypass_intercept` writes over a still-writable rope, and all four are
emptiable by `M-x buffer.undo`. Corrected in the handoff §4 (as a
table, with each writer's shape), `COHERENCE.md` §14, and the
framing's deferred-lane text. The adoption estimate gains a
consequence: the two `compile.lua` slots append and need a streaming
variant; listview and dired are whole-buffer replaces and are the
cheap half.
3. The ledger's canonical base still named `a27f646` while the same file
recorded #168 and #178. Now `fe8b8ba`, with the recovery check's
accept-or-newer floor moved with it — a stale floor is what lets a
wrong base pass verification.
4. The completed terminal lane is **removed**, not marked complete. Rule
4 of this file's own update protocol says a lane goes when it merges,
and its opening contract says the file records only what has not
landed. Its durable facts moved first: a new arc bullet in the handoff
§1 (the snapshot materializes, so the dispatch-shadow count stays at
six; `prune` reacts to removal rather than causing it; ownership means
the handle table, never found-by-name; profiles are a raw Lua table
and why the escape cache lives on `TerminalSession`; what criterion
17 must assert when it can finally be written; the `cat -v` echo probe
and count-don't-match rule), with the `set_generated_contents`
invariant already in §4. A compact entry remains under "Closed since
the last snapshot". The gate-run flake the lane carried moved to the
CI `crdt`-coverage lane, which owns its discrimination — verbatim,
including its explicit refusal to claim a root cause.
5. The refreshed handoff was internally stale: it anchors on a `main`
that contains #179 and #165 while still calling both "in review".
Both now read MERGED. Dired's durable facts are deliberately **not**
absorbed here — that is open PR #169's job, and writing it from two
PRs would put two authorities on one text — so the dired lane stays,
with a note saying why it survives rule 4 and who removes it.
Also recorded while verifying finding 4's new home: the `crdt` Clippy
failure on `main` is re-verified with exact sites (four in `src/daemon.rs`,
one in `tests/auto_indent_crdt_acceptance.rs`), because any CI job that
compiles the `crdt` targets is red on arrival until they are fixed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
Typing `\alpha` in a Lean 4 buffer gives `α`; `\<>` gives `⟨⟩` with the
point between them. The abbreviation table is vendored from
vscode-lean4 and the expander is a typed-edit consumer registered on
the Stage 4a chain at priority 50, ahead of auto-pairing.
The ordering is load-bearing. 64 abbreviation keys contain a character
in the `lean4` pair set, so with pairing first, typing `\[` would
insert `[]` and corrupt the pending key to `\[]` before the second `[`
arrives — `\[[]]` becomes unreachable. The consumer therefore claims
every keystroke that EXTENDS a pending abbreviation, not only one that
completes an expansion; claiming only completions would hand each
intermediate `[` to pairing by a different route.
The vendored table is an ORDERED SEQUENCE, not a map. Upstream breaks
equal-length ties by source declaration order — 101 prefixes depend on
it, and `\f` resolves through `f<` rather than `f>` — which a
`pairs`-iterated Lua table cannot express. `scripts/regen-lean-abbrev`
takes a vscode-lean4 commit, emits the file with its provenance header,
and aborts on a duplicate key, invalid UTF-8, or a round-trip mismatch.
Undo is cross-peer-degraded on CRDT frontends and that is accepted and
named, not papered over (Q#LN21): `\alpha` arrives as six source-peer
optimistic inserts while the expansion is one daemon-peer replace.
`set_round_trip_input` would fix it and also makes `dispatch_idle`
report false, so RET would stop inserting a newline.
Round 9 corrects three approved acceptance criteria that the real table
contradicts, found by simulating the state machine over all 1,855
entries and re-reading upstream at the pinned commit rather than
re-reading the prose. `\to` is not eager — `top`, `to0` and `toa`
extend it. `\zzzz` expands to `ζzzz ` because `ze`, `zeta` and
`zsqrtd` exist; only `$ % , ; @ W` open no key at all. And `\alpha`'s
undo does not restore `\alpha ` because `alpha` IS eager, so the
terminator is a separate edit. Criteria 38, 41 and 42 now state both
paths, and the false halves are asserted too: they read as correct
until the table is consulted.
Three implementation traps worth the record. The generator's own
round-trip check was broken twice and failed closed both times:
`str.splitlines()` splits on U+2028, which 53 symbols contain, and
escaping through `chr(byte)` produced a latin-1-shaped string that the
UTF-8 write re-encoded. The first check compared in-memory strings and
agreed with itself; it now stages the file, re-reads the bytes from
disk, and renames into place only on a match. And the expansion SHRINKS
the buffer, so the point must be placed explicitly — pairing's
no-cursor-motion rule holds only for an insert AT the cursor, and
without this every self-insert after the first expansion is silently
rejected and the editor looks dead.
25 acceptance tests plus one `--lib` test for the optimistic CRDT
producer (45f), which is where the gate list's `--features crdt` run
reaches it; a crdt-gated integration test would be dark in CI and in
the gates both. Fifteen mutations bite, each failing its target. Three
of these tests were vacuous when first written and biting is what
found them: the abandonment test asserted text a surviving record
would also produce, the re-arm test used an example that never reaches
the re-arm branch, and both switch tests ran through
`find_or_open`'s fresh-load path rather than `buffer.after-switch`.
No protocol change (Q#LN14). Also reconciles the handoff and ledger
for Stage 4a (#179) and adds `lean.abbrev` to COHERENCE.md's
config-registry adoption census, now nine settings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
Re-integrated after #178 (terminal copy mode) landed. One conflict, in
COHERENCE.md's journey table: row 7 keeps this branch's text (dired #165
is merged, so "PR #165" is wrong), row 8 takes main's, which adds copy
mode and the missing close/kill command to the terminal step.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
Documentation only; no code, no protocol change.
Handoff §1 anchors on `main` @ `fe8b8ba` and leads with terminal copy
mode (#178) and its landed-doc pair (#168). The active-work terminal lane
flips to BOTH STAGES MERGED, and the #166 lane records #168 landing with
what it measured: 264 tests dark workspace-wide for want of `crdt` in CI,
and the vterm audit's honest count of acceptances that drive a real
daemon (2, not 3).
The substantive addition is a new §4 substrate invariant for
`Buffer::set_generated_contents`, because it is now the one authorized
write for every generated buffer and three of its properties are
non-obvious enough that four review rounds were spent finding them:
- an intercept is not read-only — `Buffer::undo` reaches the rope through
`ensure_writable` without consulting the intercept chain, and rebinding
the undo chords does not help because `M-x buffer.undo` needs no
keymap;
- a bare `set_read_only` would be worse than nothing, since it also
refuses the owner's refresh — the pairing is the primitive, which is
why no Lua `set_read_only` exists;
- a rope write is only half of an edit: the returned `Edit` must be
fanned out, or a displaying window paints the new rope with stale line
offsets and replica mirrors never import the write.
Also recorded: history clearing must clear whichever history the buffer
has (the v0.1 stacks are bypassed in CRDT mode), that `*compilation*` and
listview have NOT adopted the primitive and remain emptiable by
`M-x buffer.undo`, and that this does not replace `set_round_trip_input`
— the two guards cover different copies, and a daemon refusal arrives
after the frontend has already painted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
Re-integrated after #168 landed the ledger refresh. Five conflicts, none
resolved by taking a side wholesale:
- Both head-of-main anchors take main's a27f646 wording (written by
#168); the ledger keeps this branch's caveat about lanes naming an
older base.
- The dired lane section is pruned, which is this PR's purpose, but main
had meanwhile pruned the GPU terminal input lane as landed — so the
union drops both stale lanes rather than resurrecting either.
- "Closed since the last snapshot" keeps BOTH new entries: dired Stage 1
#165 from this branch and GPU terminal input #166 from main.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
Same one-line ledger conflict as the previous integration, now against
the refreshed main (#168): keep this lane's PTY terminate section, take
main's newer Lean 4 lane heading.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
The handoff's "last updated" paragraph conflicted; resolved by writing
the current statement rather than taking either side, and the same pass
clears the staleness the ledger had accumulated while five PRs contended
for it:
- Both head-of-main anchors (handoff §1 and the ledger's canonical base)
now name a27f646, and list terminal config #173, the bottom-panel
Stage 2 framing #175, Stage 2A #177 and Lean 4 Stage 4a #179 — plus
the CRDT undo repro #157 and the inline-math landed-doc refresh #172,
which are merged but had dropped out of both lists.
- The bottom-panel lane records Stage 2A as MERGED (#177 @ 0a3fcd1)
instead of IN REVIEW, and states what 2B is: approved, unstarted,
branching from main rather than stacking, with §7.2's criteria and
§5.3's three-boundary trap named.
- The Lean 4 lane records Stage 4a as MERGED (#179 @ a27f646); its
"framing-only, no code, awaiting approval" bullet was false.
The dired lane is left alone deliberately: #169 is the PR that records
dired Stage 1 as landed, and owns that heading.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
Three conflicts, resolved by taking the newer statement on each side
rather than either side wholesale:
- COHERENCE.md journey row 7 keeps this branch's text (dired #165 is
merged, not a PR); row 8 takes main's, which records terminal config
#173.
- The ledger's canonical-base line takes main's d400f30 anchor and keeps
this branch's caveat about lanes that name an older base.
- agent-handoff's two anchors take main's d400f30 wording, with the CRDT
undo repro #157 and the inline-math landed-doc refresh #172 added
back — both are merged and main's summary had dropped them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
Review round 4, P2. A fix can invalidate a test that was never written.
Criterion 17 still specified the pre-round-2 world: remove
`set_round_trip_input` and the optimistic op "passes `ensure_writable()`
and mutates BOTH sides, silently, with no divergence to notice". That
was true while no Lua binding set `read_only`. Since
`set_generated_contents` does, the daemon refuses the op — so only the
frontend's own mirror mutates, and the copies diverge.
The gap matters precisely because 17 is unpinned. A real-GPU test
written to the old spec would hunt for a daemon-side edit that can no
longer occur and pass for the wrong reason, quietly readmitting the
round-2 regression through a test not yet built. The specification is
the artifact under review here, not the code.
Restated around unauthorized MIRROR mutation plus daemon refusal —
divergence — in all four places carrying the obsolete claim: the
criterion itself, the Q#TC6a heading, the acceptance-16 doc comment, and
the bite roster. The heading's "ONLY thing" now says what it is the only
thing FOR: the replica's own mirror. `docs/active-work.md` also still
described acceptance 16b as asserting `is_read_only()` is false, which
round 2 flipped.
Why round-trip input stays load-bearing rather than redundant, now
stated wherever the daemon guard is mentioned: a refusal arrives after
the frontend has already applied optimistically and painted. It buys
divergence instead of silent agreement; it does not prevent the mutation
the user is looking at.
Also recorded, after capturing it properly this time: the gate-run flake
in `cargo test --lib --features crdt` is
`process::tests::setsid_escapee_is_not_reaped_and_teardown_reclaims_readers`
(`active_reader_probe` -> None, "live runtime probe"), ~1 run in 5.
Pre-existing and unrelated — this branch does not touch
`src/process.rs`, the test passes 10/10 standalone and 2017/2017 at
`--test-threads=1`, and it is another instance of the known `drain_until`
trap: draining for `Started` also ticks, and a tick reaps the leader.
That also explains the unattributed "2 failed" run noted in round 2.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
Resolves the ledger conflict: keep this lane's PTY terminate section and
take main's newer Lean 4 lane heading verbatim. No content of either
lane is rewritten here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
Review round 3, on the round-2 primitive itself. One lesson covers all
three findings: a rope write is only half of an edit, and "discard
history" means whichever history the buffer actually has.
P1 — the binding swallowed the edit. `set_generated_contents` returned
`()`, so nothing reached `notify_buffer_edit_to_windows`. Two
consequences, both reproduced by the reviewer. In the default build a
window showing the buffer kept a `TextView` line index describing the
PREVIOUS contents, and the next paint indexed the new rope with stale
ranges — `assertion failed: end <= self.len()` in `src/rope.rs`. In the
CRDT build `pending_crdt_ops` stayed empty, so replica mirrors never
imported the owner's write and their optimistic edits were generated
against content already replaced. The `delete`+`insert` pair this
replaced had done that fan-out for free.
Now applies ONE whole-buffer `Replace`, returns its `Edit`, and notifies
from the binding. The doc comment states the obligation, because the
next owner to adopt the primitive inherits it.
P2 — "discard history" was false in CRDT mode. The v0.1 stacks are
bypassed entirely there; the history lives in loro's `UndoManager`.
`read_only` stops the replay but not the retention, which is the memory
cost the contract claims to eliminate. `UndoManager` exposes no clear,
but needs none: it records only what happens after it is constructed,
the same property `CrdtState::from_bytes` already uses to keep the seed
insert out of undo. `CrdtState::clear_undo_history` rebinds a fresh
manager to the same doc.
P2 — the docs described the pre-fix architecture. Q#TC6a said no Lua
binding sets `read_only` and round-trip input is the only guard; the
acceptance text still said `is_read_only() == false` while 16b had been
flipped to true; `terminal.lua`'s comment repeated the obsolete claim.
The architecture is layered and now says so: rope-level read-only
protects the daemon copy, round-trip input protects the replica's
optimistic mirror, and neither substitutes for the other. Q#TC6a keeps
its analysis under a superseded-in-part box rather than being silently
rewritten — its conclusion survives, two of its premises do not.
New pins. acc16d paints the window after a SHRINKING generated write:
stale offsets then point past the buffer end, so the failure is the
reported crash rather than merely stale pixels. acc16e asserts the
refresh is queued for mirrors, through the real copy-mode path;
`crdt`-gated and therefore dark in CI, which is why 16d drives the
binding rather than the terminal. Plus a CRDT unit test that ten renders
leave the `UndoManager` with nothing recorded.
Bites: dropping the notify panics acc16d at `rope.rs:145` and fails
acc16e with `queued: []`; dropping the `UndoManager` rebind fails the
new unit test on `can_undo`.
Still open, and recorded in COHERENCE.md §14: the fan-out obligation
makes `*compilation*`/listview adoption more than a one-line swap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
Five defects in the chain itself, plus the stale handoff state.
Each consumer now gets its own shallow copy of the typed-edit record.
Handing everyone the same table let a DECLINING consumer rewrite
provenance for the ones behind it, and pairing decides what to close
from `rec.char` — so a forged `char` turned a typed `x` into `x)`.
Every field is a scalar or an opaque id, so a shallow copy is complete.
The fan-out iterates a snapshot of the consumer list. It was iterating
the same array `add_consumer` mutates: a consumer that registered a
lower-priority one shifted itself forward under `ipairs` and ran twice,
and re-registering made that unbounded. Registrations and removals made
during a fan-out now take effect on the next one, stated as a contract
and pinned in both directions.
`tostring` on the caught error moved inside the containment. A Lua
error may be any value, including a table whose `__tostring` throws —
rendering it outside the `pcall` reintroduced exactly the escape the
containment exists to prevent.
Priorities are validated as finite integers in i32 range, matching
`pmacs.completion.register`. NaN is a number and every ordered
comparison with it is false, so a NaN consumer landed wherever the
insertion scan gave up and silently voided the lowest-first ordering
that Q#LN22 depends on.
`add_consumer` returns a handle and `remove_consumer` unregisters it,
reporting whether it was live. Without teardown the chain inherited the
`pmacs.hook.add` callback leak COHERENCE.md §13 already records, and
spread it to every consumer.
Also corrects the rationale the containment was documented with, in the
module, the test, and the framing: an uncontained throw does NOT take
the fan-out's other subscribers down. `run_all_must_succeed`
(src/hook.rs:332) collects errors and continues, so lsp.lua still
flushes didChange. The containment is still required — the throw skips
every later consumer in the chain — but the reason is narrower than
rev 7 claimed.
Criteria 46f (record isolation), 46g (snapshot iteration), and 46h
(lifecycle and priority validation) added; 46d's rationale corrected.
Four new tests, all bite-verified by mutation, each failing only its
target: shared record table (1), live-array iteration (1), unprotected
tostring (1), bare number check (1), no-op removal (2). The suite also
runs green under `--features lua54`.
docs/agent-handoff.md said Stage 4a was awaiting approval while this
branch had it implemented and in review.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
**P1-1 — layout invalidation could suppress the authoritative clear.**
Real bug. Both render paths resolved the document identity AFTER the
evaluator ran callbacks, but BOTH outcome arms carry PHASE-1 contexts.
A provider that closes the primary document split changes
`primary_document_window` mid-evaluation, so the filter compared
phase-1 contexts against a replacement identity, matched nothing, and
emitted no clear — leaving stale statusline text on the wire forever.
The identity is now captured BEFORE `evaluate_statusline` runs and
threaded through both paths (the terminal path via `terminal_chrome`).
Pinning it took three attempts, and the two failures are the useful
part:
- `pmacs.window.close()` takes no argument — it closes the ACTIVE
window. The first version passed a window id that was silently
ignored, so it closed the panel instead of the document.
- The Lua window API acts on the ACTIVE FRONTEND, so driving it against
a synthetic semantic view changed nothing at all.
- Closing the only document window is structurally REFUSED (Q#BP6
forbids a lone side window as a resting state), so the fixture needs
TWO document windows for the close to be legal.
The test now asserts its own precondition — that the callback really
changed the identity — before asserting the clear, and reproduces the
reported symptom (no `StatuslineSegments` at all) when the fix is
reverted.
**P1-2 — #21 was pinned at the helper, not the producer.** Confirmed:
reverting only the call site inside
`publish_buffer_snapshot_to_replicas` left both the helper test and the
existing socket-pair test green. The helper assertions are removed (with
a note saying why) and replaced by
`snapshot_publication_follows_the_document_under_a_focused_panel`, which
drives the real producer over socket pairs and asserts BOTH directions:
the document buffer's snapshot is delivered while a panel holds focus,
and a panel-only buffer's is not.
Biting that test exposed a defect in the test itself: the delivery read
had no timeout, so a regression made it HANG rather than fail. A hanging
test is strictly worse than a red one — every read now has a timeout.
Gates: fmt clean; workspace clippy clean; 1,832 default + 2,015 CRDT
library; Stage 2A 17; Stage 1 46; statusline 8; m11_5 2; GPU initial
target 14; terminal config 12; folding Stage 2 48; vterm 1/2 10 / 6;
M4 121; required GPU 202; `git diff --check` clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 2, P1. Undo could empty the "read-only" snapshot.
`render_snapshot` wrote with bypass_intercept, which leaves ordinary
undo history behind, and `Buffer::undo` reaches the rope through
`ensure_writable` without ever consulting the intercept chain. So a
single `C-/` — or `M-x buffer.undo`, which needs no keymap at all —
replaced a freshly rendered snapshot with an empty buffer.
`set_round_trip_input` does not help: it routes the key into the daemon
command path, which is exactly where undo runs.
Rebinding the undo chords buffer-locally would not have closed this,
and `compile.lua` already says so in a comment: "command/menu undo
stays dispatchable". `*compilation*` and listview panels therefore
carry the same latent defect today.
Adds `Buffer::set_generated_contents` (Lua:
`pmacs.buffer.set_generated_contents`): lift `read_only`, replace the
contents skipping intercepts, discard the resulting history, re-assert
`read_only`. This ships the framing's deferred immutability lane as ONE
primitive rather than exposing the setter — a bare `set_read_only`
would let a caller lock a buffer it can no longer refresh, which is
precisely why that lane was deferred. Discarding history is
load-bearing twice: it removes what undo would replay, and it stops a
periodically refreshed buffer accumulating rope clones that `read_only`
guarantees nothing can ever pop.
New acceptance 16c drives the real M-x path
(`command.invoke_interactive`), the chord, and redo, and asserts the
owner's own refresh still works — the operation plain `read_only` would
have broken. Acceptance 16b flips from asserting `is_read_only()` is
false to true, because the property it documented is the one that was
wrong. Three `buffer.rs` unit tests cover the primitive directly,
including that ten refreshes leave an empty undo stack.
Bite: restoring the delete+insert render reproduces the report exactly
— `left: Some("")` against the full snapshot — failing 16c and 16b.
Still open, and now named in the framing, COHERENCE.md §14 and the
ledger: `*compilation*` and listview have not adopted the primitive and
remain emptiable by `M-x buffer.undo`; a streaming-friendly variant is
needed for the append case. In CRDT mode `read_only` is what refuses
undo, since loro's UndoManager exposes no clear through `CrdtState`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
Verification describes the pushed tree, per the standing rule. Includes
the bite that was worthless as first written: moving only typed_edit.lua
past lsp.lua broke the runtime load instead of testing flush ordering.
A bite that kills everything has not isolated anything.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
Advance the Stage 4 framing to revision 8. Keep pending abbreviation
state frontend-owned while conservatively invalidating it after any
intervening shared-buffer edit, make the revision token explicit, and
rewrite acceptance 45i around that contract.
Correct the active-work multi-codepoint count and the stale coherence
revision label.
Four findings, all real, and they rhyme in pairs. Two implementation
defects and two vacuous pins, all four tracing to one root: a name is
not an identity, and a context-free readout is not a state observation.
A foreign buffer carrying the snapshot's name was adopted and then
overwritten. `pmacs.buffer.create` accepts any caller-chosen name and
snapshot writes use bypass_intercept, so found-by-name adoption
clobbered user data — the reviewer reproduced "do not clobber" becoming
23 newlines. Now follows dired's F7 rule: ownership means "in copy
mode's own handle table", never "found by name", and a taken name yields
a `<2>` variant.
Snapshot identity was keyed by terminal NAME.
`TerminalManager::open` uniquifies only the derived name — an explicit
`name = ...` is inserted verbatim — so two valid terminals can share
one, and a name-keyed table handed them a single snapshot: the second
invocation retargeted it, `q` returned to the wrong terminal, and
killing either removed the shared buffer. Identity is now the terminal
buffer, compared in an array, because BufferIdLua implements `__eq` but
each wrapper is a distinct table key: comparison works, hashing does
not. The kill-with-terminal callback now closes over its own record
rather than looking the name up again.
The refresh pins were vacuous. Acceptance 19 compared a quiet
terminal's snapshot against itself and 18 counted buffers, so both
passed with render_snapshot replaced by a no-op. The child is
`exec cat`, so the tests now type a marker into the focused terminal,
require it ABSENT from the existing snapshot, and only then refresh —
via `g` and via re-invocation respectively.
The tail-follow pin could not observe view state.
`TerminalManager::snapshot(buffer_id)` is context-free and always
returns the live screen, so it reported "at the tail" even for a view
forced to the oldest retained row. Now read through
`snapshot_for_view`'s at_bottom and its projected cells.
Adds acceptance 18a (a foreign same-named buffer is never adopted or
clobbered) and 18b (two same-named terminals get two independent
snapshots, each `q` returns to its own source, and killing one leaves
the other's snapshot alive).
Four new bites, all discriminating: restoring adopt-by-name fails 18a
AND 18b; restoring name-keyed identity fails 18b; making
render_snapshot a no-op fails BOTH 18 and 19, which is the vacuity
demonstrated rather than argued; and forcing the view off the tail
fails 20.
Criterion 17 stays a named follow-up, per review agreement, until the
real GPU probe is non-skipping and CI-executed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
The 4a/4b split held; five P1s against rev 6's own content, all real,
all reproduced. Four share a root: rev 6 verified its external facts and
under-verified its internal ones.
1. Stage 4a's declared footprint excluded the tests its own acceptance
required. 46a-46e cannot live in tests/auto_pair_acceptance.rs,
which criterion 46 requires byte-identical. Footprint now names
tests/typed_edit_chain_acceptance.rs and gates on it.
2. Pending abbreviation state had the wrong owner. pmacs is
multi-frontend: EditorCore.views is per-FrontendId with its own
active window, take_typed_edit is already frontend-keyed, and
buffer.after-switch fires with no arguments — so a buffer-keyed
clear-on-switch lets any frontend discard another's pending
abbreviation. Now keyed (frontend, buffer) with a window check,
frontend-scoped clearing, a frontend.detached purge, and acceptance
45i, which the buffer-keyed design passes every other criterion
without.
3. The shortest-match rule was missing its tie-break: upstream keeps
declaration order among equal-length shortest keys, and 101 prefixes
have equal-shortest candidates resolving to different symbols (f
picks f< over f>). A pairs-iterated Lua map cannot express this, so
the vendored artifact is now an ordered sequence and resolution sorts
by (#key, source rank). Rev 6 missed this because it declared the
package ships no README after a 404 on the package root, with the
directory listing showing src/README.md already in hand — a 404 on a
guessed path is not evidence of absence, and the README states the
rule in one sentence.
4. The generator's rejection rule rejected the current table: \ is a key
and " begins eleven, while acceptance 45d requires \ to work.
Replaced with canonical lossless escaping; aborts only on duplicate
keys, invalid UTF-8, and a failed self-round-trip. 45g no longer
claims to diff against abbreviations.json, which is not shipped.
5. Durable and volatile state were not reconciled. agent-handoff.md
anchored main at d152120 with neither #167 nor #170 and no Lean arc
bullet at all; active-work.md kept 407 lines of merged Stage 1/2/3a/3b
history against its own instruction to prune merged entries, under a
stale snapshot date. Durable facts moved to the handoff; the ledger
keeps only the unlanded Stage 4 lane.
Also corrected: 119 multi-codepoint symbols (26 with $CURSOR), not 93;
three backslash values, not two; Q#LN22 now states the terminating-\
reprocess rule acceptance 45d depended on; acceptance 38 says the
terminator is retained, so undo restores "\alpha " with its space;
coherence cites golden-journey step 5, not step 4; and the
config-registry prior art points at Q#LN22.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
**P1-1 — the `Invalidated` arm published the panel context on the
document wire.** Real bug, and the live half of the routing defect: the
semantic peer has ONE statusline slot, so emitting an
authoritative-empty payload for every context replaced the document's
with the panel's. Now filtered by document-window identity exactly like
the `Ready` arm; a panel's own clear belongs to `PanelFrame` in 2B.
Pinned by `invalidated_statusline_clears_only_the_document_not_the_panel`,
which reproduces the reported shape — two targets instead of one — when
the filter is removed.
Honest note on the `Ready` arm: its identity selector is **defensive**,
not independently falsifiable today, because the document context is
captured first so "first context for my frontend" happens to pick it.
Rather than leave that as a silent dependency,
`the_semantic_fan_out_captures_the_document_first` pins the order and
says why it matters.
**P1-2 — round-1 finding 3 was not closed; four of my pins were
vacuous.** All four confirmed and fixed:
- The statusline consumer test discarded `render_frame`'s output. It now
observes the WIRE payload from a v18 peer with a registered provider,
and asserts non-emptiness so it cannot pass by emitting nothing.
- The terminal test compared two NON-terminal buffers, so both routings
answered `false`. The document window now holds a REAL terminal, so
the routes disagree; reverting `semantic_terminal_key` fails it.
- The decorations test used different buffers and an empty selection —
again the same answer either way. The panel now displays the declared
buffer with a non-empty selection while the document has none.
- #1/#3/#21 had no discriminating pin at all. Their only production
caller is `dispatcher_loop`, which no test can drive, so this extracts
three named seams the loop calls — `document_buffer_to_follow`,
`document_cursor_byte`, `peer_displays_buffer_as_document` — and pins
each.
Also newly pinned: #2 the lazy CRDT upgrade (the census's sharpest
case), #7 `Viewport` aligning WITHOUT taking focus, and #9 a focused
terminal panel not suppressing the document viewport.
**Every one of the nine pins was falsified by revert.** Two needed a
second attempt after the first bite came back green.
**P2-3 — stale docs.** `StatuslineEvaluationTarget::Semantic`'s
documentation described evaluating only the focused window; it now
describes the document-plus-side fan-out, the capture order, the
identity-selection requirement, and that `active` reports actual focus.
The ledger's Stage 2A entry is corrected to five commits, 2,014 CRDT
tests, and 16 acceptance tests.
Two clippy findings the refactor introduced were fixed:
`document_buffer_to_follow` is `crdt`-gated to match its only caller,
and the `CursorByte` guard collapses into one `if`.
Gates: fmt clean; workspace clippy clean; 1,832 default + 2,014 CRDT
library; Stage 2A 16; Stage 1 46; statusline 8; m11_5 2; GPU initial
target 14; terminal config 12; folding Stage 2 48; vterm 1/2 10 / 6;
M4 121; required GPU 202; `git diff --check` clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stage 2 of docs/terminal-config-and-copy-mode-framing.md (rev 4,
approved). `M-x terminal.copy-mode`, or `C-t` in a terminal buffer —
physically `C-c C-t`, since every unescaped key goes to the child —
materializes the retained scrollback into an ordinary read-only,
path-less buffer, with `g` to re-snapshot and `q` to return.
No protocol change.
Materializing is the whole design. isearch, motion, selection and the
kill ring work with no new substrate because the snapshot is a rope, so
SearchStore and the existing match painting apply unchanged. And "keys
must not reach the child" dissolves structurally rather than being
guarded: the transport arm keys on is_terminal(buffer_id), and a
snapshot is not a terminal, so the arm never fires. The
dispatch-shadow count stays at six and describe-key keeps telling the
truth — asserted directly, since that is the observable difference
between the buffer-local idiom and a shadow.
One serializer, not two (Q#TC7). `copy_retained` builds a whole-range
selection and hands it to `copy_selection_bytes`; a second walk would
re-derive soft-wrap joining, wide-glyph continuation, cluster bytes and
per-row trailing-blank trimming, and the two would drift. Four unit
pins in view.rs assert exact bytes against the same projection fixtures
that pin the serializer itself.
Q#TC6a is implemented as two calls, and the second is the load-bearing
one: an intercept guards dispatch only, and no Lua binding sets
Buffer::read_only, so set_round_trip_input is what keeps a replica
frontend from applying optimistically and emitting an op that would
pass ensure_writable and mutate both sides. Acceptance 16 pins that
UNGATED, because CI never compiles the crdt feature.
Eight of nine criteria. Criterion 17's semantic-frontend end-to-end pin
is deliberately absent: the optimistic apply lives only in
pmacs-gpu/src/main.rs and the headless SemanticClient has no optimistic
path, so a faithful test needs the real GPU binary — the a37
foundation, which CI never compiles, silently returns ok when the
binary is unbuilt, and is load-sensitive. Both halves of the mechanism
are pinned ungated instead (16, and 16b for the hazard); the wire-level
half stays an explicit obligation of the CI crdt-coverage lane.
Substrate fact found while wiring lifecycle: TerminalManager::prune
REACTS to a buffer already gone from the registry rather than removing
one, so a child exiting leaves both the terminal and its snapshot
alive. That is why on_removed is a sound teardown hook, and why a
finished command's output stays readable.
Five bites, five different wrong implementations, each failing exactly
one test: removing set_round_trip_input fails acceptance 16 in the
DEFAULT configuration; a naive independent serializer fails all four
unit pins, with the diffs naming each drift mode; making re-invoke
create a fresh buffer fails 18; dropping the kill-with-terminal
teardown fails 18; removing the intercept fails 16b.
COHERENCE.md: §6 gains this as the worked example that a modal-looking
feature need not become a shadow; §11 records the scope="global"
deferral's second live case, making the argument for both registry
deferrals cumulative; §2 step 8 gains copy mode and keeps the
still-missing close command named.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
Stages 3a and 3b landed (#167, #170). Re-scouting Stage 4 against main
@ d400f30 produced six findings that change the plan and three that
confirm it. The pmacs-side facts were verified in a worktree at that
commit; the upstream facts by reading leanprover/vscode-lean4 @ 17d1d08.
The split: Stage 4's risk column read "refactors pair.lua's provenance
read" — every language's auto-pairing — for a stage the prose called the
Lean input method, which is exactly the rule §4 states and exactly what
round 4 found for Stage 3. Rev 5 had noticed the shape and answered it
with a commit boundary; a commit boundary is not a review boundary.
Stage 4a is now the typed-edit consumer chain (substrate, no Lean) and
4b the input method.
Rev 5's expansion semantics were wrong in three ways. Resolution is the
shortest key having the input as a prefix (\al yields ∀ from `all`, not
`alpha`); there is no terminator list at all ('+ ' is a key, so space
extends after \+; '\' is a key, so \\ yields \); and an unmatchable tail
is appended rather than dropped (\alp7 yields α7).
Three further findings. There is no cursor-motion hook, so acceptance 43
as written was not buildable and abandonment is lazy. dispatch_key is
only half of 4b's production path — \ and the letters are not excluded
from the optimistic classifier, and that producer is crdt-gated, so a
crdt-gated integration test is dark in CI and dark in the gate list. And
the whole expansion has cross-peer-degraded undo, a wider bite than
Q#LN6's three bracket pairs; set_round_trip_input would fix it and is
rejected with reasons.
New decisions Q#LN21 (undo degradation) and Q#LN22 (the state machine);
Q#LN10 and Q#LN11 rewritten; §2.11 records the upstream algorithm; §9.1
states the coherence impact for both stages. Acceptance keeps its
existing numbers and adds letter suffixes on both sides of the split.
Citation sweep per COHERENCE §25: five live citations moved in the 50
commits since rev 5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
Integrates canonical `main` @ `cf54270` and closes every finding.
**P1-1 — a stale document `Pointer` stole focus from the panel.** Real
bug. `align_primary_document_window`'s unknown-buffer arm returned
`Some(window)` despite aligning nothing, so #8's activation focused the
document *before* `dispatch_pointer` rejected the mismatched buffer. It
now returns `None`: alignment did not happen, so no caller may treat it
as a document gesture.
Pinned through `handle_dispatcher_event` — the real dispatcher seam —
because the defect lived in the PAIR of alignment and activation, not
in either alone. **The first version of that test was vacuous**: an
unregistered session is dropped at `daemon.rs:1962` (#148's
membership check) before the aligner runs, so it passed with the bug
restored. It now registers a real semantic session and fails with
exactly the reported symptom, focus moving `WindowId(2)` →
`WindowId(3)`.
**P1-2 — the approved A2A-2 fan-out was missing.** The semantic target
returned one context. It now captures the primary document PLUS the
visible side window, each provider invoked once, with a
derived-hidden side omitted (Q#BP2b — no mode line to paint, so no
callback should run for it). The acceptance asserts `windows.len() == 2`.
This exposed a second defect the finding did not name: the consumer
selected segments with `.find(|w| w.context.frontend_id == frontend_id)`
— the FIRST context for the frontend. With two contexts that silently
depended on capture order and could have shipped the panel's mode-line
text as the document status band. `emit_statusline_segments` now takes
the document `WindowId` and selects on window identity.
**P1-3 — the census suite tested the authority, not the consumers.**
Confirmed: reverting a producer to `active_window_for` left all ten
tests green. Added consumer-level pins that drive the real producers
through `SemanticRenderState::render_frame` with a panel focused, plus
the terminal-declaration guard. Bite-verified: reverting the
`LineNumbers` routing now fails
`consumer_line_numbers_follow_the_document_not_the_focused_panel`.
**P1-4 — main integrated.** The textual conflict was `docs/active-work.md`
(both lanes rewrote the same region; the terminal-config lane is kept
whole and the bottom-panel heading updated). `src/editor.rs` auto-merged,
and the full gate suite was rerun on the merge result.
**P2-5 — the painter test was vacuous.** A fixed-point check that
survived deleting `window.text_view.render`. It now asserts each of the
four extracted outputs actually appears: buffer TEXT, the line-number
GUTTER (with line numbers explicitly enabled, rather than dropping the
assertion), the window MODE LINE, and a returned caret. Bite-verified
by deleting the render call.
**P2-6 — the stale fold-projection claim is corrected.**
`src/window.rs`'s `fold_projection` doc no longer asserts that a
semantic session never enters `paint_frame`; it records that the panel
band breaks that premise and that the extracted painters take the map
as a parameter.
Gates on the merge result: fmt clean; workspace clippy clean; 1,832
default + 2,010 CRDT library tests; Stage 2A acceptance 13; Stage 1 46;
statusline 8; m11_5 2; GPU initial target 14; terminal config 12;
folding Stage 2 48; vterm 1/2 10 / 6; M4 121; required GPU 202;
`git diff --check` clean. `vterm_stage3_acceptance::a37` remains the
pre-existing flake measured on the base commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Integrates githubsucks/main @ cf54270 (terminal config Stage 1, #173).
Both conflicts were additive appends to the same lists — the handoff
preamble and the ledger's "Closed since the last snapshot" — so both
sides are kept and the preamble now leads with #173.
Also records what gating #173 measured about a37, because it changes
this lane's proposed fix rather than merely annotating it:
a37 reports `ok` without running whenever `pmacs-gpu` is absent from
the target directory. A fresh worktree reports the Stage 3 suite 9/9 in
0.17 s having never executed the arc's only real-daemon/real-PTY/real-
wgpu path; a genuine run takes about four seconds. Only
PMACS_REQUIRE_GPU=1 promotes that skip to a failure, and the standing
gate list applies that flag to `cargo test -p pmacs-gpu`, a different
package. So fix-shape part 2 must state the flag as a requirement of
the gpu-render job: a crdt leg added to the plain `test` job would run
a37 vacuously and report green.
a37 is also load-sensitive — it passed at d152120 and failed at that
same commit twenty minutes later under machine contention — which makes
a red first CI run ambiguous by construction. The lane now says to
re-run on the merge base before believing a failure, and to prefer
serialized execution over retry-until-green.
The vterm audit's "only 3 of 9 Stage 3 tests drive a real daemon" is
corrected: without the frontend binary the honest number is 2.
Ledger: terminal config Stage 1 flipped from IN REVIEW to MERGED.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
The parallel sweep failed one of the new tests for a real reason, not a
flake: drain_until ticks, and a tick can reap an immediately-exiting
child before the diagnostic runs. Recorded with the matched-load
measurement that shows the fix is load-bearing (0/15 fixed vs 1/10
unfixed under full saturation), and the final sweep numbers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZjWMjwPXhPbt9upku9mCk
Make command-time attachment healing cancel an armed terminal restart
before replacing the server, while keeping request-only lookup pure and
restart-safe.
Track config-driven server ownership privately, bound every fallback
server per SID, scope no-swap retirement to the failed root, and route
the shipped Lean diagnostics command through the safe resolver while
waiting for initialization.
Add direct acceptance counterexamples for all five review findings and
record the sixth-round verification and vacuity lesson.
The ledger's own update protocol requires a lane for volatile work, and
PR #176 had none: branch, worktree, review state, and verification were
all missing.
Records why the lane ships a diagnostic rather than a fix -- three
rejected tolerance designs, the two facts that killed the original
argument (group=true is rejected for PTY mode so the reap ledger never
applies to that path, and the ledger comment asserts EPERM cannot happen
rather than ruling that it means dead), and that the CI evidence never
established the child had exited.
Also records the round-1 test fixes and the four verified bites, so a
reader can tell which assertions are load-bearing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZjWMjwPXhPbt9upku9mCk
Ledger entry for the in-flight Stage 2A branch, plus three findings the
gate run produced that are worth carrying regardless of this PR:
- The structural test comparing the two authorities directly did NOT
catch the focus-class bite; only the consumer-level assertion did.
Both kinds are needed, and the distinction generalizes.
- `vterm_stage3_acceptance::a37` is badly flaky on this machine —
6/8 failures on the BASE commit against 7/8 on the branch in matched
isolated samples, so it is pre-existing rather than a regression. It
also returns `ok` without running unless `pmacs-gpu` is built.
- `m11_5_semantic_acceptance` reports 0 tests and
`gpu_initial_target_acceptance` reports 1 without `--features crdt`.
Both are semantic-census suites, so gating Stage 2A in the default
config alone would exercise almost none of its relevant coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five findings, all real. The blocker and both majors are the same
mistake in three places: a claim asserted somewhere cheaper than where
it actually lives.
COHERENCE.md was stale in four places, not the three reported. Step 8
still read "no keybinding" and §11 still read "five settings", but §6's
dispatch table also still cited `is_terminal_escape_chord` — a symbol
this branch deletes. §25 requires that update to ride the PR, so a PR
changing audited ground truth has to re-grep the audit for its own
symbols, not only for its topic.
Acceptance 5 asserted a registry round-trip, which is a test of the
registry: it stayed green with the setting's only consumer deleted. It
now opens a real terminal whose child overflows the 24-row screen,
scrolls the view to its oldest retained row, and asserts LINE001 is
present at 10,000 and absent at 0.
Acceptance 8a waited for the session count to fall, which the rejected
editor-side cache map satisfies exactly — a map with no purge hook
leaks while sessions drain. Adds `TerminalManager::escape_caches()`, the
lifetime half of Q#TC4c's contract that `escape_parses` cannot cover.
`table.sort` over `pmacs.terminal.profiles` raised "attempt to compare
number with string" on the unknown-profile path whenever the user's
table held both a string and a numeric key, replacing the exact
diagnostic being asked for; `%q` raised likewise on a non-string
`profile` argument. Both are partial functions applied to user input on
a diagnostic path.
Also corrects the framing's status line, and a status message whose
embedded whitespace run had survived a rustfmt reflow.
Three new bites, each falsified by revert: deleting the scrollback
consumer fails acc5 and only acc5; restoring the raw-key sort
reproduces the comparison error verbatim; and implementing the rejected
map fails the new acc8a at left: 2, right: 1 while passing the old
session-count version.
Merges githubsucks/main @ ccf29e3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
Round 5 review: one P1, a frontend scope hole, and three P2s.
**1. A fallback that SPAWNS and then dies retried forever.** The
once-per-buffer guard bounds calls to `_attach_buffer`, not the server
those calls produce. `ensure_server` still never forwards `cfg.restart`,
so the fallback inherits `OnCrash`; an executable that exits before
`initialize` is respawned by the manager with no attempt ceiling —
silently, because `latched` has already disabled the primary's failure
poll. The fallback now gets its own one-shot die-before-initialize
watch, which retires it (ending the respawn loop) and reports.
The prior failing-fallback test used a NONEXISTENT executable, so it
only ever exercised synchronous ENOENT. To reach "spawned, then died"
the fixture has to actually spawn.
**2. Simultaneous frontends.** Both repair triggers read the ambient
`pmacs.window.buffer()`, and the daemon restores `active_frontend` to
the last-dispatched frontend before `tick_processes` — so a Lean buffer
active in ANOTHER frontend receives no `buffer.after-switch` here and
stays stale after its server is globally retired.
Fixed at the seam that is frontend-agnostic: **make consumption safe.**
`attached_for_active` now rebuilds rather than returning a record whose
server is dead, and `attachment_for_request` reports none (it must not
perturb LSP state, so it cannot rebuild). Whichever frontend runs a
command is the active one while it runs, so healing at the point of use
reaches every buffer no eager sweep can. This also closes the half where
a dead attachment was handed to a command and the request vanished.
**3. The retirement sweep stopped user-managed servers.** Selecting on
`language_id == "lean4"` also names servers the user spawned from
`init.lua`, which are not derived from `pmacs.lsp.config.lean4`. It now
keys on the `default-lean4` label `ensure_server` stamps — the
derivation discriminator.
**4. Repair ran even when no swap occurred.** `swap_to_fallback()`
returning false left `latched` true, so the next tick retried the
UNCHANGED configuration and reported it as a fallback failure. Split
into `probe.fallback_installed`: repair exists to apply a swap, so no
swap means nothing to apply.
**5. The once-per-buffer assertion counted table keys**, which cannot
distinguish "once per buffer" from "every tick for one buffer" —
cardinality stays 1 either way. Replaced with a numeric attempt counter;
the bite reports 174 attempts against the expected 1.
Five bites, each against 7c37bdc: no fallback watch -> attempt reaches
4; retire by language_id -> the user's server is stopped; gate repair on
`latched` -> a repair is attempted with no swap; drop the
once-per-buffer guard -> 174 vs 1; hand back a dead attachment -> a
command receives a `stopped` server.
Two more vacuity shapes recorded in the ledger (8 and 9): counting
distinct keys cannot bound repeated work, and a nonexistent executable
cannot reach any post-spawn failure.
#175 (bottom-panel Stage 2 framing) landed after this branch's last head
and touches both shared docs, so the previous green run did not cover the
combination. Merged cleanly this time — no conflict.
Also fixes an inconsistency this PR introduced: the recovery check still
accepted `d152120` while the canonical-base line above declared a newer
commit. A threshold looser than the base it guards passes on a tree the
rest of the file does not describe, so the two now move together and the
text says why.
Both conflicts were competing rewrites of the same anchor lines: #172
refreshed the canonical base and the handoff header while this branch
did the same for #165. Resolved by taking main's list, which is the more
accurate of the two (it names Lean 4 Stage 2 #161 properly), refreshing
it to the current tip `ccf29e3`, and keeping this branch's note that
lanes naming an older base have not been re-based.
#172 also removed the inline-math lane, so the stale-header note drops
from three back to two and now says who owes the remaining updates.
Round 4 review: one P1, and it is the same defect for the FOURTH time.
`pmacs.lsp.config.lean4` is a single global entry, so swapping its
command invalidates **every** Lean buffer and **every** Lean server —
Q#LN15 gives one server per project root, so there can be several.
Rounds 1-3 each repaired one buffer and retired one server, and round 3
shipped "repair the armed target, strand the rest": status and config
said fallback while a second open Lean buffer stayed on the retired
command, and a second project root's server stayed live.
The shape that actually holds:
* **Retire ALL `lean4` servers on latch**, not the one the probe
happened to name. `probe.primary` identifies the server the VERDICT
is about; it was never the set of servers the swap invalidates.
* **Repair each buffer lazily and at most once**, when it becomes
active — on `buffer.after-switch` and on the tick. `_attach_buffer`
is an active-buffer-only seam, so a global swap cannot be applied to
every open buffer at once; it has to be applied as they surface.
lsp.lua's own `after-switch` re-pushes views but does not rebuild a
stale attachment, so nothing else covered this.
* The **once-per-buffer bound** is load-bearing: without it a fallback
that also fails to spawn would retry every tick forever — the
round-2 defect, which a naive global repair loop would reintroduce
for every buffer instead of just one.
* `shutting-down` is deliberately not treated as stale. It is still
live by `server_is_live`'s reckoning, so attaching would early-return
the stale record and burn that buffer's single attempt on a no-op.
P2: argument-inclusive attribution was implemented in round 3 but pinned
only by "contains the command name", so a mutation dropping every
argument passed. Now asserted against the exact `<command> <args>`
string.
Also fixed a vacuous assertion this refactor created: a test checked
`_probe.reattach_from == nil` for a field that no longer exists, which
reads as nil and passes for nothing. It now asserts a positive count of
recorded repair attempts.
Three bites, each against 73587b0: repair only the armed buffer -> the
second buffer stays on `lake`; retire only the named server -> one live
stale server remains; drop arguments from attribution -> the exact-string
assertion fails.
The ledger records a second durable lesson beside the vacuity one: **a
scope error repeats until the scope is named.** Four rounds of locally
correct fixes, none of which asked what the config swap invalidates.
When a change edits shared state, enumerate everything derived from it
before repairing anything.
Closes review round 3 — 1 blocking, 1 high, 1 medium.
**R3-1 (blocker) — the call-site table contradicted the source.** The
three-boundary model was right; five rows of its classification were
not, and each was a real defect:
- `:6140` is `completion_dropdown_layout` — DOCUMENT completion
placement, deriving the space below the anchor line. Classified
status-owned, it would let completion overlap the panel.
- `:7195` and `:7212` are the `status_buffer` / `status_left_buffer`
`TextBounds.top` — status text bounds, classified document-owned.
- `:7351` clips global minibuffer CANDIDATE glyphs to the dropdown's
band anchor; classified document-owned, they would be clipped
against a boundary the dropdown does not sit above.
- `:8561` (`edge_scroll_direction`, document edge scrolling) was
missing entirely, leaving it tied to the old bottom.
- `:8077` is `code_caret_rect_in_clip` — caret clipping, not
completion placement. Its class was right, its label wrong.
Every production site is now individually verified against the source
and tabulated with what it actually is. The census is stated as
arithmetic a reader can check: 29 matches = 20 production + 1
definition + 8 test sites.
Root cause recorded in the revision history: rev 3's table was built
from a `grep | head -20` over 29 matches, which is precisely why
`:8561` vanished. The minibuffer's status-owned status is now argued
from Q#BP14b rather than assumed — it is global, bufferless chrome
anchored to the status band, so all four of its sites stay with the
band.
**R3-2 (high) — clamps preserved.** The three equations permitted
negative coordinates on a surface shorter than its chrome, where
today's `text_area_bottom` clamps with `.max(0.0)`. All three now
clamp at zero, which keeps the "exact formula" exact exactly where it
matters most.
**R3-3 (medium) — attachment rejection classified SHARED.**
`validate_cells` also rejects `cell.attachment.is_some()`
(`terminal.rs:305`), whose error text reads "which terminals never
use" (`:190-191`) — phrased as a terminal-specific fact, which is why
rev 3's "exact split" missed it. Panels implement no attachment
rendering in Stage 2, so a `PanelFrame` carrying one describes a
surface the GPU would silently not draw; shared rejection fails closed
on the producer side instead. The message is reworded grid-neutral
when it moves, and giving panels attachment rendering later moves the
rejection back deliberately rather than by default.
A2B-4 now names the counts on both sides (twelve document-owned move,
eight status-owned do not) and carries the three symptom-bearing rows
that a plausible misclassification produces. §9 records that the GPU
three-boundary split belongs to 2B, not 2A — it is only observable
once a band can be installed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 3 review: two P1 asynchronous-correlation defects, with the
focused suite at 25/25 while both were live.
**1. A late version verdict retired nothing and claimed success.**
`probe.watching` is cleared the moment the server initializes — it is
failure-polling state. A slow `lake --version` landing after a
successful initialize therefore reached `fire_latch(nil)`, which retires
nothing: `_attach_buffer` found the still-live primary attachment,
early-returned it, and the retry counted that as done. Status said
"falling back", the config named the fallback, and the buffer stayed on
the old server.
**That is the round-1 silent no-op arriving through a third event
ordering** — first as "no re-attach at all", then as "re-attach cleared
by an unrelated buffer", now as "re-attach satisfied by the server we
were supposed to replace". The fix separates the two facts that were
being carried by one field: `probe.primary` is the server the verdict
applies to and survives initialization; `probe.watching` is the
failure poll and is cleared by it.
The existing fixture could not reach this ordering at all — its `serve`
sleeps, so the primary can never initialize before `--version` returns.
The new one execs the fake LSP for `serve` and delays 0.6s before
reporting 3.0.0.
**2. `buf_key` was the most recently loaded Lean buffer.** Written on
every Lean `buffer.after-load`, so a second Lean file opened before the
verdict became the rebuild target while the latch still watched the
FIRST buffer's server. Target buffer and primary server are one fact and
are now armed together, exactly once. Both files in the new test share a
package, so mis-targeting shows up as a stranded buffer rather than as
two unrelated servers.
**3. The failure message hardcoded `lake serve`** after the latch became
command-agnostic, telling a user whose `my-lean-wrapper` failed to go
debug lake. `configured_command()` names what is actually configured,
arguments included.
**4. The ledger** now records all fifteen bites across the three rounds,
both prior review rounds' findings (the round-2 block was lost when an
earlier edit script aborted before writing), and the durable lesson.
That lesson, recorded for the handoff: **six tests across three rounds
were written, ran green, and pinned nothing** — caught only by biting.
The shapes are enumerated in the ledger; the rule is that a test is not
evidence until the mutation it targets has been shown to fail it. Two
of the six are subtle enough to be worth naming here: a bite that
RAISES is swallowed by the hook's pcall and "passes" for the wrong
reason, and a fixture whose `serve` sleeps cannot reach any ordering
where the primary comes up first.
Closes review round 2 — 1 blocking, 2 high, 1 medium — decides both
remaining open items, and re-integrates canonical `main` @ `ccf29e3`
(#172 + #157; documentation plus one `src/buffer.rs` regression test,
no protocol or Stage 2 source anchor moved).
**R2-1 (blocker) — the seam is three boundaries, not one.** Rev 2 asked
for a single document-bottom accessor. That is wrong: once a panel is
installed the present single value must DIVERGE, because several of its
consumers must not move at all. `text_area_bottom`
(`pmacs-gpu/src/main.rs:8490`) is today `status_band_top`,
`geometry_capacity_bottom`, and `document_text_bottom` at once. Rev 3
defines all three, classifies every one of its ~19 call sites as
status-owned / document-owned / geometry, and records that four sites
rev 2 named (`:3175`, `:3185`, `:6601`, `:6607`) consume a status-band
HEIGHT and no bottom coordinate at all, while the status background
`:5908` and status text `:7134`/`:7922` must stay at the physical
window bottom.
The acceptance is now a contrast assertion: installing a panel moves
every document-owned consumer WHILE the status band stays
pixel-identical. "Everything moved" alone is passed by a blanket
rewrite of the helper, which is exactly the wrong implementation.
**R2-2 (high) — epoch exactness.** `accept_frame_geometry` returns
`Advanced | Duplicate | Rejected` instead of a boolean that cannot
separate reconcile-needed from already-current from stale; if a boolean
is ever kept internally it must be named `advanced`, since `Duplicate`
is also accepted. Rev 2's exhaustion wording permitted retaining stale
geometry, which is not fail-closed — a real resize after exhaustion
would keep painting a panel sized to disowned geometry. The grid path
now clears `frame_geometry` to unknown and reconciles hidden, and the
frontend takes a terminal latch so a retained matching `Present` cannot
resurrect the band; only a fresh session clears it.
**R2-3 (high) — parent acceptance 52 splits.** 2A has no semantic panel
projection, so it can only prove the extracted painter honors an
explicit `None` map plus the `src/window.rs:562` comment fix. The real
contract is production-reachable only in 2B and is reasserted there
beside 42/43/44.
**R2-4 (medium) — touched gates named**: `statusline_segments_acceptance`,
`m11_5_semantic_acceptance`, `gpu_initial_target_acceptance`,
`gpu_font_acceptance`, beside the vterm, folding, and GPU suites.
Open items decided: `BASE_DIVIDER_HEIGHT = 4.0` at scale 1.0, scaled by
`FontMetrics::scale`, whole strip painted `ui.divider` and used as the
exact hover/drag hit rect; `TEXT_TOP` stays `16.0` unscaled, with
Q#BP15a's "all quantities use the frontend's current scale" narrowed to
font-derived metrics and the divider. Wholesale surface-inset/DPI
scaling is recorded as separate work, not smuggled in.
The ledger's bottom-panel lane keeps its census correction and gains
the three-boundary one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes review round 1 — 2 blocking, 3 high, 3 revision points — and
rebases the ground truth onto `main` @ `d152120`.
Both blockers were rev 1 asserting something the parent framing
already decided otherwise:
- **R1-1.** Rev 1 said all 23 census reads route through
`primary_document_window`. Q#BP14 routes only the **Projection**
class that way; focus/input (#13-#15, #23), focus chrome and
surface-routed (#16-#19), and focus/session (#20) keep their own
authorities. Rev 1's rule would have broken remote-op validation and
application, `DispatchIdle`, presence, focused search/menu/completion
routing, and terminal bell ownership. §3.2 restores the four classes
as a table and the acceptance asserts each separately — the
focus-class assertions are the load-bearing half, since a test that
only proves "the document is used" passes with them wrongly
rerouted.
- **R1-2.** The three `src/statusline.rs` active reads have two
dispositions, not one. Only `:644` selects the wrong window; `:629`
and `:675` must keep tracking actual focus, because grid contexts
need a truthful `active`, revalidation must notice a focus change,
and parent acceptance 42 requires a document provider to be able to
observe `active = false` while the panel is focused.
The three high findings:
- Q#BP2S1 resolves to frontend-owned epochs (option 1) — a font or
scale transaction can need to invalidate an old `PanelFrame` while
the derived `CellSize` is identical, which daemon value dedup cannot
detect. Rev 2 adds the four-row transition table, splits grid
allocation from semantic acceptance into two APIs rather than one
ambiguous method, moves the grid allocator off `saturating_add` to
checked-with-fail-closed, and defines the initial epoch and both
exhaustion behaviors. Rev 1's "rejects a lower-or-equal epoch
carrying different data" was itself wrong: a lower epoch carrying
identical data is still stale.
- The `panel_capable` flip is narrowed to an authenticated semantic
session negotiated at **v21 or later**. Denying a v20 peer the new
events is insufficient if the daemon still places its window in a
side panel it cannot render — the gate is on placement.
- Parent acceptance criteria 37-55 are declared authoritative and
mapped to slices 2A/2B, with rev 1's eleven drafts demoted to
refinements. The painter-extraction criterion now pins cells, the
returned cursor, the focused window's `view_top` mutation, and
passive-window state.
All four scout obligations are closed (§5), and the pixel formula is
treated as contract work, not implementation detail:
- The shared/terminal-only validator boundary is named exactly.
- Four new outbox tail-coalescing tags beside the existing four.
- **`State::mono_advance` is unsafe to adopt**: absent a `FontFacts`
probe it samples the document's first shaped glyph, which would make
panel columns document-dependent. The declaration uses the existing
stable normal-face `probe_mono_advance` instead, and declares zero
usable geometry when it returns `None`.
- `BASE_DIVIDER_HEIGHT` does not exist. Rev 2 decides its scaling and
requires **one** document-bottom accessor routing every consumer
(caret, hits, minimap, terminal geometry, clipping, edge scrolling)
— a second unrouted seam is precisely the Stage 1 `Layout::compute`
two-caller defect. The concrete base value is left open for round 2.
Also: the coherence statement now names journey steps 7-10 instead of
claiming none, and drops rev 1's overclaim that this advances
background-work visibility — a panel gives output a placement but adds
no join key to COHERENCE §9's four disjoint activity planes.
The ledger's bottom-panel lane is updated from "no branch and no
framing yet" to the framing's real state, and carries an explicit
correction: that entry was itself the source of rev 1's census
mis-statement.
Factual corrections: `InitialTargetResult` is at `message.rs:1145`;
`primary_document_window` has four references and two production paths
(`daemon.rs:1639`, and `daemon.rs:2998` via `primary_document_buffer`,
which is census #22); fifteen PRs merged since the parent's last
re-scout, not eleven.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stage 1 of the terminal config/copy-mode arc is in review; Stage 2 is
not started. Records the four decisions forced by scouted ground truth,
the four bites against four different wrong implementations, the two
reusable test instruments, and the gate results.
Three P1 lifecycle defects and two P2s. The focused suite was 20/20 with
every one of them live, which is the part worth keeping.
**1. The crashed primary respawned forever underneath the fallback.**
Round 2 skipped the retire call for terminal servers to avoid corrupting
them — but the crash had already armed `next_restart_at`, and
`maybe_restart` fires on every elapsed backoff with no attempt ceiling.
The broken command kept respawning under the live fallback.
The right call depends on the state, and each is wrong for the other:
`forget` REQUIRES a terminal state and removes the client outright,
which also drops the restart timer; `stop` is for a live one and
corrupts a terminal one (its not-initialized branch parks it in
`ShuttingDown` forever). `retire_server` now dispatches on state.
**2. Re-attachment targeted whatever buffer was active when the
asynchronous verdict landed.** `_attach_buffer` is an active-buffer-only
seam, and "some attachment now names a different server" is satisfied by
an unrelated Rust buffer — clearing the retry and leaving the Lean buffer
stale forever. The initiating buffer is now captured and the retry waits
for it.
**3. A failing fallback retried every tick forever, silently**,
contradicting acceptance 27's promise that a second failure surfaces.
"Waiting for the old server to go" and "attempting the replacement" are
now separate: once the old one is terminal or gone, the replacement is
attempted EXACTLY once, and a spawn failure is reported.
**4. The Lake version parser was being applied to arbitrary wrappers.**
`version_below_3_1` encodes lake's output contract; a working
`my-lean-wrapper` reporting "wrapper 1.0" would have been replaced
despite its server initializing fine. The version probe is now gated on
the command's basename being `lake`. The FAILURE latch stays
command-agnostic — that one keys on the server actually not starting,
which is true of any command.
**5. An unconfigured Lean server was reported as a failure** and latched,
poisoning the session so a later configuration could never take effect.
Absent config or command now means disabled; only a configured command
that produced no attachment is a failure.
**6. The ledger recorded pre-fix counts** after the fixes were pushed.
Now 25/25 and 3,214. That is the #161 fmt-blocker error in a slower
form: verification must describe the pushed tree.
Sign-offs requested in review: `M.fallback` is now `M._fallback`, an
underscored test seam, and its idempotence check compares args as well as
command — the same command with different arguments is not "already
applied". Dropping the `command ~= "lake"` guard stands for the failure
latch only.
Five regression tests added, and **three of them were too weak on first
write; only bite-testing found it**:
* asserting "no live non-fallback server" misses a respawn loop,
because a respawning server sits in `crashed` most of the time —
`attempt` is the observable that counts respawns;
* returning to a buffer with `find_or_open` re-fires
`buffer.after-load`, which repairs the attachment regardless of the
code under test — `switch_buffer` is the honest return;
* a MISSING command fails synchronously inside `after-load` where the
rebuild happens inline, so the async race cannot occur — only the
probe path exercises it.
Each of the five now fails against the exact round-2 mutation it targets.
Both conflicts were docs-only and resolved as unions, with one repair
taken from main: #156 fixed a pre-existing corrupted duplicate of the
"GPU initial target LANDED — #148" bullet in the handoff, whose tail ran
into the protocol-version text. This branch still carried the broken
copy, so the resolution keeps main's repaired `- Protocol **v20**` bullet
and drops the stub, along with main's now-superseded "Stage 1 IN REVIEW
as PR #165" sub-bullet.
Refreshed the canonical base to `d152120` and widened the stale-header
note from two lanes to three: #158 merged but its lane still reads
"PR #158 OPEN".
Brings the three required docs current after #158 merged, and discharges
the follow-up that framing named for itself.
COHERENCE.md section 16 audits the claim that the GPU frontend exceeds
the TUI "under real divergence pressure" without a privileged frontend
emerging. Inline math is the sharpest instance of that so far -- the GPU
typesets $...$ while the TUI shows LaTeX source, and the TUI fallback is
a named deferral. Section 25 makes that update ride the PR, so the
enumerated list gains the case along with what keeps it inside the rule:
the slice reserves no protocol version and adds no wire surface, so the
divergence is presentational and both frontends read the same model.
docs/inline-math-framing.md carried a licence error the slice framing
flagged in its own section 9 and deliberately did not fix in-branch,
since the parent is a merged document. Latin Modern Math is under the
GUST Font License, not the OFL; the row now says so and records the
~717 KiB bundled size.
docs/agent-handoff.md records the landing and re-anchors section 1 to
d152120. The bullet leads with the facts a fresh agent would otherwise
have to rediscover: the whole slice lives in pmacs-gpu because pmacs-gpu
depends only on pmacs-protocol and never on pmacs; the v0 subset is 34
Greek symbols, sub/superscript and \frac; an unsupported command fails
the WHOLE span back to source, so most inline spans in a real paper
still show LaTeX by design; and math is suppressed while the caret is
inside its span.
docs/active-work.md removes the merged lane per its own update protocol
and adds a Closed entry. Four things there are reusable beyond this arc:
a stale frontend binary is invisible from the source tree, so diagnose
with strings on the binary rather than by re-reading a checkout that is
already current; the dangerous integration was the one that did NOT
conflict, so decide from the shared-file set rather than from whether
git complained; integration is proved by predicting the other side's
test-count delta and checking it; and m4_5_basedpyright has no timeout,
hangs forever, and is intermittent, so an earlier clean sweep proves
nothing. It also corrects a claim I recorded on main: the branch's
missing CI was not an unidentified cause -- a conflicting PR builds no
merge ref, so no pull_request run is created.
Docs only; no code changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZjWMjwPXhPbt9upku9mCk