P1, and the worst of them: the AUTHORITY BLOCK was stale in four ways
while this lane's whole purpose was retiring stale ledger state. The
canonical base said `122b8e8` (actual: `e67ad07`), protocol said
`v6..=v22` (actual: v23, #228), the recovery threshold accepted
`9a26ac8` --- far older than the base above it, so the check it defines
would pass on a tree missing everything since --- and this lane's own
entry named base `0e4c58d` and declared itself PARKED beneath a heading
saying ready-for-PR. A ledger that contradicts itself defeats its own
cross-machine purpose. All four corrected; the park bullet is replaced
by what actually happened.
"Exactly two journey deficits" was stronger than the evidence permits,
and the table said so three paragraphs below itself: GPU 3(c) and 9 are
INFERRED, so a third failure cannot be ruled out. Now "known failures at
two steps; exact set pending verification". GPU 6(c) is inferred too and
is noted as unable to change step 6, which 6(e) already floors.
The product subgrade had criteria but no GRADING RULE --- three closure
conditions distinguish closed from not-closed, which does not derive
`Weak` rather than `Partial` or `Missing`. A rule is now stated, and the
distinction it turns on is worth having: `Partial` would mean the GPU is
merely unfinished; `Weak` means it is BEHIND a frontend shipping in the
same binary. Criterion 3 is marked NOT ASSESSED --- §3.2's register is
seeded empty --- rather than being quietly counted as a pass.
The framing REQUIRED a handoff §6 cross-reference and there was none;
only the Lean 4 numbering note existed. Added, carrying the facts a
cross-machine reader needs first: the product subgrade, the P2 start
gate, and that "Arc 8" means Lean 4.
Three more scorecard-versus-ground-truth drifts, the same class this
lane exists to close: row 14 said listview has 4 call sites "all LSP
panels" (it has 5 in `lsp.lua` and 4 in `git.lua` since #227, so it is
no longer LSP-only); row 15 said "no git integration at all" while §15
records status and diff; and §9's plane table said no builtin opens
`*lsp*`, though `M-x lsp.status` does (`lsp.lua:3533`) --- it has no
BINDING, which is the distinction this document now draws everywhere
else.
Finally, §20 physically placed the arc after Priority 5 while its prose
said after Priority 1. In a *Recommended Priority Order* position is the
schedule, so the block moves to sit directly after P1, and now states
its interleaving with P2-P5 explicitly rather than leaving it to
position alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The three remaining Stage 0 items, and the first is the one that
justifies the arc existing.
§16 now carries TWO subgrades. Architectural stays Strong; Product is
Weak. The single grade was true and was answering the wrong question ---
the protocol is excellent AND the GUI is not yet a daily driver, and one
cell could not hold both, so the product half was invisible. That is how
a frontend the reporter would not choose came to sit inside the
healthiest concern in this document.
The product subgrade has criteria, not adjectives: GPU >= local TUI at
every journey step (currently FAILS at 5 and 12, per §2b); the
daily-driver blocker list empty (currently nine); every surviving
divergence declared. The GUI arc is what moves it, and it should be
re-graded when the arc closes, not before. The scorecard row now POINTS
at §16 instead of carrying a grade, which is the "no orphan scorecard
row" ruling from revision 3 --- criteria and ground truth first, pointer
second.
§20 places the arc: Half A after Priority 1, with reaching Stage 4b as a
P2 START GATE. No later GUI stage begins until P2 has an approved
framing and an opened lane; the gate is on STARTING P2, not finishing
it, so the arc is never blocked on work nobody has begun and cannot
outrun the model it depends on.
Arc 8 retires as a GUI label (Q#GA4). "Arc 8" now unambiguously means
the LEAN 4 arc, which is the numbering that stays; the roadmap's "Arc 8
--- GPU structural parity" scope is Half B of the GUI arc, which is a
name and not a number. Recorded in `docs/agent-handoff.md` beside the
Lean 4 entry too, because that is where a cross-machine reader meets the
number first. Landed framings that say "Arc 8 adjacent" keep their text
--- they are dated records, and this is the disambiguation for anything
written from here on.
Also swept while in §16: its ground truth still said
`SUPPORTED=[6..=21]`. Same stale-range class as the scorecard row fixed
earlier in this lane, one section deeper, and found only because the
subgrade work required reading the section rather than the row.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Rule 4 retires a lane only once its durable facts have reached
`docs/agent-handoff.md`, so that was CHECKED per lane rather than
assumed. Four of the five were already covered --- the `commit_to`
capture-at-request-time lesson (#231), per-worktree gate target dirs
(#225), the `build-crdt` step and the `gpu_invocation` signature
(#228/#229), the v23 frozen-variant lesson (#228). One was NOT:
`texlab` appeared ZERO times in the handoff, so #230's lane could not
be retired yet and its facts are added first --- LaTeX is served by
texlab, its root comes from an upward `.texlabroot` walk verified in
texlab's own source, and `.git` is deliberately excluded because a
repository root is the wrong answer for a multi-file document.
The handoff's Fake LSP mode list was stale in the same breath: it named
four modes when the binary has twenty-eight. Enumerated from the binary
and grouped, because a stale mode list is precisely how a test ends up
covering the shape next to the defect --- which is the failure this
lane has now watched happen twice in the file-watcher work.
Retired: #224, #225, #228's PR-specific block, #230, #231. That is 737
lines of ledger describing work that has shipped.
#228 is the one that is NOT a deletion. Its PR block is retired INTO
the standing Discovery lane, which is rewritten to "Stages 1 and 2
merged; later work remains" --- deleting that lane on the strength of
one merged stage would have dropped four named pieces of open work.
While rewriting it, its first open item turned out to be what Stage 2
actually shipped (richer M-x rows over v23), so it is removed and the
rest renumbered: a completed item sitting in an open list is the same
staleness this commit exists to clear, one level down.
Left deliberately: #229's and #232's lanes, which are already labelled
MERGED accurately. They are Rule 4 candidates but not STALE, and
widening this commit to them would mix "the ledger lied" with "the
ledger is verbose".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Rebased onto e67ad07; the framing commit replayed with no conflict.
Three corrections, each verified against the tree rather than against
this lane's own notes:
COHERENCE.md said `v6..=v21` schema support. The ceiling moved TWICE
since: #221 to v22 for `LineWrapFacts`, #228 to v23 for
`MinibufferPromptRows`. Now v23. The same row's "production attach
remains v20" is correct --- `ADVERTISED_PROTOCOL_VERSION` is 20 --- and
is deliberately left alone.
Journey step 11 read "Works but undiscoverable --- no statusline
spinner/progress indicator anywhere". #232 shipped exactly that
indicator on 2026-08-09, so the row went stale the day it landed. Now
Partial, with what actually exists and what does not: the indicator is
there, the `*workers*` view still has no keybinding.
§9's "No progress indicator exists anywhere" carried the grep that was
the evidence for opening worker identity in the first place. Corrected,
with the part that did NOT change stated as plainly: a purpose says
what a job is doing, never who asked, and attribution is what §9
grades. THE SECTION'S GRADE IS LEFT UNTOUCHED pending a re-audit ---
moving a grade is an audit act, not a documentation correction, and
Stage 0 is docs-only.
U9's row claimed "whatever this is, it is not the tree" about a
same-tree green. This file's own rerun rule forbids that: a same-tree
green establishes intermittence only, and a tree can raise an
intermittent failure RATE without making it deterministic. Replaced
with "not deterministic on this tree; causation and rate effect
unresolved", and the wrong claim is quoted rather than deleted, because
it is the one a later reader would otherwise reach for.
Also corrects this lane's own earlier claim that `add0ba1` had done
half of Stage 0's absorption. It absorbed #227 and #234; five stale
lanes and both COHERENCE corrections remained.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Opens the GUI arc. The framing and the lane are this branch's FIRST
commit, which is both the arc's own rule for every PR in it and the
reason this branch exists today rather than after #227: the document
spent two review rounds as an untracked file in one worktree, and
uncommitted work does not travel between machines.
Revision 3 closed two blocking findings from the second round.
The closure comparison is falsifiable now. Revision 2 ordered its
grades but never said how a cell acquires one, and its "normalize to
the head grade" rule mis-graded exactly the rows it was written for:
journey steps 7 and 11 both read "works but undiscoverable" and would
both have graded `Works`, with the undiscoverability demoted to prose
that cannot block closure --- while discoverability IS the substance of
steps 4, 7 and 11. Grades now have observable criteria, a compound step
takes the minimum over declared subclaims, and the table carries three
columns (local TUI, attached TUI, GPU) compared against the local TUI.
Half B's dependency graph was inverted. Viewport facts are ABOUT a
window, and whether a semantic window is a daemon projection or a
frontend-local object is what the model stage decides --- so the facts
cannot be designed first. Revision 2 also had a framing-only stage
handing the sidebar geometry no stage shipped. The order is now model
framing, then the viewport/identity substrate, then splits plus
IMPLEMENTED side geometry, then the sidebar.
Q#GA4 through Q#GA12 are ruled and closed.
Four claims this revision inherited were wrong and are corrected with
their sources rather than quietly dropped: the daemon has always
emitted `InstanceSignal::Bell` (daemon.rs:1373), so Bell is
consumer-only; the GPU NEVER consumes daemon `view_top`, which the
backlog states in as many words; Q#DS9 scopes save AND restore, so
Stage 4b owns both; and the protocol ceiling is v23, not v22 --- #228
raised it after #221, and revisions 2 and 3 both read the range at the
audit anchor without re-reading it afterwards.
The three-column claim is narrowed to what it can support. The attached
TUI handshakes `semantic_render: false` and never consumes the
SemanticFrame family, so a shared GPU/attached gap cannot implicate the
semantic wire. The columns separate local from daemon-attached, and
attached-grid from semantic; source tracing still assigns ownership.
Q#GA6's citation is corrected to the one site that matters: local and
attached document events converge on the document handler, whose only
wheel arms are Up/Down at editor.rs:3189. The sites revision 3 cited
were terminal-content paths.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The arc completed at 122b8e8, so per rule 4 the lane comes out of the
ledger: durable facts move to the handoff section-1 arc bullet (the
pool-thread cost of sleep jobs, the loops-that-never-run cancellation
lesson, the group state machine and its double-enforced retirement,
the server-owned scan root with its reachable deterministic fallback,
the core-count-assumption CI lesson and the taskset rehearsal, probe
and witness-bound discipline, the stray-marker re-rooting hazard, and
the deliberately unbuilt kernel-notification option). The handoff
anchor and the canonical-base line move to 122b8e8.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The four-blockers bullet kept its own copy of the round-one claim that
round two disproved; both copies now point at the manual-spawn
adoption path. The facts-list heading no longer counts itself.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review round three on PR #235, plus the diagnosis of its first CI run:
all five test legs failed on one test, deterministically, while
sixteen local cores stayed green.
The mid-walk cancellation bound could not bite the per-entry poll
alone. The cancel lands two files into a 41-file directory --- root
contributes three dir entries --- so with the per-entry poll deleted
the directory finishes and the per-DIRECTORY poll catches at seen ==
44, under the old bound of 60. The bound is now 40 against an expected
exactly-35 (3 + one 32-entry poll stride), and the entry-poll-only
bite goes red at 44. Verified both ways.
The retirement helper observed a REQUEST, not settlement: it returned
as soon as an active row showed cancel_requested, which a worker that
ignored the token and completed successfully would satisfy. It now
waits for a completed row with status == "cancelled", making the
lane's "settles cancelled" claim true at the witness, not just at the
Rust layer.
The CI red: d3_pump(1600) between the mid-walk join and the late.bbb
write assumed the held walk would complete within 1.6 s. On a 3-thread
CI pool, 8 sleeps of 1200 ms drain in ~3.6 s of waves, so the file
landed before the held walk even STARTED and folded into the joiner's
baseline --- exactly the fold the test exists to assert for mid.bbb,
applied to the wrong file. Deterministic on every 2-4-core runner,
invisible on 16 cores. The drain is now an observable condition ---
at least one post-join walk completed and none active --- with the
saturation sleeps at 800 ms, and the three saturation tests plus the
whole eighteen-test family re-run green under taskset -c 0-3, the CI
pool shape reproduced locally.
A fixed-duration pump against pool-dependent timing is a core-count
assumption in disguise; the lane records it as such.
Superseded round-one text in the lane (the fallback "unreachability"
claim round two disproved) is corrected in place.
One gate run also hit the live attach-retry BrokenPipe row --- fourth
occurrence, all three required fragments verified against the durable
sweep log, recorded in docs/ci-red-signatures.md. This lane touches no
pmacs-gpu code, no wire, and no protocol; the same sweep passed twice
earlier the same day on materially the same tree. The retirement bar
(mechanism, not rate) is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements docs/lsp-file-watch-d3-framing.md revision 4, approved
2026-08-11 with the four rulings adopted as proposed: the honest bar
(absent at idle, one attributable job per concurrently due group), no
exclusions by default, server root_uri -> cwd -> attachment fallback,
and constants rather than config keys.
pmacs.fs.walk_tree: the whole recursive tree as ONE cancellable job
(JobKind::FsWalkTree, reply reuses ReplyKind::ReadDir --- identical
payload shape, so the Lua boundary needs no second conversion). Names
are base-relative; symlinks recorded, never traversed; an unreadable
subdirectory skips its subtree (scan_tree's pcall behaviour); only the
root failing to open fails the walk; the cancel token is polled once
per directory. Eight Rust unit tests, including flat-directory entry
parity with read_dir_blocking and the two review-round cancellation
cases (empty-tree pre-cancel; mid-walk via the cfg(test) entry hook).
The watcher itself is rewritten as the framed group scheduler. No
sleeps anywhere: one process.after-tick subscription (installed once
and guarded --- pmacs.hook.remove does not exist) drives every
(server, base) group's deadline off monotonic_ms, autosave's Q#AS2
idiom. The old design held one pool thread per sleeping watcher and
allocated 1 sleep + D read_dir jobs per watcher per tick --- 1,326
per tick for rust-analyzer's six watchers on this 220-directory
checkout. At idle there is now NO running job, which is also the
strongest witness in the suite: activity_summary settles to None, and
that assertion is unwritable under the old design.
The scheduler is the framing's state machine, all three review rounds
included: single-flight per group with generation-checked completions;
deadlines advanced from completion; the round-3 three-arm completion
partition (success / stale-or-retired / live non-success, with the
failure latch and quiet cancellation); joins wake the group, queue
exactly one follow-up mid-walk, and never reset the backoff curve;
per-watcher baselines --- the first snapshot whose WALK STARTED after
the join; membership captured at scan start; per-member cancellation
recheck at emit through the preserved _after_scan_for_tests seam;
backoff 250ms x2 to a 4s cap, reset by any emitted change; retirement
cancels the in-flight walk cooperatively.
Verification: eighteen acceptance tests. The six #234 tests are
byte-unchanged and green. Ten witnesses cover the framing's plan (the
review rounds added the fallback-determinism and root-boundary pair,
making twelve):
idle absence (and never a sleep purpose), one walk job per scan on a
twelve-directory fixture, join-wakes plus the registration epoch,
queued baseline for a mid-walk join (driven by saturating the worker
pool so the walk genuinely queues), single-flight under a withheld
completion pump, retirement and rebaseline through the fake's
unregister/re-register triggers, live cancel via pmacs._async._cancel
on the queued job, live failure with the once-per-error latch and the
preserved-snapshot recovery (DELETED for the pre-failure file is only
derivable from the retained snapshot), backoff shape from seam
timestamps, and the configured-root base.
Every witness was mutation-tested. Two findings from the bites:
- Retirement is DOUBLE-ENFORCED (unregister path and post-scan sweep)
and biting either copy alone is masked by the other; only biting
both goes red. Kept deliberately: the sweep covers seam-cancelled
members, the unregister path covers idle groups whose next deadline
is seconds away.
- The first idle probe was VACUOUS: it read pmacs.async instead of
pmacs._async, errored, and the unwrap_or_default made every sample
read as "absent". The probe now expects rather than defaults, so a
broken probe is a red test, not a green lie.
One environmental fact, recorded in the lane: an empty stray /tmp/.git
(since removed) made project detection root every markerless tempdir
fixture at /tmp, which under Q#D3-3 the watcher then faithfully
watched. A markerless-fixture red that looks like a watcher bug may be
an ancestor marker.
A pre-commit review round found four blockers, all fixed here:
- walk_tree checked cancellation only inside its entry loops, which an
EMPTY tree never enters --- a pre-cancelled queued walk returned an
empty SUCCESS, which the success arm would commit and diff into a
deletion storm. Cancellation is now checked before opening and
before returning, cancellation outranks a missing-root error, and a
unit test pins both.
- The neither-root-nor-cwd attachment fallback was still pairs-order
nondeterministic --- the exact accident D3 set out to remove, behind
a comment claiming otherwise. It now takes the lexicographically
smallest attachment directory. Verified at the spawn sites: every
server spawned with an attached file gets cwd = root, so the arm is
defensive and unreachable through production spawning --- which is
also why it carries no through-the-server witness.
- A base at the filesystem root joined as //path (and file:////path in
URIs). Both join sites now go through join_under, the root-aware
idiom dired's handler already uses, and the dir-of capture for a
root-level file ("" from the match) normalizes to "/".
- The walk-count and scan-times probes defaulted on error, so two
broken probes could compare equal and pass the retirement witness.
Every probe now expects --- a broken probe is a red test, the same
correction the vacuous idle probe forced.
A second pre-commit round found three more, all fixed here:
- Mid-walk cancellation was UNWITNESSED: both Rust cancel tests
pre-cancelled and the acceptance test cancelled a queued walk, so
deleting the internal polls left every test green. A cfg(test)
entry hook now flips the token at an exact entry boundary and the
witness asserts the walk stopped NEAR it (bound on entries
processed), which is what discriminates the polls from the
entry/exit checks. The retirement witness now holds a walk in
flight across the unregister and asserts the job settles cancelled.
- The "unreachable fallback" claim was WRONG: pmacs.lsp.spawn may
omit both cwd and root_uri, and ensure_server adopts such a live
server for markerless files (root_uri and key_uri both nil). The
lexicographic-minimum fallback now has a through-the-server
witness: five sibling directories, the minimum opened last ---
five, because with two the build's hash order coincided with the
lexicographic answer and the first-pairs bite survived.
- The root-boundary joins gained a witness through exported
production functions (the _deliver_status pattern): the matcher and
URI builder driven at base "/", where reverting either join_under
call makes the anchored glob refuse //hit and the URI grow a fourth
slash. No fixture can walk / for real.
Verification totals after both rounds: eight walk_tree unit tests,
eighteen acceptance tests (six byte-unchanged, twelve witnesses), all
mutation-verified.
No wire change, no PROTOCOL_VERSION bump; walk_tree is an fs binding.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Revision 4 absorbs review round 3. A live walk cancellation or failure
now has an explicit group-state transition: no snapshot, epoch, or emit;
the prior state and backoff survive; in-flight clears; queued joins run
immediately; otherwise the group reschedules. Distinct failures report
once until a successful scan clears the latch.
Correct the queued-baseline latency bound and add live-cancel and
live-failure witnesses. Record the round in the active-work lane; the
four user rulings still block implementation.
Review round 2: two P1 design gaps and one P2 overclaim, all in the
cadence revision 2 introduced.
A join now wakes the group: next_scan_at pulls to now, an in-flight
walk queues exactly one immediate follow-up, and a baseline is only a
snapshot whose WALK STARTED after the join --- an in-flight walk may
have passed a directory before a pre-join file appeared there, so its
snapshot as a baseline would turn that file into a false CREATED.
Without the wake, a backed-off group folds post-registration files
into the baseline and never reports them; today registration scans
immediately and the coalesced design must not regress that.
The group is now a defined state machine: single-flight per group
(overlap unrepresentable, not avoided), deadlines advanced from
completion (a walk outliving its interval degrades to back-to-back
scans, never overlap), stale completions rejected by generation
(#234 P2 at group scope), and retirement --- last member gone or
server death --- that cooperatively cancels the walk. Cancellation
therefore enters walk_tree contract and tests; polling the cancel
token between directory reads is the established job shape. The
after-tick subscription installs once and guards, because
pmacs.hook.remove does not exist (the P3 gap).
Q#D3-1 restated honestly: groups key on (server, base), so several
can be due on one frame and per-group single-flight still permits N
jobs. The bar offered is absence at idle plus one attributable job
per concurrently due group, with a global scan queue as the
alternative if one-at-a-time must be guaranteed.
Six round-2 witnesses join the plan: join-wakes, no-overlap,
retirement, queued-baseline, plus the round-1 epoch and idle pair.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review round 1 (five findings, four P1) and revision 1 did not survive
it. The findings are recorded in the document; three are cases where
the framing reasoned from the wrong mechanism.
The promised idle state was impossible: pmacs.workers.sleep is a
RUNNING job for its whole duration --- dispatched onto the worker
pool, sleeping in 1ms slices on one of available_parallelism - 1
threads --- and activity_summary counts every running job. A 4s
backoff sleep renders as a constant "sleep 4000ms", and filtering it
would touch the instrument. Revision 2 removes the sleep from the
design: one process.after-tick subscription owns every group's
schedule via monotonic_ms --- autosave's Q#AS2 idiom, whose own
comment names the pool-thread hazard. Waiting now allocates no job
and no thread; the indicator is absent at idle by its None-at-zero
contract, which also becomes the strongest witness in the plan.
The scan root is the server's, not a guess: root_uri then cwd then
the attached-file fallback. project.detect was wrong by the tree's
own testimony --- server rooting honors configured strings and
resolvers first, and texlab's Q#LX2 documents a root that detect can
never produce.
Coalescing gained delivery semantics: shared snapshots, per-watcher
baselines (the first snapshot completed after join), membership
captured at scan start, cancellation rechecked per watcher at emit
(#234's P2 rule, per member). Two epoch witnesses join the plan; the
six existing tests do not cover this and were never claimed to.
Exclusions default to NONE. Any unconditional skip deviates from the
registered glob contract (**/-leading globs can match under .git/,
and a server may register .git/HEAD outright), and walk_tree removes
the job-count economics that made skipping look necessary: 80% of
jobs becomes readdir syscalls inside one job.
Arithmetic corrected to this checkout: D = 220, so 221 jobs per
watcher per tick and 1,326 for rust-analyzer's six --- six of them
pool-thread-holding sleeps. Revised steady state: zero jobs at idle,
one walk_tree job while a scan runs.
Q#D3-1..4 rewritten accordingly; all four still block implementation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Framing and lane update at the branch's first commit, portable during
review. Revision 1 is a DRAFT and no implementation may start from it.
The remainder of issue #233 after #234: the watcher is correct and
still walks everything. The framing re-verifies the cost model at
add0ba1 and adds two facts the issue does not carry:
- On this checkout .git alone is 177 of 220 directories --- over 80%
of every walk --- and the machine does not even have an in-tree
target/ (external CARGO_TARGET_DIR). The issue's 187/46 numbers were
measured with one.
- The string-form base is NONDETERMINISTIC: resolve_watcher takes the
directory of whichever attachment pairs() yields first, so which
tree a bare-string watcher can see depends on table hash order. #234
made matching correct per base; which base is still accidental.
Design space A-E with the trade-offs stated: coalesce per (server,
base); a Rust walk_tree primitive (one job per scan instead of one per
directory --- no new crate, no wire change); an ignore list, with the
target/ staleness hazard named (rust-analyzer's **/*.rs covers
OUT_DIR outputs, so an aggressive default trades churn for staleness
in exactly the server the issue is about); idle backoff; and kernel
notification, deliberately staged separately because A-D are pure wins
it does not obsolete and a new-crate decision deserves its own
framing.
Proposed Stage 1: walk_tree + coalescing + backoff, VCS-only skip
default, project-root base preference. At rest on this repo: from
~1,100 jobs per scan-bound tick to one job every four seconds, named
for its root.
Four rulings block implementation (Q#D3-1..4): the acceptance bar
(one attributable blip at idle, not silence --- silence is Stage 2),
the skip-list default and where it lives (a list-valued setting is
not expressible in today's ConfigValue), the string-form base change,
and knobs vs constants.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The absorption pass after the two merges of 2026-08-11: the LSP
file-watcher fix#234 (ae84d58) and git integration Stage 1 #227
(b867f64), landed in that order so the 2026-08-10 hold on #227 was
discharged rather than overridden.
The handoff anchor moves 9a26ac8 -> b867f64 and lists the ten-merge
first-parent chain between them. Only #227 and #234 are absorbed into
section 1 at this anchor; the eight between keep their facts in their
ledger lanes, several of whose headers still say OPEN --- the anchor
paragraph says so explicitly and repeats the ledger's own rule: trust
the canonical-base line over any lane header.
Both lanes are rewritten to their remainders per rule 4's arc test:
- LSP file watcher: the arc is issue #233 and it stays open until D3
closes it. The lane now carries the D3 start-state --- the walk
still recurses into everything every 250ms, no notify dependency in
the tree, no ignore-list infrastructure to reuse --- and the user's
ruling that D3 is next.
- Git integration: Stage 2 (gutter markers) is the remainder, and the
lane preserves the one scheduling constraint that matters: it needs
new DecorationKind variants, a PROTOCOL_VERSION bump, so it must run
alone. The five-round review history stays in the PR and framing;
what the handoff absorbs is what the next lane needs: the
capture-at-invocation census and its single-mechanism fix, the macOS
EILSEQ portability fact and its latent crdt-module sibling, the
root-parsing byte lessons, and the copy/rename presentation ruling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review of the #233 implementation found two correctness defects. Both
are cases where the FIRST fix for #233 was itself wrong, which is worth
naming: this lane repaired absolute globs and, in the same change, broke
a case that had worked since May.
P1 --- `resolve_watcher` returned "absolute" for EVERY string, so the
form was carried but derived from the union arm rather than from the
pattern. A bare `*.txt` is a valid relative pattern (LSP 3.17 defines
`Pattern` relative to a base path; VS Code treats string watchers as
applying across workspace folders), and classifying it absolute matched
it against `<base>/foo.txt`, which `^[^/]*%.txt$` can never match. A
leading `/` is what makes a pattern absolute. The `filewatchflat` test
could not catch this: it sends the RelativePattern OBJECT form, so it
constrains the object arm, never the string arm the regression lived in
--- the framing's own F1 finding, repeating inside the lane that named
it.
P2 --- `scan_tree` awaits `read_dir` once per directory, so the watcher
coroutine sits suspended for most of a tick with `_sleep` already
cleared. A cancel arriving there sets `cancelled` and has no sleep to
interrupt, and the resumed scan ran on to `did_change_watched_files`:
one stale batch under the superseded pattern, which is a wrong-pattern
notification the server acts on. Cancellation and liveness are rechecked
after the scan.
Both tests are mutation-checked and each bite fails only its own defect.
Reverting P1 fails the bare-string test alone --- the absolute test
still passes, so the two readings are independent. Deleting P2's recheck
reproduces the defect verbatim: `.received = "1 file:///…/foo.txt"`,
a batch from a watcher that was already cancelled.
P2's witness needs a seam. The race is a cancel landing during one of
the scan's suspensions, which no arrangement of real timing produces on
demand --- the same situation, and the same device, as `git.lua`'s
`_deliver_status`. `pmacs.lsp._after_scan_for_tests` is nil in
production and is handed the SCAN RESULT deliberately: a test that
cancels on any other scan passes with the recheck deleted, because the
loop would break at the post-sleep check and emit nothing anyway.
Gates: 9/9 green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The lane header carries the PR number and the ref it was opened at, so
the next machine can find the review without searching GitHub.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements D1 and D2 from docs/lsp-file-watcher-framing.md (revision 1,
approved 2026-08-10). The user ruled that the walking survives this
lane; D3 gets its own framing.
D1. resolve_watcher now returns (base, pattern, form) and the watch
record carries the form. Per LSP, a plain-string glob matches the
file's ABSOLUTE path while a RelativePattern's pattern is relative to
its base --- and resolve_watcher discarded the distinction, so every
downstream consumer matched relatively. Real servers send absolute
globs: rust-analyzer was never told about any file change (all six of
its globs absolute), and gopls saw go.mod but never a .go edit. The
match subject is chosen in start_file_watcher --- form "absolute"
matches base .. "/" .. rel, form "relative" matches rel unchanged.
scan_tree still walks in relative terms; only the string handed to the
matcher changes.
D2. register_file_watchers now cancels the outgoing record list before
file_watchers[skey][reg.id] = recs drops the only reference to it. The
cancel loop is factored into cancel_watch_records, shared with
unregister_file_watchers, so the two paths cannot diverge.
rust-analyzer registers the same id twice with no unregister between
--- previously 12 concurrent pollers, 6 permanently uncancellable.
Verification, per the framing's plan. Three new fake-LSP modes and
tests beside m4_24, each mutation-tested against the defect it names:
- filewatchabs registers a plain-string absolute glob whose relative
reading matches nothing. Red before D1 (reverting the match subject
fails exactly this test), green after.
- filewatchflat registers a RelativePattern without a leading **/
(*.txt at the base) --- F2's guard. Matching every form absolutely
fails exactly this test, so the obvious wrong fix cannot land green.
It also pins that a base-level pattern does not match into
subdirectories.
- filewatchrereg registers the same id twice (rust-analyzer's shape).
The witness is observable polling, not table shape: f.old exists on
disk before either .new event lands, so a leaked watcher at the same
250ms cadence reports it before the second positive. Reverting D2
fails exactly this test, the leaked .old event visible in .received.
m4_24 stayed green under all three mutations --- the framing's F1
finding (the existing test is insensitive to D1 in both directions),
confirmed rather than assumed. It is kept unchanged.
The framing doc records the approval and the answered ruling; the
active-work lane moves to IMPLEMENTED with the verification results.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Framing and lane at the branch's first commit, so the document is
portable while it is reviewed. The GUI arc framing spent two review
rounds as an untracked file in one worktree; that is the lesson being
applied, not a preference.
Revision 1 is a DRAFT and no implementation may start from it.
Scope is D1 and D2, the two bug-shaped defects. Every claim in the
framing was read or executed against `0e4c58d` rather than carried from
the issue --- including re-running the tree's own `expand_braces` /
`glob_one_to_pattern` / `glob_matcher` under LuaJIT, which reproduces
the issue's glob table exactly, compiled patterns included.
#232 is NOT at fault and nothing about it should be reverted. The
activity indicator renders real in-flight jobs; what changed is
visibility, not behaviour, and the watcher has polled since 1c25730 in
May. Quieting the indicator would delete the instrument that found this.
Two findings the issue does not carry, both of which change the fix:
The existing test cannot discriminate this fix IN EITHER DIRECTION. The
fake LSP registers `**/*.txt`, which compiles to `^.-[^/]*%.txt$`, and
`.-` spans `/` --- so it matches relative and absolute subjects alike
and `m4_24` passes whether D1 is fixed or broken. The issue calls the
tested and exercised paths disjoint; the sharper statement is that the
one existing test is INSENSITIVE.
And "match the absolute path" alone would break `RelativePattern`:
measured, `*.txt` matches `a.txt` but not `/base/a.txt`.
`resolve_watcher` returns `(base, pattern)` and discards which form it
came from, so its CONTRACT has to change, not just the match subject. A
fix that misses this trades six broken rust-analyzer globs for every
RelativePattern that does not begin `**/`. The verification plan
includes a test that fails against exactly that wrong fix.
The framing also records what this lane does NOT fix, so the report is
not mistaken for closed: neither D1 nor D2 stops the walking, because
`walk` recurses unconditionally and `matches` gates only recording. The
modeline will keep flipping at roughly half the rate. Whether that meets
the acceptance bar is an open ruling for the user, stated as one rather
than assumed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The async completions rendered through whichever frontend was ambient
when git exited. Run `M-x git.status` in frontend A, let B become
active while `git status` runs, and A's panel opened in B. The
generation and the root were already captured at the keypress; the
frontend was the one input still read late.
Captured at invocation in all four entrances --- `git.status()`,
`_on_refresh`, `_deliver_root`'s hand-off, and the `git.diff-file`
command --- and threaded on the request table exactly as this module
already threads the generation and root. Committed under the profile
each surface actually takes: `"panel"` for `*git-status*`, `"document"`
for `*git-diff*`.
`set_status` moved INSIDE the status commit. Rows and message are now
computed first and emitted together, because a failure message
announcing a panel that the commit then refuses is the same misrouting
in its most confusing form.
A refused commit DROPS the render, which is the answer the
`expect_buffer` rule already gives when the panel a refresh belongs to
was killed. `commit_to` refuses before the body runs, so there is no
partial render to undo.
THE FIRST VERSION OF `g6_25` WAS WORTHLESS AND PASSED ITS OWN MUTATION.
`panel_text` finds `*git-status*` by NAME, which is global --- it
answers "does this buffer exist", not "which frontend is showing it"
--- so a render into the competitor satisfied it. Rewritten against
`side_window_for` and each view's active window, it now fails both
bites: removing the status commit grows a `*git-status*` panel in the
competing frontend; removing the diff commit hands it the document
window.
The merge also surfaced a cross-lane break invisible until the suites
ran: #232 made `purpose` required on `pmacs.process.spawn`, and this
module's spawn is on this branch, so it was never among the 11 sites
#232 updated. Each spawn now carries its own purpose and NOT the label
--- all three are labelled `git`, and only the purpose separates
resolving a repository from reading its status from diffing one file.
Existing tests that drive `_deliver_status` / `_deliver_diff` through
the internal seam now supply a captured destination. That includes the
one place it is load-bearing rather than uniform: `g6_23` reuses its
"stale" request table at the CURRENT ticket as its positive control,
which is the only one of those three deliveries that reaches a render.
Gates: 12/12 green --- git_status_stage1, destination_capture, journey
and worker_identity acceptance, plus the full sweep.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Merged rather than rebased, for the reason #231 was: nineteen commits
replayed against a ledger four lanes had rewritten is nineteen chances
to lose a lane entry, and the reviewed commits keep their SHAs. One
file conflicted.
The conflict was diff3-style with an EMPTY base --- both sides added
lanes at the same offset --- so it resolved by keeping both, this
branch's #227 lane ahead of main's. Worth noting because an empty base
section is exactly what produced the stray `|||||||` markers found
committed in this file earlier today: the base half of a diff3 conflict
reads as content if it is resolved by hand in a hurry.
This unblocks the lane. `pmacs.window.capture_destination` now exists
on the branch (`src/lua_bindings/window_panel.rs:638`), which is what
#227's review found missing and what #231 was split out to provide.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Merged rather than rebased. Eighteen commits replayed against a ledger
that three other lanes had rewritten meant eighteen conflict
resolutions in `docs/active-work.md`, each one a chance to lose a lane
entry; merging resolves it once, against the state that actually ships,
and leaves the reviewed commits' SHAs intact. Only one file conflicted.
`docs/ci-red-signatures.md` auto-merged **without a conflict** — the
same silent path that produced duplicate U4/U5 ids when #232 rebased.
Verified by hand afterwards: ids U1-U8 are disjoint. They are out of
numeric order (U6/U7 sit ahead of U4/U5) and are left that way rather
than moved, since the note at the U6 row explains the history and
relocating sixty lines inside a merge commit hides real changes.
Three leftover conflict markers were sitting in `docs/active-work.md`
on `main`, committed by an earlier lane's resolution. `git diff --check`
flags them — but only for a working-tree diff, which is why the gate's
`diff-check` step never saw them and they survived several merges.
Removed here.
The U4 row is corrected on evidence this lane produced:
- **Flavour was wrong as a matching key.** The row was filed from
#229's `lua54` red and put the flavour in the key; #231 reddened the
identical selector with the identical three fragments twice on
`luajit`. Matching as filed would have missed both.
- **A fourth sighting was a deliberate bite, not an occurrence** — the
defect reintroduced on purpose during the test's own development. It
is recorded for what it proves instead: the genuine defect and these
CI reds are signature-indistinguishable, same message class and same
full-timeout duration.
- **The control experiment is written down with its own bounds** — five
green base observations against 0/2, 4.8% under an equal-rate model,
and the two facts that bound it: attempt 5 reddened a different
selector, and the branch side was never resampled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The pre-rebase warning was right, and the mechanism is worth recording
because it is the quiet kind.
gate-protocol-build landed its own U4 and U5 in #229. On this rebase
git merged docs/ci-red-signatures.md WITHOUT A CONFLICT --- the two
lanes appended their rows in different places, so there was nothing
textual to resolve --- and produced two ### U4 and two ### U5 headings
describing entirely different incidents. No marker, no complaint.
That is the failure the matching rule exists to prevent, arriving
through the one path a careful conflict resolution would never catch:
there was no conflict to resolve.
Renumbered across all four sites the warning enumerated: both headings,
the prose relation-to-U4 field inside what is now U7, and the
active-work.md reference. Ids verified unique afterwards rather than
assumed.
The warning block itself is retired in place, replaced by a note saying
what was done and why, so the next reader sees a completed action
rather than an outstanding one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The previous round left the suite at 24 and this round adds one test,
so it is 25. grep -c on the test attribute confirms 25 and the run
reports 25/25. The bullet said 26, and I repeated it upstream without
counting.
The entry now shows the arithmetic --- 24 before, plus one --- rather
than just a corrected number, and names the earlier figure so a reader
who saw it does not treat this as a second suite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The two reds recorded a moment ago were followed by a full green run of
the same command on the same tree — all 13 steps, log
`20260809T200907Z-2672209`. Both the lane entry and U5 now carry that,
because a signature row that records only the reds overstates them: the
green rerun is part of the evidence, not a reason to delete the row.
The row stays live and stays U-classified. Three load-sensitive render
budgets going red one per run and then green is consistent with a loaded
machine and with nothing else in hand; it is not a measurement of one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The lane entry gains round 3: the wrong-surface diagnostic, why the job
and process refusals now say different things, the anti-collapse test
and its three mutation checks. Written here rather than left in the
commit message because this file is what a recovering agent reads.
`docs/ci-red-signatures.md` gains **U5**. Two consecutive `scripts/gate`
runs of the same command, on the same tree, red on step `12-sweep` with
a DIFFERENT wall-clock render-budget test each time — 224ms and 258ms
against a 200ms budget, 114ms against a 100ms budget, at load average
12.9/23.9 with sibling worktrees building. Each passes in an isolated
rerun of its own selector, and no selector reds twice.
The rotating selector is the signature, and it is a stronger one than
any single test name: a regression that moved between three unrelated
render paths on an unchanged tree is far less likely than one loaded
machine. The observing diff is two string literals, their doc comments
and one test, and touches no render path at all.
Kept separate from U4 rather than merged. U4 is two budget tests in
`04-lib-crdt` failing TOGETHER; this is three render-budget tests in
`12-sweep` failing ONE PER RUN. Merging them would assert a shared
mechanism nothing in hand shows, and the load confound stays unmeasured
in both — a rival explanation, not a finding.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two files, no code.
## `docs/active-work.md` — review round 2
The lane's volatile block gains the round-2 record: the three findings,
why P2a's fix is a mapped diagnostic and P2b's is escaping at
presentation rather than rejection at the registry, and the seven new
mutation checks. The gate outcome is recorded with its step counts and
the two stop-signal facts — `journey_acceptance` 47/47 and all three
`#pmacs.process.list()` leak detectors byte-identical to `main`.
It also records what P2a's audit found and did NOT fix:
`pmacs.process.spawn`'s other string fields still convert generically.
That is pre-existing and out of this lane's diff, and it is named so it
is not silently inherited by whoever reads the fixed `purpose` read and
assumes the rest matches.
## `docs/ci-red-signatures.md` — R7's third occurrence, and U4
**R7 reproduced, and the control the second-occurrence note prescribed
finally discriminated — against its own hypothesis.**
Occurrence 2 left exactly one causal path open: the observing lane had
added a GPU-heavy `render_offscreen` test to the same binary, and
contention with a one-second socket handshake was plausible. That note
prescribed the control to run if a third occurrence landed — with the
added test removed, not at the merge base. A third occurrence landed, at
the gate's `gpu` step, with all three fragments verified against the
durable log.
The control was run. **Ten full `-p pmacs-gpu` runs with the added test:
10/10 green. Ten with it `#[ignore]`d, nothing else changed: 1 failure
in 10, all three fragments present.** Removing the suspect made the
failure more frequent, so the concurrent-test path is excluded — no
contention story from that test survives that direction.
The more useful result is the rate. This is the first rerun in R7's
history to reproduce anything at all, and it puts the failure at roughly
1-in-10 under ordinary `-p pmacs-gpu` load. Three sightings were not
enough to bisect a handshake; 1-in-10 is. The row now says so, and tells
the next agent to instrument which side closes the pipe rather than
re-run for green.
The lane is still not attributed — now for a measured reason rather than
an argument from diff shape: the arm without the lane's only
`pmacs-gpu` addition is the arm that went red.
**U4** records the other two reds from that same gate run:
`criterion_1_end_of_line_typing_completes_sub_frame_per_keystroke` and
`composition_overhead_under_ten_percent`, both wall-clock budget
assertions, failing together in `04-lib-crdt` and both green in
isolation and in the next full run. Fragments captured, so unlike U1–U3
it is matchable — it is a `U` row for want of a mechanism, not for want
of evidence. The signature named is **the pair**: two budget tests
failing in one run and neither in the next is far more likely to be one
loaded machine than two simultaneous regressions, and a future run that
reds only one of them is a different incident.
Neither row claims harmlessness, and the concurrent-worktree load
confound is recorded as a rival explanation rather than as a finding,
because it was not measured.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Review round 2, findings P2a and P2b, plus P3's stale recovery
summaries. Three defects, and the fix is deliberately different in each
place because the constraint is.
## P2a — invalid UTF-8 bypassed the `purpose` diagnostic
`required_purpose` read the field with `value.to_str()?`. Lua strings
are BYTE strings, so `purpose = string.char(255)` is a value a caller
can write, and `?` surfaced mlua's generic conversion error BEFORE this
lane's own diagnostic was ever constructed: the caller was told neither
the field nor the rule.
**This is the third time this project has hit the class** — an unowned
Lua string converted with `?` ahead of the owned message; the
destination-capture lane corrected the same shape two rounds ago. It
refused before spawning and nothing leaked, so the defect was the
message, not the outcome. The conversion failure is now mapped onto
this function's own message, and the new acceptance row asserts on
message CONTENT so retyping the read as a bare `?` breaks the test
rather than silently degrading the error.
Auditing the rest of the lane's diff for the same class turned up
exactly one more: `_push_dispatch_name` took `name: String`, so a
registered handler name that was not valid UTF-8 failed at first
dispatch with mlua's generic message. It now takes `mlua::String` and
maps that failure onto an owned diagnostic naming the argument and the
rule. Those are the only two Lua-string reads this lane added; every
other binding it adds takes `()`.
## P2b — no safe display-text boundary. Two halves, two different fixes
### Handler names are refused at the source
`pmacs.workers.register` type-checked its name and nothing more, which
was defensible while the name died inside `dispatch`. It no longer dies
there: the ambient carries it into every job the handler allocates and
composes it into `purpose`, which `*workers*` and the modeline both
render. So it now gets `purpose`'s meaningful-value standard —
non-empty, not whitespace-only — plus control characters, which have no
legitimate place in a registered identifier.
### Purposes are ESCAPED at presentation, not rejected at the registry
A purpose may legitimately contain a newline: a filesystem path can, and
`pmacs-magit`'s spawn purpose is a whole argv. **This is the shape of
the `#228` decision, and it is consistent with it** — the one-line
constraint belongs to the surface that has it, not to the registry that
does not. There, `Command.description` stays free-form and the two
single-row consumers clip with `description_first_line`. Here the
equivalent is escaping rather than clipping, because a purpose's later
words are load-bearing: an argv's second word says which file, and a
clip would drop it silently.
`purpose_for_one_row` states the property it exists for: **a row must
not be able to forge another row.** It escapes `\n`, `\r`, `\t` and the
rest of the Unicode `Cc` class (which covers ESC, so a purpose cannot
open a terminal escape sequence either), borrows unchanged when there is
nothing to escape — making byte-identity structural rather than
asserted — and deliberately does NOT escape backslashes: no number of
them produces a second row, and doubling them would cost byte-identity
for ordinary text.
Two surfaces call it: the `*workers*` rows, and `ActivitySummary`, which
exists for one consumer that has exactly one row.
`pmacs.workers.snapshot()` is this lane's `describe-command` and stays
raw, which is what makes this a rendering decision rather than data
loss — asserted, not assumed.
## P3 — two stale recovery summaries
`docs/worker-identity-framing.md` still said "Implementation may
proceed"; it is implemented. `docs/active-work.md` still said Stage 1
takes the "first two" of owner/purpose/parent — `owner` was REMOVED in
revision 2, so it takes one of the three, and the claim the whole
`owner` argument overturned was still standing in the volatile state of
record. Both fixed section-locally.
## Verification
`tests/worker_identity_acceptance.rs`, 18 -> 24 tests:
* invalid-UTF-8 purpose refused by THIS lane's message, asserted on
content, alongside the absent / empty / whitespace / wrong-type /
metatable rows;
* a whitespace-only handler name and a control-character one are each
refused AT `register`, asserted on the error and on the handler not
being installed (dispatch reports `unknown handler`);
* a non-UTF-8 handler name is refused before the handler runs, with the
dispatch-name stack left empty;
* a purpose containing a newline renders as ONE row in `*workers*` and
as one line in the modeline — through the real rendering path, the
latter through a painted frame as well as the evaluator;
* **a purpose crafted to look like a row boundary does not produce a
second row** — asserted by counting rows, with the escaped text
asserted present so a renderer that dropped the purpose entirely could
not pass;
* a purpose with no control characters is byte-identical on both
surfaces, fixtured with a literal backslash, a literal `\v`, quotes
and a non-ASCII character.
Mutation-checked, seven guards, each failing its own test and no other:
the purpose UTF-8 diagnostic; the `_push_dispatch_name` one; the
register whitespace guard; the register control-character guard; the
`*workers*` call site; the `ActivitySummary` call site; and
`purpose_for_one_row` itself neutered to the identity, which fails both
surfaces' tests and nothing else.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two section-local edits, no reflowing.
**`docs/worker-identity-framing.md` §2** named the three existing
statusline adopters by FILE — `terminal.lua`, `syntax.lua`, `lsp.lua` —
which is accurate and misleading together: `syntax.lua` registers its
provider under the name **`"mode"`**, so the registry inventory reads
`["mode", "terminal", "lsp"]` and a reader looking for the syntax adopter
by name does not find one. That is what made this lane's change to
`tests/statusline_segments_acceptance.rs` surprising, and the next reader
should not have to rediscover it. Also records that where a fourth
registration sorts is decided by **load order**, not by name.
**`docs/active-work.md`'s lane block** records review round 1: the
`pmacs.process.spawn` blocker and its fix at `2162737`, the five refused
shapes, the eleven updated call sites, the two audit-fixture occurrences
that are deliberately untouched, the three added mutation checks, and the
two acceptance suites the round added to the gate line
(`compile_mode_acceptance` and `m8_6_acceptance`, because the round moved
their spawn call sites and `m8_6` covers the `pmacs-magit` package
fixture).
It also records the breaking-change decision with the reasoning that
justified it, rather than only the outcome: the binding has no
API-reference documentation and no stability promise, `lua_to_spec` has
one caller, and §10/P7 put the third-party population at ~zero — so the
cost of requiring the field is at its minimum now and rises from here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Section-local update to `docs/active-work.md`'s "Worker identity Stage 1
(§9)" block, which was written pre-implementation with the lane's first
commit. It now records what actually shipped rather than what was
planned, because the difference is where the reviewable claims are.
Three things it records that a status flip alone would not:
- **`journey_acceptance` passed UNTOUCHED (47/47).** Q#W-7 edits the
`commit_to` guard family, so that suite was the lane's stop signal: any
established pin needing an edit would have meant the change altered
Journey Stage 1a's semantics rather than closing a gap in them. The
same for all three `#pmacs.process.list()` leak detectors, which are
Q#W-4's preservation claim.
- **One pre-existing assertion did change**, and it is named here so the
change is not mistaken for an accommodation: the builtin statusline
provider inventory in `statusline_segments_acceptance` grows by the
fourth adopter. That assertion exists to grow.
- **Two residuals, stated rather than tested around.** A raw
`coroutine.yield` inside either dynamic scope still leaks the scope,
and Q#W-7's reachability by a real caller is unproven.
Also lists the surfaces that changed shape — the collapsed allocation
funnel, the two grown constructor signatures, the new required fields —
for anyone rebasing a concurrent lane onto this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two changes, both from review, both about claiming exactly what is
enforced and no more.
Rule 1 said it covered "all yield points". It covers the two supported
pmacs yield APIs. Raw coroutine.yield remains reachable: R46 is a
convention ("package code uses :await() rather than coroutine.yield",
async.lua:26-27), not an enforcement, and the scheduler diagnoses a
non-Handle yield only after the fact --- step() resumes at :197 and
inspects what came back at :212, by which point the coroutine has
already suspended and the enclosing dispatch never returns to run its
pop. No refusal sited in a yield helper can intercept that.
So the property is stated as what it is: the supported ways to yield
are refused inside the scope, and an R46 violation can still leak the
name --- loudly, through pmacs.error into *errors*, but unrestored.
Section 6 says explicitly that this is NOT asserted, because a test
implying coverage the design lacks is worse than the recorded gap.
Q#W-7 is approved into this lane rather than split out. Same supported
helper, same invariant, same async.lua edit family; splitting would
preserve a known hole without reducing integration risk. So
yield_to_next_tick gains both refusals --- the new
_in_dispatch_name_scope and the missing _in_commit_scope --- and the
commit_to gap closes in the same commit as rule 1. Its witnesses are
the same pair as rule 1: the refusal fires AND the scope restores, on
the reasoning that a guard which raises while leaving the scope pushed
trades a silent fault for a loud one and fixes neither.
Reachability by a real caller stays UNPROVEN and the framing says so in
three places, including here. The defect was found by reading; the
tests pin the guard rather than reproducing a user-visible bug. Nobody
should later cite this as evidence the bug was observed.
The causal-extent paragraph and the Q#W-5 comparison were both
re-scoped to match, since both leaned on "non-yieldable" as an
unqualified property.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Review found that revision 2 asserted the property its whole design
rested on. It called the dispatch-name extent "synchronous" and never
checked. A registered handler is arbitrary Lua running inside
pmacs.async and may call Handle:await(), which parks the coroutine with
the name still pushed --- so every tick callback and every other
coroutine allocating a job in the meantime inherits it. The existing
tests already await inside pcall, so this is the ordinary shape of an
awaiting handler, not a corner case.
Rule 1 now enforces non-yieldability instead of assuming it, and the
enforcement was already in this file: Handle:await refuses to run
inside pmacs.window.commit_to (async.lua:87-90), with a comment giving
exactly this reasoning --- yielding "would restore the scope while this
coroutine is still parked". _in_dispatch_name_scope joins
_in_commit_scope in the same place.
Three details decide whether the guard holds, and all three are pinned:
it rejects before the park, not after; it rejects unconditionally
rather than only when the handle is incomplete, because a guard keyed
on whether the job happened to finish first passes under test and fails
intermittently in production; and it covers both yield points.
That last one is a finding. pmacs.async.yield_to_next_tick
(async.lua:243-245) also yields and is public. Guarding only await
would have left the hole open through a second door.
Which exposes Q#W-7: the existing commit_to guard has exactly that gap
today. yield_to_next_tick carries no _in_commit_scope refusal, so
Journey Stage 1a Q#JR14b invariant has a second entrance. Reported
rather than patched, and reachability by a real caller is explicitly
UNPROVEN --- it is a code reading, not a repro. My vote is to fix it in
this lane since the lane already edits that function family, but it is
another lane invariant so it is a question.
Q#W-5 justification is rewritten rather than left standing. It argued
parent was deferrable because its ambient would span asynchronous
lifetimes while this one did not --- an argument revision 2 was not
entitled to make, since its own ambient could be parked by any awaiting
handler. With rule 1 the distinction is real: this extent cannot be
suspended, and parent cannot be rescued by refusing to yield because
yielding is the mechanism it needs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two review blockers, both correct, both about the same failure: a field
or a claim that looks like attribution without being it.
BLOCKER 1 --- owner is not honest before P3. Revision 1 proposed
owner = package-or-builtin while populating it from static
per-subsystem constants at each dispatcher. Those disagree. A generic
dispatcher has no trustworthy knowledge of who invoked it, and
pmacs.process.spawn is callable by any package, so a static "lsp" label
is an origin or category and would confidently misattribute
third-party work to a builtin at exactly the point section 9 wants
attribution.
owner is removed rather than renamed. origin or subsystem would be
honest wording, but a second string field beside purpose, used to group
the view, gets adopted as ownership by the next reader regardless of
its name --- and it would squat on the slot P3 has to fill. Stage 2
needs a grouping key and should get a real one. No P3 alignment is
claimed any more.
BLOCKER 2 --- the handler name needs a mechanism, not a parameter.
Revision 1 said the name was "in hand at the one place that throws it
away". That was wrong about the call chain, and re-reading it is what
showed why: dispatch(name) calls an arbitrary handler, which calls a
Lua wrapper, which calls the Rust binding, and name is a parameter of
none of them. Worse, async.lua:337-345 documents the wrapper layer as
bypassable --- other runtime files are told to call their own raw
_dispatch_* primitives --- so capturing in the wrappers would miss
exactly the callers attribution exists for.
Q#W-2 is rewritten as a contract: a dispatch-name stack owned by the
async runtime and read at allocate, the same single funnel Q#W-1
collapses. Seven rules, including the two that decide whether it is
better than nothing --- unwind-safe popping, because one erroring
handler would otherwise poison every later dispatch with a stale name,
and composition rather than replacement of a caller-supplied purpose,
because replacing recreates blocker 1 in a new place.
It also answers the objection it invites: why is this ambient allowed
when Q#W-5 defers parent for needing one. Because they are different
mechanisms --- this is a synchronous single-threaded extent with a
deterministic pop, and parent needs a lifetime model spanning ticks and
post-settlement callbacks.
Verification takes the reviewer wording fix: presence is a type
obligation now that purpose is non-optional in a private JobSpec, so
the compiler proves it and the tests prove semantics at representative
entry paths. The handler witness must be a registered handler calling a
real dispatcher, not a synthetic funnel test.
The title changed too: "who asked for it" overclaimed once owner left.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
COHERENCE.md section 0 names background-work ownership as one of two
remaining thin ends of the golden journey, and section 20 puts it
outside Priority 1 while conceding it is the last of that priority own
work. Section 9 grades the worker model "mechanism without identity".
The felt gap is narrower than the arc and is checkable: grep -c for
spinner/progress/busy in src/statusline.rs returns 0, so section 3
promise of "visible asynchronous work" is false today unless the user
knows to run M-x editor.list-workers. The git Stage 1 lane in flight
right now records a deliberate negative section 9 impact for exactly
this reason; this lane is the one that repays it.
Two scouting findings shaped the staging rather than confirmed it.
PendingJob carries eight fields, not the seven the audit lists, and the
eighth doc comment cites section 9 by name as the reason identity
belongs on the job and not in a side map. So this extends a merged
decision instead of introducing one.
pmacs.process.list filters to LineOriented, dating to the vterm Stage 1
commit, and three acceptance suites use #pmacs.process.list() as a leak
detector. Widening that accessor to show terminal PTYs would inflate
all three baselines. Making PTYs visible therefore moves to Stage 2
behind a separate accessor, which is a better answer than editing tests
that are correctly detecting a semantic change.
NO WIRE CHANGE, and that is load-bearing for scheduling: discovery
Stage 2 holds the v22-to-v23 bump slot and git Stage 2 is queued behind
it. The activity indicator is a fourth pmacs.statusline.register
provider on the existing StatuslineSegments vector.
The framing also flags a deliberate deviation from the audit rather
than quietly taking it: section 9 names owner/purpose/parent together,
and Stage 1 takes only the first two, because an unpopulated parent
field reads as "no parent" rather than "not tracked".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
panel_commit_dedication_refusal matches on `fid` as well as on the
profile: a nested commit for a DIFFERENT frontend may dedicate that
frontend's own side slot, because resolve_placement consults only the
requesting frontend's panel_capable and its own one side window, so
nothing done to B can change where A's side request lands.
That promise was documented and unpinned. Both revision 9 nesting
tests drive a single frontend, so the comparison is trivially true
throughout them: deleting it, and making any outer "panel" contract
globally restrictive, passed the whole file.
a_nested_commit_for_another_frontend_may_dedicate_its_own_slot runs
two frontends. While an outer "panel" commit for A is in force, a
nested commit for B dedicates B's slot and is ALLOWED --- and B's
slot is asserted really dedicated afterwards, not merely unrefused.
The far side runs in the same test: A's slot stays undedicated and
A's result still lands in A's panel, so the row cannot pass by having
weakened the restriction generally.
This is the suite's only POSITIVE row; every other asserts a refusal,
which is the shape it was thinnest on. An exception only the doc
comment knows about is one review round from being simplified out.
Mutation-checked: deleting `&& contract.destination.frontend == fid`
fails ONLY this test. Both single-frontend nesting tests pass under
it, which is the evidence they are independent of the frontend match
rather than merely looking so. journey_acceptance (47),
dired_acceptance (31) and cargo test --lib (1920) stay green.
Two ledger corrections, both section-local:
* "Eight writes exist; five are reachable" then listed four. The
fifth is quit_window's QuitAction::Restore --- the site proved
unreachable and guarded anyway. It now appears in the list that
justifies it, and the bullet counts what actually matters: all five
are guarded.
* The revision 9 mutation paragraph had the preservation counts
REVERSED (journey 31 / dired 47). It is journey 47 / dired 31,
matching the bullet further up and measured per target. The same
reversal is in 394fa43's commit message; that is left as written
rather than rewriting a pushed commit, and the ledger now says so
where the numbers are, so a reader following the SHA takes the
corrected pair.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 8 refuses, inside a "panel" commit_to, the mutations that
would make its relaxed preflight wrong. A nested commit_to REPLACED
the enclosing contract with its own and restored it afterwards, so
the outer restriction went out of force for the whole inner body:
commit_to(outer, function() -- "panel", relaxed preflight
commit_to(inner, function() -- "document", MASKS the outer
set_params(panel(), { dedicated = true }) -- ...and succeeds
end)
display(result, { side = "bottom" }) -- ...which now FALLS BACK
end, "panel")
Every step is legal on its own, and the outer commit then overwrote
a newer document buffer --- the P1a failure the lane exists to
remove, reached through one extra call.
What this invalidated, precisely: NOT the enumeration of dedication
write sites. Every site in it is real and still guarded. What was
wrong was the claim that the guard was in force for the whole outer
body. So the enumeration is inherited and qualified, not redone.
Contracts now COMPOSE rather than replace. The core holds a stack;
ScopedFrontendGuard pushes on entry and truncates back to its own
depth on every exit path; panel_commit_dedication_refusal consults
every contract in force rather than the innermost. The strictest
active restriction wins. Matching stays per frontend --- a nested
commit for a different frontend may dedicate its own side slot,
which cannot change where this frontend's side request lands.
Nesting itself is NOT forbidden, which was the other candidate fix.
It closes the hole by prohibiting a construction no rule objects to:
commit_to is public Lua API for saying where a continuation's result
belongs, and a body committing to a second destination (a diff
beside a status panel) is where #227's adoption is heading. Only the
restriction needed preserving. Detecting the dedication when the
outer commit resumed was not available either --- that is a late
refusal, which is what revision 7 was rejected for.
Two pins, and they are a pair rather than one test written twice:
* a_nested_commit_cannot_mask_an_outer_panel_restriction drives the
same four write-site rows through a nested, entirely valid
"document" commit, and asserts the attempt is refused, the slot is
still undedicated, and the outer commit's destination is intact.
* an_ordinary_nested_commit_still_runs_and_restores_the_outer_restriction
pins that nesting without dedication is accepted, that the
enclosing restriction is back in force once the nested commit
returns, and that outside every commit dedication is ordinary
again.
Mutation-checked: restoring the guard to the innermost contract
(.last(), exactly revision 8's swapped slot) fails only the first of
those. The other 13 pins, journey_acceptance (31), dired_acceptance
(47) and cargo test --lib (1920) all stay green. The ordinary-nesting
pin deliberately survives it --- it exists to fail the other
candidate fix.
Also sweeps the comments left by revision 7, which revision 8
superseded: no fallback_commit_refusal symbol remains, but six doc
sites still described placement-boundary enforcement as the
guarantee (ViewDestination, CommitProfile::Panel, CommitContract,
capture_view_destination, commit_destination_refusal,
panel_placement_can_fall_back), plus two comment blocks in the
commit_to binding and one stale mutation note in the acceptance
suite. Net rustdoc warnings down three.
Framing to revision 9; the active-work lane entry updated in place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 8 of `docs/destination-capture-framing.md`, replacing the
revision-7 design at `ca72461`, plus the invalid-UTF-8 profile hole.
The framing now carries §3's enumeration, performed.
THE BLOCKER, unchanged. The `"panel"` commit profile skips preflight
checks 2-4 on the claim that a panel result never touches a document
window. Panel placement FALLS BACK into an ordinary document window
when the frontend is not `panel_capable` or its one side slot is
dedicated elsewhere, and installs the result there --- so a `"panel"`
commit could replace a NEWER document with every stale-intent guard
skipped.
TWO REJECTED SHAPES, kept in the framing as the record of why not
those. Revision 6 predicted the fallback at preflight and argued the
body could not change it; false, because refusing `await` stops another
COROUTINE interleaving, not the body's own synchronous statements.
Revision 7 (`ca72461`) moved enforcement to the placement boundary;
that breaks the invariant `commit_to` exists for --- `docs/agent-handoff.md`
requires it to preflight BEFORE the callback, because a body creates
buffers, handles and paint long before it asks to display anything, so
"validating at display time is four mutations too late". A refusal
arriving after all of that is a partial commit with an error return.
REVISION 8 DOES NEITHER. The preflight stays exactly where it was, and
the mutations that would invalidate it are REFUSED AT THE ATTEMPT ---
the same shape as `Handle:await` being refused inside a commit scope,
for the identical reason: something that would invalidate the scope's
guarantee is rejected outright rather than predicted around or caught
late. With them refused, the fallback never comes into existence.
THE ENUMERATION, PERFORMED --- this is the load-bearing part, and it is
closed for a structural reason rather than because inspection ran out
of ideas. Full working in the framing §3.
`resolve_placement` reaches `Ordinary` from a side request through
exactly two branches, so only two pieces of state are levers at all:
`panel_capable`, and the one side window's `dedicated`.
`panel_capable` is UNREACHABLE from a body: written only where a
`FrontendView` is constructed, and nothing in `src/lua_bindings/`
constructs, registers or unregisters one --- `register_frontend_view`
has callers only in `daemon.rs` and core unit tests.
`dedicated` has eight writes. Five are reachable: `apply_placement`'s
`Side` created, replacing and non-replacing arms, and `set_params`. Two
`Ordinary` arms are harmless --- every `Ordinary` target is filtered
`!is_side`, and one only ever clears the flag. One is a unit test.
Closing the side window is NOT a route, checked rather than assumed:
with no side leaf `side_window_for` returns `None` and placement
CREATES a fresh panel instead of falling back. `panel_hidden` is not
consulted by placement, and `params.side` is unreachable.
`quit_window`'s `QuitAction::Restore { dedicated: true }` is
UNREACHABLE, and this was the surprise --- it looked like a route with
no `dedicated` argument at the call site at all. `Restore` is stored
only on a REPLACING side placement, and a dedicated slot can never be
the target of one: a side request with a different buffer falls through
to `Ordinary`, and an exact-target request is refused by
`window_accepts_buffer`. Guarded anyway, labelled defensive, because
its unreachability is emergent from two rules in another function.
GUARDS SITED WHERE THE PROPERTY CONVERGES. All three `Side` arms are
reached through `apply_placement`, which has EXACTLY ONE caller --- so
one guard in `display_buffer` covers every request-driven dedication,
including spellings that do not exist yet. `set_params` is a genuinely
separate write and is guarded separately; dedication does NOT converge
before the field itself, and that is stated rather than papered over.
`Window::params.dedicated` is a public field, so the compiler does not
enforce the funnel --- the acceptance rows are what would catch a new
direct writer.
WHAT IS DELIBERATELY NOT REFUSED. The document profile is untouched:
constraining its body would newly refuse dired's own documented panel
path, a preservation-suite stop signal. Dedicating a DOCUMENT window is
still allowed, since it cannot change which of panel-or-document a side
request resolves to. And falling back is still allowed --- a frontend
that cannot render a panel degrades gracefully exactly as today,
because this refuses the mutation that MANUFACTURES a fallback, never
the fallback itself.
THE SECOND HOLE. `commit_profile` did `name.to_str()?`, but Lua strings
are BYTE strings, so `string.char(255)` hit mlua's generic UTF-8 error
before `BAD_COMMIT_PROFILE` was constructed --- the same reachability
class as the `Option<String>` defect revision 5 fixed, one layer down.
Bytes now, with the row asserting on message content.
TESTS: 12 pins. The inside-the-body test is ONE ROW PER REACHABLE WRITE
SITE, not per call spelling, because one spelling reaches three
different writes: `set_params`, and `display{side, dedicated}` in each
of the created, replacing and non-replacing arms. Each asserts the
three things revision 8 requires --- the dedication call is refused, the
slot is still undedicated afterwards, and nothing partial was installed
(no `*result*` buffer, panel unchanged, document unchanged).
Mutation-checked per guard: deleting the `display_buffer` guard fails
all three display rows, verified INDIVIDUALLY by rotating each to the
front so the first failure cannot mask the rest; deleting the
`set_params` guard fails only that row.
THREE FRAMING CORRECTIONS ride along, all of them cases of the document
teaching something it later argues against. Section 3 stated the
disproved premise unconditionally --- "the panel case would inherit a
check about a window it never touches" --- a hundred lines before
correcting it, so a reader met the wrong claim first; it is now
qualified at the point of the claim, and section 2 carried the same
unconditional form one section earlier ("it lands in the bottom panel")
and now says it REQUESTS one. The handoff citation was written "section
748" twice when it is LINE 748, and this document's authority is that
its citations can be followed. And the "not asserted exhaustive" hedge
on the route list is retired: the enumeration is closed structurally,
because `resolve_placement` reaches `Ordinary` from a side request
through exactly two branches.
`journey_acceptance` (47) and `dired_acceptance` (31) pass UNCHANGED.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Review found dedication is reachable by a second path. Beyond
set_params, a body can call display(buf, { side = "bottom", dedicated =
true }), which writes request.dedicated straight into the side window
at editor_core.rs:4535, then request a second panel buffer and cause
the fallback. An implementation guarding only the named set_params call
passes revision 8 test while keeping the original defect.
That is the important part, and it is worth more than the route itself:
the second route was found in review AFTER the first was specified,
which is the evidence that guarding one named call site is not a
design. The framing now requires every discovered route recorded and
given its OWN acceptance row, states that the two known routes are not
asserted exhaustive, and says finding a third is part of the work
rather than a later review job.
The ledger head still announced revision 7 as implemented and correct,
declared the blocker closed, and prescribed placement-boundary
enforcement --- the design review had just rejected. I corrected the
lower Q#DC-2 paragraph last round and left the authoritative block
alone, so recovery met the rejected design first and the correction
second. That is the same one-site correction failure this session keeps
reproducing, and this time in the file whose entire job is to be the
volatile state of record.
The head now names all three designs, which two were rejected and why,
and that the shipped code implements the rejected one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revisions 6 and 7 were both wrong, in opposite directions, and review
caught each.
Revision 6 predicted the fallback at preflight and argued the body
could not change it. False: the await refusal stops concurrent
interleaving, not the body, which is arbitrary synchronous Lua and can
dedicate the side slot itself.
Revision 7 moved enforcement to the placement boundary. That breaks the
invariant commit_to exists for. Handoff section 748 states it without
qualification --- it preflights every precondition BEFORE invoking the
callback, because dired mutates handle state, prev and paint long
before it reaches anything that could refuse, so validating at display
time is four mutations too late. A refusal that arrives after arbitrary
Lua has created buffers, handles and paint is not a refusal; it is a
partial commit with an error return.
So revision 8 does neither. It keeps the preflight where it is and
REFUSES the mutations that would invalidate it --- the same shape as
the await refusal already in this file, for the identical reason:
something that would invalidate the scope guarantee is rejected rather
than predicted around. Refusal stays mutation-free on the normal
(false, reason) path.
The mutation surface is narrow, which is what makes this tight rather
than aspirational. dedicated is writable from Lua and is one of only
two writable window fields per Q#BP2c; panel_capable has no Lua binding
at all, checked across src/lua_bindings. But the implementation must
ENUMERATE the body-reachable transitions rather than trust that list
--- closing the side window, or any other route to no usable side slot,
counts, and I have not proven those two exhaustive.
If the enumeration is open-ended, the named fallback is to collapse the
two profiles and always run all four checks. Safe, simple, honest, and
it makes the parameterization pointless --- which is why it is the
fallback and not the answer, and why choosing it needs its own
approval.
The inside-the-body test is strengthened accordingly. Revision 7 asked
it to assert that document B was not replaced, which passes on a design
that lets the body mutate freely and merely declines the final
installation. It now asserts the dedication call is refused, the slot
is still undedicated afterwards, and nothing partial was installed. The
refusal must land on the mutation, not on the outcome.
The ledger Q#DC-2 summary still repeated the disproved premise
verbatim, so a recovering reader met two incompatible answers in one
lane entry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 7 of `docs/destination-capture-framing.md`, closing the
correctness blocker review found in `0efc8c0` and the smaller
reachability hole beside it.
THE BLOCKER. The `"panel"` commit profile skipped preflight checks 2-4
on the claim that a panel result never touches a document window. That
claim is false: panel placement FALLS BACK to an ordinary document
window when the frontend is not `panel_capable` or its one side slot is
dedicated elsewhere -- `apply_placement` says so in its own comment --
and then installs the result there. So a `"panel"` commit could replace
a NEWER document with every stale-intent guard skipped: capture A, the
user opens B, the continuation lands, B is gone. That is the exact
failure `commit_to` exists to prevent, reached through the profile
meant to be the safe one.
WHY NOT A PREFLIGHT PREDICTION. Revision 6 proposed predicting the
fallback at preflight, arguing nothing could change in between because
the body cannot `await`. Refusing `await` prevents another COROUTINE
interleaving; it places no restriction on the body itself, which is
arbitrary Lua running synchronously and can invalidate the snapshot in
two statements -- take the panel, set it `dedicated`, then request a
side display. No preflight predicate closes that, however phrased: the
measurement is taken before the thing it measures is decided.
WHAT THIS DOES INSTEAD. `EditorCore::display_buffer` refuses between
`resolve_placement` and `apply_placement` when a side request resolved
to `PlacementKind::Ordinary` under an active `"panel"` contract whose
destination fails the document preconditions. That is the first moment
the fallback is a fact rather than a guess, and refusing before
`apply_placement` means a refused fallback mutates nothing. The
contract rides on the core, installed and restored by the same
`ScopedFrontendGuard` that scopes the frontend, so a profile can never
outlive the body that declared it; the field is crate-private, so Lua
cannot claim a profile for a placement it did not commit to.
The preflight predicate SURVIVES as an early refusal and not as the
guarantee. `panel_placement_can_fall_back` still gates the relaxation
in `commit_destination_refusal`, so the statically knowable case -- a
frontend that cannot render a panel at all, and will not acquire the
capability mid-body -- refuses before the body allocates a buffer,
registers a handle and paints. That is the same reason `commit_to`
preflights at all. Both layers are pinned, and neither pin subsumes the
other.
The four document checks now live once, in
`EditorCore::document_destination_refusal`: they are evaluated from two
sites, and two hand-written copies is how a backstop ends up weaker
than the thing it backs.
THREE DELIBERATE LIMITS, each a different decision rather than a
stricter version of this one. The document profile is untouched --
re-running its checks at placement would newly refuse dired's own
documented panel path, which is a preservation-suite stop signal. Only
a fallback is guarded, not every `Ordinary` placement -- a `"panel"`
body calling `display_file` is pinned as succeeding. And the refusal is
of the PLACEMENT, not of falling back: a `"panel"` commit with an
intact destination still degrades gracefully into the document window,
because turning graceful degradation into an error would regress every
consumer that works today on a frontend without panel capability.
THE SECOND HOLE. `commit_profile` did `name.to_str()?`, but Lua strings
are BYTE strings, so a `string.char(255)` profile hit mlua's generic
UTF-8 conversion error before `BAD_COMMIT_PROFILE` was ever
constructed -- the same reachability class as the `Option<String>`
defect revision 5 fixed, one layer down. The comparison is on bytes
now, and the invalid-UTF-8 row joins the number/table/boolean rows
asserting on message content.
FOUR DOC SITES repeated the false claim (`ViewDestination`'s own doc
twice, `capture_view_destination`, `ViewDestinationLua`) and are
corrected. Nothing else relied on it: dired, the only Lua `commit_to`
consumer, takes the two-argument document profile and already had all
four checks; `compile.lua`'s `already_in_panel` queries live state; and
the terminal adopter's rollback keys off `created_side`, already false
on a fallback.
Tests: 12 pins, up from 8. Three carry the enforcement split and none
subsumes another -- the pre-established fallback (both causes, the body
must not run), the inside-the-body transition (the body runs, the
result must not land), and the graceful fallback (a valid destination
still lands). Mutation-checked four ways; the pattern of which rows
survive each mutation is in `docs/active-work.md`.
`journey_acceptance` (47) and `dired_acceptance` (31) pass UNCHANGED.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Review found revision 6 fix unsound for the same reason its target was.
Revision 6 moved the panel/document decision to a preflight prediction,
arguing nothing could change before placement because commit_to refuses
await. That refusal stops CONCURRENT INTERLEAVING --- another coroutine
mutating state while this one is parked. It does nothing about the body
itself, which is arbitrary synchronous Lua and can obtain the panel,
set dedicated = true, and then request panel display. Preflight sees a
reusable panel and relaxes checks 2-4; the body causes the fallback;
the result replaces a stale document.
No preflight predicate closes this, however phrased --- the measurement
is taken before the thing it measures is decided. So enforcement moves
to the placement boundary, where resolving to Ordinary for a request
that asked for a side IS the fallback rather than a forecast of one.
The commit scope is already Rust-side app data, so the profile and the
destination can ride there.
The tempting non-fix is named so nobody reaches for it: widening the
predicate from "will it fall back" to "could it ever" is always true,
since the body can always dedicate the slot --- which collapses the two
profiles and buys nothing.
Section 7 gains the test that distinguishes the designs: the callback
dedicates the side slot MID-COMMIT. Both fallback tests revision 6
asked for establish their state before commit_to is entered, so a
preflight-snapshot design passes them. A design passing only those two
has not been shown to work.
The ledger claimed the lane implemented with eight pins covering
section 7. Those pins were written against revision 5 matrix, which
review disproved --- none exercises a fallback placement. A recovering
machine reading that entry would have prepared a PR from a lane with an
open correctness blocker.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Q#DC-2 gave the panel profile only check 1, on the stated claim that a
panel result never touches a document window. That claim is false, and
the tree says so in its own comment: reaching Ordinary while a side was
REQUESTED means the request fell back --- not panel-capable, or the one
side slot is dedicated elsewhere --- and the result is then installed
into an ordinary document window.
So a "panel" commit on a non-panel-capable frontend could replace a
NEWER document while skipping every stale-intent guard, reintroducing
exactly the failure this API exists to prevent. Reproduced in review,
not theorised. That makes it a correctness defect rather than a
strictness preference, and it is my framing error: I wrote the matrix.
The relaxation is now conditional on the placement really being a
panel. Both fallback causes are readable from core state at preflight,
and nothing can change between preflight and placement because
commit_to runs its body synchronously in a scope that refuses await ---
so the prediction cannot go stale under the commit it guards.
What is deliberately NOT the fix: refusing a panel commit that would
fall back. Falling back is existing, intentional behaviour for a
frame without panel capability, and refusing would turn a graceful
degradation into an error. The panel profile relaxes checks; it does
not get to change where things land.
Also closes an invalid-UTF-8 hole in the profile diagnostic. Lua
strings are byte strings, so string.char(255) reaches to_str() and
produces mlua generic conversion error before the documented message
naming the accepted values is ever constructed. Same reachability class
as revision 5 Option<String> defect, one layer further down --- which
is worth noticing, because I fixed that one and did not look for the
next one.
And the header said "Pre-implementation. Awaiting approval" through
revisions 2 to 5 while the ledger recorded the lane approved and
implemented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Section-local edit to the lane's own block; several lanes edit this file
concurrently.
Records the two commits, the green gate line (both preservation suites
unchanged), the mutation checks that show a matrix of deliberate
omissions is not passing vacuously, and the two places the framing did
not match the tree:
- the rename was 11 references across 5 files, not 8 across 4 —
`src/daemon.rs` also calls the capture;
- Q#DC-4's "frontend with no document window" is a DEFENSIVE branch.
Q#BP6 asserts a layout always keeps a non-side window, with a
`debug_assert!` in `non_side_target` that fires under `cargo test`, so
a registered frontend in a healthy editor always has a live document
window. The decision stands, but #227 should not expect to meet that
refusal.
Neither changed a decision, and both are recorded rather than quietly
absorbed: the framing says "counted, not estimated", and the next reader
will check.
`ViewDestination`'s own doc comment is corrected in the same commit,
because it repeated the framing's over-claim ("a frontend showing only a
side window") in the one place a reader would trust it, and
`capture_view_destination` now says how reachable its empty pair
actually is. Code, not only ledger, since the ledger is not what someone
reads when they wonder whether that branch can fire.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 4 API spec contradicted itself at the binding boundary. It
required profile: Option<String> AND a pointed error naming "document"
and "panel" when a non-string arrives. mlua rejects a number or table
during argument conversion, before the closure body runs, so that
message was unreachable: a caller passing 42 would have got mlua
generic conversion error instead.
This is the identical trap the existing binding already documents for
dest --- typed Value rather than AnyUserData specifically so the
message stays REACHABLE and names the rule --- and revision 4 quoted
that comment as its reasoning while repeating the mistake one argument
to the right.
The profile is now mlua::Value, validated in the body. Nil and absence
BOTH mean document, spelled out because a Lua caller threading an
optional variable produces nil rather than absence and a third
behaviour there would stay invisible until someone hit it. A
non-string is refused by the same message that names the accepted
values.
The verification bullet is now the guard on the type choice rather than
on the behaviour: the non-string refusal is asserted ON ITS CONTENT, so
retyping the argument to Option<String> later stops the assertion
matching rather than silently degrading the error a user sees.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revisions 2 and 3 said "the profile is declared at commit_to" and never
said how. That is not a detail. The binding accepts exactly (dest,
body) today, so without a specified form #227 has no stable API to
adopt against, and the promise that existing callers keep their
semantics was a hope rather than a contract.
Q#DC-5: commit_to(dest, body [, profile]). An optional TRAILING string,
typed Option<String>, so there is no arity sniffing and no
table-or-function dispatch on argument 2 --- the existing binding chose
Value over AnyUserData specifically to keep its error message reachable
and naming the rule, and a polymorphic second argument would undo that.
Trailing reads badly after a long inline closure, but that is not the
call shape in use: dired defines a named local commit at dired.lua:670
and calls commit_to(opts.dest, commit) at :717. Verified, not assumed.
Against a named body the trailing profile reads fine.
The value set is CLOSED --- document and panel, exactly Q#DC-2 two
profiles. A third is a decision, not a spelling.
Omitted means document, and that is the load-bearing part: every
existing two-argument call keeps all four preflight checks by
definition of the signature, so journey_acceptance passing untouched
follows from the API shape rather than from care.
An unrecognized profile is an ERROR naming the accepted values, not a
silent fallback to document. A fallback would hand a caller stricter or
looser checks than it asked for, which is the failure the whole
parameterization exists to prevent. Its witness asserts the legacy
two-argument form through a check the panel profile OMITS --- a
stale-buffer refusal --- because asserting merely that it does not
error would pass on a call silently downgraded to panel, which is the
regression that would quietly void Journey Stage 1a guarantees.
Git mapping settled here rather than rediscovered during adoption:
*git-status* takes panel, *git-diff* takes document.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Q#DC-4 contradicted Q#DC-2, and on the primary panel API. Q#DC-2
concluded a panel profile depends only on a live frontend, so it can
commit with no document window at all; Q#DC-4 still voted to return nil
in exactly that case and told git to fall back to ambient behaviour.
Those cannot both hold, and the fallback advice was independently
wrong: falling back to ambient IS the P1a bug this lane exists to
remove.
Decided rather than voted on, since it is the primary API. The
destination document pair is optional; capture_destination() is
profile-blind and argument-free, because making capture profile-aware
would force a caller to know at capture time what it will do at commit
time, which is the opposite of why capture exists. The profile is
declared at commit_to, where Q#DC-2 parameterization already lives, and
a document-profile commit with no document pair is refused alongside
the other four preflight refusals. Capture never returns nil while a
frame exists.
Section 4 outline and Q#DC-1 were updated to match rather than left to
disagree --- Q#DC-1 no-arguments answer is now load-bearing rather than
incidental, because no arguments is what keeps capture profile-blind.
The ledger gate line said "new suite plus dired". --acceptance is
repeatable, so it now carries the executable command including
journey_acceptance and dired_acceptance, both named as preservation
suites and a stop signal. A volatile ledger that understates required
coverage is how a recovering machine runs a weaker gate than the lane
agreed to.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Three review findings.
Q#DC-2 parameterization was incomplete. Revision 1 said only "skip the
stale-buffer check for a non-replacing continuation", but a panel
result does not depend on the captured document window at all: it does
not replace that window buffer (check 3), does not occupy it (check 4),
and does not need that specific window to exist (check 2). Retaining
any of the three can reject git.status for an unrelated document-window
change; dropping them without an explicit profile risks weakening
document replacement. The question now carries a four-row matrix with
two profiles, and check 1 --- the requesting frontend still has a
layout --- is the entire panel profile.
That has a consequence the framing now states rather than leaving to be
discovered: if the panel profile needs only the frontend, a frontend
with no document window can still host a panel, so Q#DC-4 return-nil
rule is right for the document profile and possibly wrong for the panel
one. Settled as part of answering Q#DC-2, not after it.
tests/journey_acceptance.rs joins dired as a named preservation suite
and stop signal. It carries 27 commit_to references across nine named
pins --- forged destination, scope-and-restore on normal return and on
raise, await refusal, delivery to the requesting frontend, the
declining-listener redirect guard, and two already named preservation_*
--- and Journey Stage 1a own framing treats it as a required gate. A
lane that generalizes its substrate does not get to relax that. The
stop signal now covers both suites: a suite edited to accommodate the
change under test has stopped being evidence.
The coherence-impact section was missing entirely. CLAUDE.md and
COHERENCE.md section 25 both require one for coherence-affecting work,
and this lane qualifies twice over --- new Lua API surface, and a
generalization of a Journey substrate. Section 16 is the section it
serves. Journey steps: none added, one protected. Islands, config
registry: none. Section 9: neutral, and stated precisely, because
knowing which frontend a result belongs to is NOT knowing who asked for
it --- that is the worker-identity arc and the two should not be
conflated just because both concern async continuations.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
PR #227 review found that git async completions surface in whichever
frame is active when git exits, and named the right mechanism:
commit_to exists for exactly this continuation boundary, built by
Journey Stage 1a Q#JR14 because the work settles a tick or more later,
by which time the ambient frontend, window and buffer may all name
something else.
The fix is not available to git, which is why this is a lane rather
than a line in #227. commit_to takes a DirectoryDestinationLua that is
nonconstructible from Lua by deliberate design, and the only site that
mints one is inside the path.open-directory listener dispatch, from a
pub(crate) capture. Any async Lua continuation that is not a directory
open has no way to say where its result belongs.
The captured data is already generic --- frontend, window, buffer, with
nothing directory-specific in it. Only the name and the capture site
are, and the rename is 8 references across 4 files, counted rather than
estimated.
The substantive question is Q#DC-2, and scouting is what surfaced it.
Git two continuations are different in kind. *git-status* goes to the
bottom panel, because listview.open resolves display with a "panel"
default. *git-diff* replaces a document window, deliberately, so the
status panel it was invoked from stays visible beside it. The
stale-intent check that commit_to preflight runs --- the window still
shows the captured buffer --- is right for the second and wrong for the
first: the panel never touches that window buffer, so refusing because
the user switched files there is a refusal with no relationship to what
the continuation does. One shape either over-refuses the panel case or
under-checks the document case, and the framing votes for a
parameterized preflight while holding that vote loosely.
No adopter in this lane. Git adoption is #227 work after this lands; a
prerequisite that also converts its first consumer makes the two
impossible to review separately.
Verification carries a stop signal rather than a target: if any
existing dired test needs editing, the generalization changed Journey
Stage 1a semantics and that is cause to stop, not to adjust a test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
PR #228 review found a correctness gap this lane made reachable. The
GPU dropdown derives its height, its visible window and its
selection-highlight offset from `rows.len()` — ONE logical row per
candidate — while a detail carrying a line break shapes into more
physical lines than that. One such row misaligns every row below it
and the highlight with it. The grid TUI has the same exposure from the
other side: it writes the description into a single-row suffix on the
minibuffer band.
## Why not reject CR/LF at registration
That was the obvious fix. It was implemented, measured, and abandoned
on evidence.
MCP tool registration renders a whole schema block into
`Command.description` — tool text, blank line, `Arguments:`, then one
line per argument (`tests/fixtures/pmacs-mcp-tools/init.lua:272`, a
`table.concat(lines, "\n")`, used at `:496`). And
`tests/m9_6_acceptance.rs:583-598` ASSERTS four of those lines. A
one-line guard in `CommandRegistry::define` fails 36 tests across
`m9_6` (19/25), `m9_7` (16/19) and `m9_8` (1/17), in both feature
configurations, and could only be made green by deleting a shipped
acceptance criterion.
So the one-line constraint goes where the constraint actually is: the
surfaces that have one row. `Command.description` stays free-form,
which it legitimately is.
## The change
`Command::description_first_line` clips to the first CR **or** LF — a
lone CR ends a line too, and an LF-only clip would pass a bare `\r`
straight through to the same surface. Both single-row consumers call
it: the semantic producer filling `MinibufferRow.detail`
(`src/semantic_render.rs`) and the TUI suffix (`src/editor.rs`). A
first line that is empty ships as `None` rather than `Some("")`, which
would draw trailing padding.
No ellipsis or truncation marker, matching the in-tree precedent and
the minibuffer's own width rule.
`describe-command` and `help.list-commands` are untouched and still
report every line. That is what makes this a rendering decision rather
than data loss, and it is asserted, not assumed.
## Precedent, already in this tree
The same MCP fixture clips a tool RESULT to its first line because
"a multi-line set_status would corrupt the row layout"
(`init.lua:277-285`), leaving width clipping to the frontend. Same
hazard class, same resolution.
## Verification
`src/command.rs`: a schema block registers AND clips, in all three
break forms; a single-line description is byte-identical after the
clip; an empty first line clips to empty.
`tests/discovery_stage2_acceptance.rs`: an MCP-shaped description
reaches the TUI band and the GPU row as one line, through the real
prompt path — with the full text still reachable via
`describe-command` asserted alongside, so a clip that deleted the
schema block everywhere would fail rather than pass.
`pmacs-gpu`: one physical shaped line per logical candidate row — the
geometry invariant the dropdown depends on.
Mutation-checked: neutering `first_line` to the identity fails all
four new break-handling tests
(`a_multi_line_description_registers_and_clips_to_its_first_line`,
`a_description_whose_first_line_is_empty_clips_to_empty`,
`a_multi_line_description_reaches_the_tui_band_as_one_line`,
`a_multi_line_description_reaches_the_gpu_row_as_one_physical_line`)
and leaves the two "did not tighten past purpose" tests green.
`Command.description`'s doc comment claimed "one-line", which the MCP
path openly violates. It now states the real contract and records why
a registration guard must not be re-proposed.
`m9_6`/`m9_7`/`m9_8` pass COMPLETELY UNTOUCHED, and are now named
gate suites so that stays on the record.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai