Three blocking and three major findings, all accepted, all verified in
the code before acceptance.
- The PTY errno proposal is withdrawn. `nix::unistd::tcgetpgrp` needs
`F: AsFd`; `MasterPty` exposes only `Option<RawFd>`; every route from
a raw fd to `AsFd` is unsafe and the crate forbids unsafe. The claim is
reduced to distinguishing the fallback without the errno.
- Revision 2 called `getpgid` ungated. It is not: the gate is a
block-level `feature!` opened 168 lines above the function, and rev 2
read the four lines immediately above it. Both calls are available
because nix's own `signal` feature depends on `process`, verified with
`cargo tree` and by compiling them. The lane now declares `process`
explicitly rather than inheriting it.
- Bet 4's `setsid` fixture was impossible: a `spec.group` child is
already a process-group leader and a group leader's `setsid` fails
with EPERM. Replaced with a `setpgid`-into-an-existing-group fixture
with a readiness handshake and explicit cleanup.
- "Recoverable" is withdrawn. `tick_reap_ledger` drops its entry on any
probe error — including the EPERM this occurrence proves reachable —
and discards the SIGKILL result while marking the entry killed. The
claim is now that escalation remains scheduled, and that still-silent
path is parked as its own lane.
- Revision 2 falsified the wrong sentence. Stage A's disjointness claim
was about the PTY path and remains true; what the occurrence falsifies
is the ledger's own EPERM comment.
- Failed SIGUSR1 and SIGTERM are disposition-identical, because every
failed kill returns before the fatal-signal branch. Acceptance 3 now
separates the failure-format comparison from a successful-call
disposition control.
- Bet 5 proposed a test that already exists at src/process.rs:2501. It is
cited as ground truth and retained, not invented.
No code changes. Framing only, awaiting review round 3.
Stage B of the lane that merged as PR #176. Stage A parked every
tolerance rule behind one condition — evidence — and the macOS failure on
PR #191 is the first occurrence carrying Stage A's self-describing
format, so the evidence now exists.
It does not support any parked rule, and per review round 1 it does not
support an identity claim either. This lane is evidence collection only:
no tolerance rule, no retargeting, no disposition change.
Round 1 raised two blocking and two major findings. All four are
accepted, and all four were verified against the code first:
- the landed acceptance at src/process.rs:2400 asserts the same pid three
times, so substituting leader_pid for tcgetpgrp would pass it;
- a numeric pgid cannot establish group identity, and no portable
mechanism can (pidfd covers processes, not groups; macOS has none);
- the PTY foreground-lookup fallback renders identically to a normal pipe
child, and portable-pty discards the tcgetpgrp errno before pmacs sees
it;
- the report never names the signal, though disposition differs between
fatal and non-fatal ones.
Revision 1 was written to a session scratchpad rather than a branch, so
it was not on githubsucks and review necessarily landed on Stage A's
merged document instead. That is recorded in the revision history: work
is portable only after it is committed and pushed, and this lane broke
that rule on its first step.
No code changes. Framing only, awaiting review round 2.
`docs/active-work.md` was the only conflicting file. #196 added the dired
Stage 2a lane at the position this branch had used to relabel the #188
framing lane header; the resolution keeps both, changing neither side's
wording.
`src/editor_core.rs` auto-merged. Both lanes touch it, so a clean
textual merge is not evidence of a clean semantic one — the gate suite
is re-run in full on the merged tree rather than inherited from the
pre-merge head.
Resolution verified for line loss in both directions: the resolved file
differs from `main` only by this branch's own authored edits, and
differs from this branch only by additions taken from `main`.
Review round 2, three findings.
setsid is util-linux, not coreutils, and the standard `cargo test --lib`
gate must not hard-fail on a tool the README does not declare -- a
minimal or BusyBox container would fail without ever testing pmacs. The
hard assert becomes skip-unless-armed via PMACS_REQUIRE_SETSID, which is
the pattern the silent-skip lane already established, so the test cannot
quietly report `ok` having never run where the tool is guaranteed. CI
arms it on Linux; README declares it. Both arms verified against a PATH
with setsid genuinely removed: unarmed skips with its message, armed
FAILS with the diagnostic.
The durable causal account was wrong, and this corrects it in the
framing, the handoff and the ledger. basedpyright's console script runs
bundled node through `subprocess.run` and WAITS
(nodejs_wheel/executable.py:50, verified in the installed 1.39.6). It
does not exit at spawn. What orphans node is pmacs: `shutdown()` SIGTERMs
the recorded pid -- the Python wrapper -- which dies without forwarding
the signal, leaving node at PPid 1 holding the pipes. The refutation was
already in hand: the initialize handshake succeeds, which a wrapper that
exited at spawn could not have done, and the PPid 1 observation was taken
after shutdown had killed it.
The fix is unaffected -- the deadlock and its bite are unchanged -- but
the parked follow-up changes target: not "tolerate servers that
self-orphan" but "stop orphaning them", i.e. signal the process group
rather than a wrapper pid that swallows the signal. Framing section 5 P2
restated.
Also corrects a stale CI-ordering claim: the handoff said pyright must
stay unarmed until the timeout lane lands, but #195 is this PR's base and
gave every job a timeout-minutes. The one live reason is that CI does not
install basedpyright at all. The ci.yml comment asserting the job has no
timeout-minutes was stale for the same reason and is rewritten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
The four findings were one shape — a failure that left state wrong and
told nobody — so the lane records them as one lesson rather than four
bugs: every one was a `pcall` or a discarded return value, and each
looked like defensive coding.
Also records the round-1 pin that passed with its own bug restored
(acceptance 53's attribution assertion was satisfied by the deleted
path's basename appearing elsewhere in the same message), the refreshed
gate numbers, and that `main` was re-measured after the round and had
not moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Bite-verifying the round-1 pins caught one of them passing with the bug
restored. `contains("only.txt")` was satisfied by the status message's
own `deleted only.txt:` prefix — the deleted path's basename — so
stripping the `buffer "…"` attribution changed nothing the assertion
could see.
Both halves now assert the buffer's OWN name, which for a path-backed
buffer is the full path and which only the attribution can produce.
Dropping either name — the refusal reason's or the kept-modified list's
— now fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
All four findings were the same shape: a failure that left state wrong
and told nobody.
**Delete refusals reach the user.** `reconcile_delete_and_fire` returned
`kept_modified` and `refused` and both production callers discarded
them, so a last-buffer refusal or the asynchronous modified-buffer race
left the file gone and the buffer still bound to it — and the next
`C-x C-s` recreates the deleted file. Reporting now happens inside the
shared seam, not at its call sites, for the same reason the
reconciliation does: a caller that has to remember to report is a caller
that will forget. The message names the buffers (capped, with a count
for the rest) and states the consequence, and it is written to
`EditorCore::status`, not `pmacs.error` — that channel is defined only
by a test stub, so a report there would be the same silence.
`reconcile_delete` now prefixes `kill_buffer`'s reason with the buffer
name, because "cannot kill the last remaining buffer" does not say which
buffer is now bound to a deleted path.
**The LSP subscribers stop swallowing their own failures.** Ignored
`pcall`s around `did_close`, `forget_uri`, `did_open` and overlay
re-rooting made the callback return successfully, so the
`all-must-succeed` logger had nothing to log — concretely, a stale server
made `forget_uri` raise while the callback carried on with the old
stores, routes and `documents` entry all live. A shared failure sink
attributes each step, reports on both channels, and raises **after** the
loop, so one unreachable server cannot leave every other attachment
unreconciled.
**`forget_uri` abandons requests through the established path.** It
purged `pending_routes` and `pending_external` but not the same ids
`send_request` put in `LspClient.pending`, and recorded nothing in
`cancelled_rids`. The per-rid work is extracted from
`drain_cancelled_externals` as `abandon_request` and reused, rather than
a second incomplete copy: route, client pending, cancelled record and
`$/cancelRequest` now happen together.
**Acceptance 35 is pinned.** With a plain delete the forbidden fallback
was unobservable — `find_or_open` raises out of `load_file` and the
`pcall` swallows it — so both assertions passed with the fallback
present. The plan now deletes the origin's file and recreates it, which
gives the fallback something to open and makes "restores nothing"
falsifiable. The corrected G1 explanation also reaches the production
comments, which still repeated the false `resolve_target_buffer::NotFound`
story.
New pins: acceptance 53 and 53b assert the status channel; a stale-server
row asserts attribution on both channels *and* that the healthy
attachment still reconciles; an `lsp.rs` unit test asserts the client-side
abandonment with an unrelated request as its control.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Round 2 facts for the lane-4 entry: the dash/bash divergence that
falsified the `<&0` form and how the positive control caught it, the
eleven-suite Bet 2 result, and the evidence that acc28 on macos/lua54 was
a flake -- a rerun of the same job on the identical head, not an
assumption.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
CI falsified rev 2 of the framing. The synthetic reproduction used
`sh -c 'cat <&0 & exit 0'`, and `<&0` does not defeat the POSIX rule it
was chosen to defeat: /dev/null is assigned to an asynchronous list's
stdin *before any explicit redirections*, so by the time `<&0` runs, fd 0
already IS /dev/null and the redirect duplicates it onto itself. bash
happens to skip the default when a stdin redirect is present; dash --
Ubuntu's /bin/sh, and CI's -- does not. It passed locally and failed on
three CI legs.
Control 2 caught it and named its own cause. That is the fourth vacuous
reproduction in this lane and the first found by a control rather than by
a reviewer -- which is the argument for the controls, so the lesson is
recorded that way in the handoff.
The reproduction now uses `setsid --fork cat`: it forks, the parent
exits, and the child inherits stdin/stdout/stderr untouched. No shell, no
asynchronous list, no /dev/null rule, no implementation variance.
setsid(1) presence is asserted rather than skipped -- a skip would
reintroduce the silent-green shape the arming lane removed.
The fix under test is unchanged. Bite re-verified by revert on the new
form: ok in 2.03s with `stdin.take()`, FAILED at 10.00s on the
recv_timeout without it, both controls passing first.
Also adds bottom_panel_stage1_acceptance to the framing's Bet 2 falsifier
list. It holds PTY-in-panel tests and its absence from rev 1 was a real
gap, not a judgement call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Integrated late, immediately before push, per the ledger-contention
rule. Records the measured base, the recovery command, the defect, the
reproduce-first diagnosis method, the full gate table with the
revert-verified bite, and what is deliberately parked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`RuntimeHandles::drop` joined its reader threads in the `Drop` body,
which runs before any field drops. The `ChildStdin` sink lives inside
`StdinWriter` in the `stdin` FIELD, so it could only be released after
the join returned -- and the join was waiting on readers blocked in
`read()` on pipes whose write ends the child still held, because the
child never received the stdin EOF that would have made it exit.
A closed cycle, entirely inside one function. Teardown hung forever.
This is the root cause of `m4_5_basedpyright_initializes_and_negotiates_
encoding` hanging indefinitely -- diagnosed with gdb stacks plus /proc fd
forensics on a wedged process, reproduced 5/5 deterministically. It also
explains why the hang looked intermittent and machine-local: a
shim-launched server orphans its real process (basedpyright's console
script spawns bundled `node` and exits, leaving it at `PPid 1`), so
nothing teardown signals can reach it, while a direct binary like clangd
or gopls is a genuine child whose pipes close on reap.
`spawn_reader`'s `cancel` flag does not help: it is consulted between
reads and around `send_timeout`, never while `read` is blocked. The
existing comment's premise -- "dropping the master closes the kernel pipe
and unblocks `read`" -- holds for a PTY master but not for pipe mode,
where `read` returns only once *every* write end closes.
The fix reuses `close_stdin`'s existing, already-idempotent mechanism at
the one site missing it. Reordering the struct's fields cannot work: a
type's `Drop::drop` body runs before all of its fields regardless of
declaration order.
Bounded claim: this delivers EOF, so it fixes children that drain stdin
to EOF -- which stdio language servers do. A child that ignores EOF, or
that stops draining while bytes are queued (the writer's `write_all` is
blocking), still wedges the join. Making the `read` itself cancellable
via the poll path already used by `spawn_group_reader` is the standing
deferral that covers those, and is deliberately not in this change.
Test: `teardown_closes_stdin_before_joining_readers`, in `--lib` so it
runs in the standard gate. It models the real shape with an orphaned
grandchild, and carries two positive controls, because this lane wrote
three reproductions that passed against the unfixed tree before one
bit. The `<&0` redirect is load-bearing: POSIX XCU 2.9.3 assigns
`/dev/null` to an asynchronous list's stdin when job control is off, so a
bare `cat &` exits immediately and proves nothing. Teardown runs on a
worker thread behind `recv_timeout` so a regression FAILS in 10s rather
than hanging -- a hanging test would reproduce the hazard being removed.
Bite verified by revert: with the fix `ok` in 2.03s; with the single
`stdin.take()` line commented out, FAILED at 10.00s on the timeout, both
controls having passed first.
Docs: framing doc added; handoff gains the drop-body-before-fields lesson
and the reproduction-needs-a-control generalization, and its section 3
caveat is corrected -- the desktop's basedpyright binary was never
broken. The `--skip basedpyright` gate entry stays for now; dropping it
is a separate proposal owed evidence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Rides this branch rather than a standalone ledger PR: with several PRs
open, a lane written on `main` for work that lands elsewhere
re-conflicts on every merge.
Records the measured merge-base as pasted output, what 2b and 2c still
owe so the split boundary is auditable, the two re-pinned m4 rows, the
one framing claim found wrong, the two bites that were vacuous as
specified and why, the gate numbers, and the §16 ownership warning
against starting Journey Stage 1b while this is open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`rd9` and `rd14` pinned #190's deliberate restraint on the
`apply_resource_op` delete arm: descendants stay orphaned, and only the
first of two duplicate path-bound buffers is reconciled. Both doc
comments gave the same reason — widening would have routed N buffers
through `remove_buffer_and_fire`, which is phase 2 without phase 1, so a
tree delete would have left up to N windows on removed ids.
`EditorCore::reconcile_delete` composes both phases, so that constraint
is discharged and the old assertions are no longer merely obsolete: an
orphaned buffer whose next `C-x C-s` recreates a file the user deleted
is the defect. Each row now asserts the new contract in BOTH directions
— the buffer is reconciled away, AND no window holds a removed id — so
neither an exact-path/first-match regression nor a widening that skips
phase 1 can pass. Each direction is bite-verified.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Running the bites found three that did not falsify anything.
Item 28's rename row cannot pin the walk's containment rule:
`reconcile_rename` calls `Path::strip_prefix` to rebuild a descendant's
tail, and that is component-aware too, so a string-prefix walk is
silently corrected a second time. Deletion has no such second guard —
the walk's verdict IS the kill list — so the row moves there, and a
string prefix now provably destroys a buffer on `foobar.txt` when
`foo/` is deleted.
Item 30's composition-order assertion was a tautology: the LSP attach
leaves `diagnostic` LAST in the stack, and moving the last element to
the end is a no-op, so a remove-and-re-push was indistinguishable from
an in-place mutation. The row now pushes one more overlay after it and
asserts that precondition explicitly.
Item 34 needed both a restructure and a correction. §5's G1 says a
stale captured path "materializes a phantom" via
`resolve_target_buffer`'s `NotFound` arm.
It does not: `pmacs.buffer.find_or_open` calls `file_io::load_file`
directly and maps the error, so a missing path RAISES, and the
`NotFound` arm belongs to `resolve_target_buffer`, which serves
`pmacs.window.display_file` and the startup target rather than this
binding. The real defect is smaller and still real — the `pcall`
swallows the raise and the user is stranded wherever the last applied
op left them — so the plan now edits another file first, which is what
makes the restore observable at all. The correction is recorded at the
test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`tests/resource_reconciliation_acceptance.rs`, 23 rows, no dired
content — items 23–37 and 50–55 driven through the real entry points:
`pmacs.fs.rename` / `pmacs.fs.remove` fire-and-forget for the drain
harvest, `pmacs.buffer.apply_resource_op` for the synchronous arm, and
the fake server's `workspace/applyEdit` for the applier.
The rows that took design rather than transcription:
Item 27 opens two descendants AND two buffers on one exact path, since
one child would not defeat a first-match lookup. Item 29 tests name
provenance in both directions, including a name explicitly set to a
string that normalizes to the file's own path — the case a
path-equivalence heuristic gets wrong. Item 30 paints a real frame and
counts diagnostic underlines per window rect, because
`DiagnosticView.uri` is private and a store assertion would prove
nothing about re-rooting; it also pins each overlay's index in the
composition order, which is what a remove-and-re-push breaks. Item 53b
states its three assertions individually, since a compound check can
pass on two of the three.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`src/lsp.rs` gains nine: the fourteen-family store inventory with a
17-entry precondition so it cannot pass vacuously, the route purge with
`workspace/symbol` and another server's route both surviving, the
awaiter drain joined on the rid, the error contract's two arms, the
late-publish drop with its does-not-over-reach companion, the
`mark_document_stale` gate across all three stale stores, exact-pair
tombstone identity, and reclamation under both `start_generation` and
terminal `forget`.
`src/diag.rs` pins that `forget` drops the epoch while `clear`
deliberately bumps it — the leak a `clear`-based forget would leave in
the one map nothing prunes.
`src/async_runtime.rs` injects two resource replies onto the private bus
in each order and asserts `TickOutcome.resources` reports arrival order,
not allocation order; plus that a failed or cancelled mutation is not
harvested at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`resource.renamed` and `resource.deleted` are declared
`all-must-succeed`, so one raising subscriber does not stop the rest
from reconciling.
`lsp.lua` gains the two subscribers. Rename runs the ordered teardown
per attachment — flush the pending didChange, didClose the old URI,
`forget_uri` against the OLD server, re-run `ensure_server` (a rename
across project roots needs a different one), didOpen the new URI, then
re-root the diagnostic overlays. Delete tears the attachment down,
because the buffer may be gone entirely and a retained record is a
dangling handle.
The workspace-edit applier captures the origin BUFFER instead of its
path, and restores nothing when that buffer is gone. A captured Lua
local is unreachable to any transaction, and the old path fallback is
what materialized a phantom empty buffer at the renamed-away path.
`fs.lua` states the overlapping-mutation serialization precondition as
a correctness rule, with the counterexample showing why no static
ordering rule substitutes for it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
One shared walk query (`buffers_bound_under`), lifted out of #190's
`delete_verdict` so the guard and both reconciliation seams cannot
disagree about which buffers an operation touches: every buffer, both
sides normalized, component-aware containment.
`EditorCore::reconcile_rename` moves the stored path and — only for a
`PathDerived` name — the buffer name. `EditorCore::reconcile_delete`
composes the same two removal phases `pmacs.buffer.kill` composes,
preflighting `editing_in_progress` because a `ConcurrentEdit` refusal
arrives after `kill_buffer` has already moved windows. Phase 2 stays
with the caller; `EditorCore` gains no Lua handle.
`AsyncRuntime::tick` now returns a `TickOutcome` carrying the settled
ids plus the successful resource mutations, in bus-arrival order, which
is documented as not being execution order. `PendingJob.resource`
retains the paths the dispatchers move into the worker closure.
`LspManager::forget_uri` purges the routes carrying a URI, drains the
awaiters joined to them on the rid, and clears all fourteen stores plus
`documents`. A generation-scoped exact-pair tombstone gates the two
uncorrelated writers that can otherwise resurrect what it cleared:
`publishDiagnostics` and `mark_document_stale`, which now takes a
server id. `ResponseRoute::scoped_uri` is the one variant list, with
`uri()` delegating to it.
New Lua surface: `pmacs.buffer.set_name`, `pmacs.lsp.forget_uri`,
`pmacs.diag._rename_resource`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`BufferNameOrigin` records where a buffer's name came from instead of
inferring it from the string: a path-backed buffer's name is the path
*as given*, so a relative open is named `foo.rs` while its stored path
is absolute, and a user may legitimately choose a name that normalizes
to its own file's path. Rename reconciliation asks the bit.
Every path-backed creation site is audited onto the new
`set_path_derived_name` door: `EditorCore::get_or_load_buffer`, the
`NotFound` arm of `resolve_target_buffer`, `pmacs.buffer.from_file`,
and `pmacs.buffer.find_or_open`. Ordinary `Buffer::set_name` records
`Explicit`.
`View::rename_resource` is the seam that re-roots a URI-keyed overlay
in place, so it keeps its position in the window's composition order;
`DiagnosticView` overrides it, whose `uri` is private and set once at
construction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
One conflict, `docs/active-work.md`, with three strands rather than the
usual one: main gained #190's lane, this branch carries its own Stage 1
lane and a relabel of the #188 framing lane, and main had removed the
documentation lane while this branch still had it.
Resolved by construction. Main's file taken whole; this branch's Stage
1 lane reinserted at its own position ahead of the bottom-panel lane;
this branch's relabelled framing lane ("MERGED AS PR #188") kept in
place of main's stale "OPEN, PROPOSED" version; main's removal of the
documentation lane preserved.
Verified against both parents rather than by inspection: the Stage 1
block is byte-identical to this branch's, the documentation lane is
gone, no conflict markers survive, and the update-protocol rule 6 seam
check finds no double blanks.
Note for whoever absorbs next: main now carries three lanes describing
merged PRs (#190, #188, #194). This merge keeps this branch's more
accurate labelling of the #188 one but does not remove any of them ---
rule 4 permits removal only once durable facts reach
`docs/agent-handoff.md`, and none of those three PRs touched it.
Pure insertion: main's ledger taken whole with this lane's 3a block
placed before the parked lane. Verified against both parents --- zero
lines removed relative to main, the lane-3a block byte-identical to
this branch's, no conflict markers, and the update-protocol rule 6 seam
check clean.
Deliberately NOT absorbed here, because absorption is not mechanical
and this PR is approved on its current content. `main` now carries
THREE lanes describing merged PRs: the resource-op delete guard
(#190), the generated-buffer immutability framing (#188), and this
arc's own silent-skip arming (#194). Rule 4 forbids relabelling any of
them and permits removal only once their durable facts reach
`docs/agent-handoff.md` --- and none of the three PRs touched that
file, so all three absorptions are genuinely owed rather than
overlooked.
Two belong to other arcs. The third (#194) belongs to this one, and
its durable fact is not yet written down anywhere: that
`PMACS_REQUIRE_*` arms an otherwise-vacuous skip, and that
basedpyright stays unarmed until the reader-join hang and the CI
timeouts both land. That wants a handoff bullet, which is content
rather than a merge resolution.
P3 --- required status checks are name-coupled to job names, and a
required context that no longer exists does NOT fail. It leaves every
PR pinned on "Expected --- waiting for status" forever, which is `main`
becoming unmergeable by policy rather than by a red run. Three of this
lane's own deferrals will do exactly that: the macOS matrix trim
removes two contexts outright, and nextest or the serial/parallel split
rename or add jobs.
The rule is now in the ledger entry --- any job rename, removal, or
matrix change updates the branch-protection required-checks list in the
same motion --- and it is recorded HERE deliberately, because this is
the single entry that both enabled protection and named the lanes that
will invalidate it. Arming the warning anywhere else would separate the
trap from the thing that sets it.
P4 --- the rewritten top comment said "everything else keeps 25 against
a sub-4-minute observed max" and dropped the clause noting that
`m6-perf-gates` keeps its own tighter 15. Restored. Worth the fixup in
a change whose entire subject was comments matching reality.
Beyond the PR, and taken here rather than deferred: `TEST_IMPROVEMENT.md`
on `main` still said "no branch protection on `main` (verified via API:
404, so every job is advisory)" and listed §5.1 as open. Both went
stale during this session, and THIS lane is what made them stale, so it
carries the correction rather than leaving it for whoever touches the
file next. Struck through in both places rather than rewritten: the 404
was a true reading at audit time, and the document is the arc's scoping
record, so what changed is more useful than a clean-looking present
tense. Note also that protection shipped wider than §5.1 proposed ---
all 12 contexts required, not the cheap-jobs-only starter --- which the
correction states.
Verified: YAML parses; the seam check from update-protocol rule 6 finds
no double blanks; `git diff --check` clean.
P1 --- the ceiling was justified against the wrong number. Revision 1
cited "~14.6 min, ample headroom", which was one reading quoted as a
property, and this ledger's own rule applies to it: a census is a
reading, not a constant. Re-measured over two windows --- 17 min max
over 25 runs, 15.8 over 12, both macOS/luajit, every other job under
4 --- so a flat 25 was about 1.5x the observed tail, not "ample".
Two facts shape the fix. `timeout-minutes` counts EXECUTION, not queue,
so the 33-minute wall-clock run in that window executed its longest job
in 17 and no run in observed history would have been killed by either
value. And the real exposure is the case no window contains: a cold
cache. A stable-toolchain bump invalidates Swatinem's key on every leg
at once, and a cold macOS debug build plus suite is the plausible way a
HEALTHY run overruns --- presenting as four legs timing out
simultaneously the day after a Rust release.
So the test job takes 35 (~2x its observed max) and the rest keep 25
(~6x theirs), and the diagnosis is written into the workflow BEFORE the
event: simultaneous four-leg timeouts after a toolchain release are a
cold cache, not a hang; a single leg timing out beside passing siblings
is the hang case these ceilings exist to catch. 35 still beats the
360-minute default by an order of magnitude, so the basedpyright
arming this gate unblocks is unaffected.
P2 --- §5.1 was missing from both lists, and review was right that the
omission matters. But its premise had gone stale, which is worth
recording rather than quietly working around: branch protection is ON.
It was enabled earlier in this session, and I re-verified against the
API rather than trusting either the review or my own memory of doing
it:
{"enforce_admins":false,"force_push":false,
"required_checks":12,"strict":false}
Recorded in the ledger as DONE with the settings and the reasoning for
each --- `strict` off so a PR need not rebase every time `main` moves,
`enforce_admins` off so the user keeps an override. This also settles
the concurrency comment, which justifies exempting `main` pushes by
appeal to "the branch-protection record": that record exists, so the
justification is real rather than aspirational, and no softening is
needed.
P3 --- the double blank line before the parked lane, third PR running.
Fixed, and added to the ledger's own update protocol as step 6, since
fixing the instance three times has not stopped it: a block ending in a
blank line inserted above a heading already preceded by one leaves the
seam, and it survives review by sitting beneath the level anyone reads
at. The rule now names the check.
Verified: YAML parses; ceilings are 25 except test at 35 and
m6-perf-gates at its tighter 15; the seam check finds no double blanks
anywhere in the ledger; `git diff --check` clean. Workflow and ledger
only.
Keep both active-work lanes while taking the silent-skip arming and
generated-buffer framing changes from current main. The resource-op lane
retains its round-2 fixes and updates its recorded merge-base.
Record the path-normalization and partial-first-operation fixes, their
acceptance criteria and bite pre-images, and the green round-2 gate
results in the active-work ledger.
Lane 3a of the testing arc --- the three cheap, deterministic items of
`TEST_IMPROVEMENT.md` §5-6. The larger ones (nextest, the
serial/parallel split, a parallel canary leg, the nightly cron, the
macOS matrix trim) are deliberately NOT here: each changes what CI
certifies or how it runs, and each deserves its own decision rather
than riding in on a timeout patch.
`timeout-minutes` on every job (§5.2). Measured before changing rather
than assumed: SEVEN of eight jobs had none and inherited GitHub's
360-minute default; only `m6-perf-gates` had one, at 15. So a single
hung test burnt six hours --- times four on the test matrix --- and
reported nothing useful at the end of it. Set to 25 against a measured
~14.6 min critical path (macOS/luajit), which leaves ample headroom for
a slow runner while catching a hang in under half an hour.
This is the gate that has to exist before `PMACS_REQUIRE_PYRIGHT` can
ever be set. Lane 2 left basedpyright unarmed *because* this did not
exist; the two decisions are the same decision, half a lane apart.
`concurrency` with `cancel-in-progress` (§6.1), scoped to pull
requests. This project rebases heavily --- the ledger re-conflicts on
nearly every merge --- so branches take several pushes while earlier
runs are still going, and macOS minutes are both the expensive ones and
the critical path. Pushes to `main` are deliberately exempt:
`github.event.pull_request.number` is empty there, so the fallback keys
those runs by SHA and none can cancel another. Cancelling a `main` run
would leave the branch-protection record ambiguous about a commit that
has already landed, which is the one place the saving is not worth
having.
`-p pmacs-protocol` clippy (§5.7). The root-package clippy never
covered it --- the workspace default member is only `pmacs` --- so a
warning introduced through a protocol-only change would reach `main`
unseen. Verified passing locally BEFORE proposing it, so it cannot turn
CI red on arrival.
The timeout rationale is stated once above the job list rather than
copied onto each job: the first draft duplicated a seven-line comment
across seven jobs, which is the same degraded-copy shape this arc keeps
removing elsewhere.
Verified: YAML parses; all eight jobs carry a timeout (seven at 25,
m6-perf-gates keeping its tighter 15); `cargo fmt --all --check`,
`clippy -p pmacs-protocol` and `clippy -p pmacs-gpu` all exit 0;
`git diff --check` clean. The diff touches `ci.yml` and the ledger and
nothing else, so no code gate is affected.
Normalize batch dependency paths through the registry's lexical
canonical form so equivalent URI spellings do not revive the
initial-state preflight bug.
Separate execution-started state from the count of completed plan
items. Preflight failures retain the no-mutation guarantee, while
runtime failures conservatively acknowledge that the failing item may
itself have changed a buffer or the filesystem.
Add real-server-pump acceptance for dot-path dependency aliases,
partial text edits within one item, and resource-operation side
effects, and record the review-round corrections in the framing.
One conflict, in `docs/active-work.md`, with an extra strand: main
gained #188's lane while this branch had removed the documentation
lane, so the two sides disagreed about a region neither had edited
against the other.
Resolved by construction rather than by editing markers --- main's file
taken whole, the documentation lane removed, this lane's block
inserted before the parked lane. Verified against both parents: exactly
26 lines removed relative to main, which is the documentation lane and
nothing else, and the lane-2 block byte-identical to this branch's.
Records, without fixing, that #188's own lane now reads "OPEN,
PROPOSED" on a merged PR. Rule 4 forbids relabelling and allows removal
only once the facts reach `docs/agent-handoff.md`, which #188 did not
touch. That absorption belongs to the immutability arc's next PR, not
to a testing lane reaching across into it.
Bring the approved generated-buffer immutability framing onto the Stage
1 branch and update both active-work lanes to the landed #188 state.
Main @ 27b1185 changes documentation only relative to the prior base.
Record the revision-7 selection and acceptance reconciliation, the
exact code checkpoint, the non-vacuous fan-out bite, and the final gate
results. Keep PR 188's proposed status and merge ordering explicit.
Adopt Q#GB6's clamp-or-clear rule in both window-coordinate
normalization paths. Preserve shortened selections, clear only those
collapsed by a moved endpoint, and pin both outcomes through the real
generated-write and view-rebuild callers.
Make listview refresh rely on the generated-write notification before
reseating, so Stage 1 criterion 7's fan-out mutation bites both
adopters. Align criteria 5, 11, and 12 with framing revision 7.
Review round 2 found no new defects; this is the one durable item it
asked to be carried further than a commit message.
`cmd | tail -2` returns TAIL's exit status, not `cmd`'s, in fish and
bash alike. So a gate chain of `cargo test ... | tail -2 && ... && echo
"ALL GATES CLEAN"` prints the clean line even when a suite failed, and
that is what happened while gating this lane: a `pmacs-gpu` failure was
summarized as clean. The point worth keeping is that this is not
carelessness a closer read would catch --- the verdict is structurally
absent from the summary the PR then cites. §5 now says to check
`$pipestatus[1]`, or better to redirect each gate to a file and read it
afterwards, which also preserves the full log that section already asks
for. Filed beside the skip-reports-`ok` lesson, which is the same
family: the thing that summarizes a gate must not be able to lose the
gate's verdict.
Also fixes the doc-comment splice in `tests/support/mod.rs`, where the
why-two-directories paragraph landed mid-sentence and left the
include-mechanics explanation stranded inside it. Cosmetic, and review
called it not worth a round on its own --- folded in here because the
file was being touched anyway.
P1 --- the workflow contradicted this lane's own claim. `components:
rust-analyzer` rode the shared `dtolnay/rust-toolchain` step, which
matrixes over ubuntu AND macos, so the binary would have been present
on both. PRESENCE, not PMACS_REQUIRE_LSP, is what decides whether a
gated test body runs --- the unset variables on macOS only meant
absence would be tolerated there, not that the tests stay skipped. Two
rust-analyzer tests would therefore have executed on macOS for the
first time ever, on the legs that are simultaneously the CI critical
path and the documented flake surface, one of them doing real indexing,
none of it covered by the Linux-only local runs behind this lane. Moved
into the Linux-gated step so the text and the workflow agree.
P2 --- the ledger promised a base and pasted nothing. Worse than the
review knew: a script was run to substitute it, reported success,
matched no text, and the result was never re-read. The claim shipped on
the strength of a tool's exit status. The entry now carries the pasted
base and a recovery command, and records the lesson in the terms this
ledger keeps relearning --- asserting a measurement is not making one,
and a tool reporting success is not the measurement either.
P3 --- tool versions pinned (gopls v0.16.2,
vscode-langservers-extracted 4.10.0, yaml-language-server 1.15.0), so
CI no longer drifts with upstream publishes and a break has a commit
here to bisect against. `tests/support/` now states why it exists
beside `tests/common/`: the latter re-exports daemon and PTY
machinery, and pulling that into three binaries that spawn neither to
reach a six-line environment check is the wrong trade. Recorded as a
cost, with the rule that a third such directory means consolidating
rather than continuing.
Also recorded, because §1.2 is NOT fully closed by this lane: the
guards arm the ENTRY skip only, and m4_acceptance's mid-test
"workspace likely still indexing; skipping" survives --- so even armed,
that test's one assertion can still vanish under load, which is exactly
when a regression would show. Mid-test skips want their own pass. And
the follow-up this lane creates: removing the documentation lane
removes the only pointer to githubsucks/handoff-2026-07-20, so that
branch needs deleting after merge or nothing will ever mention it
again.
P4 --- the consolidated lua skip message names PMACS_TEST_LUAJIT again;
the `or_else(locate_shell("luajit"))` path still honours it and the
escape hatch had become undiscoverable. Double blank line before the
parked lane collapsed.
One gate note worth carrying rather than burying. A `PMACS_REQUIRE_GPU=1
cargo test -p pmacs-gpu` run failed once during this round, naming the
`--bin pmacs-gpu` target, and my gate chain still printed a clean
summary because `cmd | tail -2` in an `&&` chain returns TAIL's exit
status, not cargo's. Four subsequent runs pass 202/202 and the failure
has not reproduced, so it is recorded as observed-and-unreproduced
rather than explained. The masking is the durable part: a gate summary
assembled through a pipe can report success over a failure.
Gates re-run after the fix: fmt; clippy -D warnings; --lib 1863; --lib
--features crdt 2048; m4_acceptance 121; m6_5 11; m6_8 8; required GPU
202 (x4); git diff --check clean.
Absorb #193 without rewriting the reviewed revision 6 or revision 7
commits. Resolve docs/active-work.md by retaining PR #188's proposed
lane and accepting main's removal of the landed bite lane.
Record that the intervening base change touches no product source, so
revision 7's source readings remain current.
Lane 2 of the testing arc (`TEST_IMPROVEMENT.md` §1.2, §5.4).
The shape being fixed reports GREEN when the tool is missing:
let Ok(_) = which_binary("gopls") else {
eprintln!("gopls not on PATH; skipping");
return;
};
CI installed none of these tools, so a block of real-language-server
and multi-shell tests had never once executed their bodies while
reporting success on every run. A suite that cannot distinguish
"passed" from "never ran" is worse than a missing suite, because it
reads as coverage in exactly the place someone would go looking for it.
The fix is this project's own pattern rather than a new one:
PMACS_REQUIRE_GPU already turns a missing adapter into a hard failure
for the headless render job. This adds PMACS_REQUIRE_LSP,
PMACS_REQUIRE_SHELLS and PMACS_REQUIRE_LUA, and the CI step that
installs the tools they promise. Per-tool variables rather than one
blanket flag, so a tool that must stay unarmed keeps that decision
visible at the call site instead of buried in a workflow file.
basedpyright is deliberately NOT installed and NOT armed. Its test has
no timeout and hangs forever; the root cause is the non-interruptible
reader-thread join in `RuntimeHandles::drop`, already a named deferral
in `src/process.rs`, and the `test` job has no `timeout-minutes`.
Arming it today would trade a vacuous green for a six-hour hang across
four legs. PMACS_REQUIRE_PYRIGHT exists and is never set, so the flip
is one line after the hang fix and the CI timeouts land.
A trap found while writing the workflow rather than after: the natural
Actions idiom
PMACS_REQUIRE_LSP: ${{ runner.os == 'Linux' && '1' || '' }}
sets the variable to the EMPTY STRING on every other platform, and
`var_os(..).is_some()` is true for `Some("")`. That would have armed
the guard on precisely the runners with none of the tools installed
and failed every one of them. The helper treats empty as unset, which
makes the common spelling safe instead of subtly wrong.
The helper is SHARED via `#[path = "support/mod.rs"]` rather than
copied into three test binaries. `m6_8_multi_repl_acceptance.rs`
carried a comment saying cross-test-binary sharing "would need a
fixture crate"; it does not, and a correct helper in one file beside a
degraded copy in another is this suite's most repeated defect.
Verified by execution in all three states, using a tool genuinely
absent from this machine (vscode-json-language-server): unset skips
green; armed fails hard, naming the CI step that should have installed
it; empty string skips green. On `main` the armed state cannot fail at
all, because no guard exists.
And the question none of this could answer until now --- whether the
tests pass when they actually run --- is answered: armed locally, 11
m6_5 and 8 m6_8 REPL tests are green, and all six real-LSP tests
(clangd x2, gopls x2, rust-analyzer x2) pass individually. The coverage
was real the whole time. It just never ran.
Linux only for now, deliberately: macOS needs the brew equivalents and
roughly doubles install cost on the slowest matrix leg. The variables
stay unset there, so those tests skip cleanly as before.
Also removes the documentation lane from the ledger. Its disposition
was left undecided pending confirmation that its branch carried
nothing unique; measured, `githubsucks/handoff-2026-07-20` is 1 ahead
and 365 behind, and its whole unique diff is four doc files at 42
insertions against 88 deletions --- merging it would REVERT current
documentation. The section asked whoever confirmed that to remove it.
Gates: fmt; clippy -D warnings; --lib 1863; --lib --features crdt
2048; m4_acceptance 121 (unarmed, per CLAUDE.md); m6_5 11; m6_8 8;
PMACS_REQUIRE_GPU=1 -p pmacs-gpu 202; git diff --check clean.
The entry pasted `4 0`, measured immediately before the commit that
wrote it; at the pushed head the same command said `5 0`. An
ahead-count is self-referential in the file that carries it, so the
entry now pastes only the merge-base — which is stable — and says to
run the count on demand. Behind-count stays the thing that matters,
and the merge-base tells you whether it can still be zero.
Close review round 6 by moving CRDT quarantine to common divergence
detection, guarding every snapshot and later owner-write route, and
making the acceptance pre-image classifications honest.
Advance the active-work lane to the actual integration checkpoint and
replace the obsolete delete-to-insert recovery rule.
The count in the previous commit was measured against `64883eb` and
was stale the moment that merge landed. Re-measured and pasted as
command output. `main` moved twice during this round; the entry now
says so, and says why a stale base matters under branch protection.
Resolution is a deletion, not a rewrite: `main` REMOVED the
`scripts/bite` lane entry when #192 landed, so this side drops it too
rather than resurrecting a merged lane. This lane keeps its own entry;
its pasted position measurement is re-taken in the next commit,
because the merge itself moves it.
The previous commit pasted `git rev-parse`/`git log`/`merge-base` output
showing `main` at `64883eb` and "main IS integrated". #193 merged between
writing that and pushing it, so the pasted output was false in the tree
that carried it --- and "main is integrated" is exactly the kind of claim
this review round was about: the tree asserting something the record does
not support.
Re-measured and re-integrated, and the bullet now says the thing the
first attempt got wrong. Pasting command output is necessary and NOT
sufficient: a base measured before the last push is a claim about a tree
nobody has, so it has to be re-measured at push time and treated as
expired on sight. `main` moved three times inside this one lane
(#187 -> #192 -> #193).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T