Two blocking, two cleanups. All four accepted, and both blockers
verified by running Lua rather than by reading it.
Recovery was inconsistent across buffers sharing an affinity. Revision
2 cleared `failures[K]` on a successful spawn but cleared only the
succeeding buffer's projection, so: buffer A fails, buffer B succeeds
for the same key, `M-x lsp.status` reports nothing wrong, and A's
modeline still reads `LSP:!`. Revision 2's claim that the two tables
are "written and cleared at the same moment" was false for exactly the
cross-buffer case, which is the normal case for a project with more
than one file. Each projection now carries its affinity key and a
success sweeps every projection holding it. The new pin asserts on A,
not B — a version checking B passes on the broken implementation.
The markerless key had no Lua representation. `key_uri` is deliberately
nil and `t[nil] = v` raises "table index is nil" — confirmed under both
LuaJIT and Lua 5.4 — so the central markerless criterion was literally
unimplementable as written, and leaving it to implementation would have
produced two ad-hoc encodings for the two tables. The framing now
prescribes one key function used by both, with a `u`/`n` discriminator
that cannot collide with any URI and a NUL separator (Lua strings are
8-bit clean, checked).
Acceptance 10 could not have observed what it claimed: making the
command resolvable changes no state by itself, since `failures` is
cleared by a successful spawn. The pin now reattaches before pressing
`g`, and says why that step is load-bearing.
Also corrects the ledger heading, which still said revision 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Two blocking, three major, one minor. All six accepted and verified in
the code before the doc changed.
The affinity key was misstated. `ensure_server` sets `key_uri` only
when the root came from config or a marker walk, so a markerless file
keys on nil and loose files across unrelated directories deliberately
share one server per language. Revision 1's "(root, language)" would
have split what the runtime shares and re-reported one failure once per
directory. The memo now uses the real key and the stage changes no
reuse behaviour.
Dedupe and current-failure state were conflated into one record, and
revision 1 said nothing about recovery. One record cannot do both jobs:
keep it and `*lsp*` shows a failure the user has already fixed; clear
it and the message returns on the next file open. They are now two
records with different lifetimes, the reported identity includes the
command so repointing config at a different missing executable reports
again, and recovery is pinned in both surfaces.
The modeline provider is a pure per-buffer projection by design — it
runs for every window on every paint. Revision 1's "read the failure
table" would have made it derive an affinity key, invoking user root
resolvers and project detection inside painting. The failure is now
projected per buffer at attach time and the segment stays one map
lookup, with a preservation pin that counts resolver invocations.
"Adopt listview's idiom" is replaced by requiring `pmacs.listview.open`
and naming what it guarantees, including `on_refresh` — without which
`listview.refresh` early-returns and `g` is a bound key that silently
does nothing.
The acceptance had no journey ratchet row despite the stage making step
6 real; `tests/journey_acceptance.rs` states that steps 6-12 join as
later stages make them real, and 1b-1 added step 9 the same way. The
end-to-end row is now acceptance 1 and the M4 pins stay beneath it.
Also refreshes the ledger's canonical-base anchor and recovery floor
from `7586905` to `fbcf235`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`COHERENCE.md` §1.2's canonical silence, and journey step 6: a
preconfigured-but-missing language server fails with no status message,
no record, and no modeline marker, while tree-sitter highlighting keeps
working and masks it.
Three findings shape the design, all verified in the tree:
- The reporting pattern is already adopted twice inside `lsp.lua`
itself — root-resolver failures and subscriber failures both report
through `pmacs.editor.set_status` with the `pmacs.error` arm riding
along. The canonical spawn failure at `:658-674` was simply never
converted, so this stage finishes an adoption rather than starting
one.
- Half the stage is already built and unwired.
`LspManager::status_buffer_text()` renders "the `*lsp*` status
buffer", `last_error(sid)` exists, and both are exposed to Lua and
tested — with no production caller, no `*lsp*` buffer and no command.
Several doc comments already refer to that buffer as though it
exists.
- COHERENCE's frequency note is wrong, and the correction decides the
hardest question. It records the failure as firing once per project
root; `LspManager::spawn` returns early *before* both
`status_tracker.ensure` and `clients.insert`, so a failed spawn
leaves no record, the affinity loop cannot see it, and the real rate
is once per file open. Hence the rule the stage adopts: memoize the
report, not the failure — the spawn is still retried, so installing
the binary mid-session recovers with nothing to invalidate.
Records one limitation rather than hiding it: `status_buffer_text`
renders from `self.clients`, which a failed spawn never enters, so the
durable surface cannot yet show the failure natively. The record stays
in Lua for this stage.
Also logs four stale `COHERENCE.md` §1.2 citations for correction when
the stage lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Review round 1, one finding, accepted.
"A failed escalation is never retried by anything" was false.
`shutdown()`'s force-kill loop iterates the reap ledger with **no**
`!entry.killed` guard, so it does re-kill an entry the escalation arm
gave up on. The accurate claim is that no later *tick* retries it —
`tick_reap_ledger`'s escalation is guarded by `!entry.killed` and never
fires again for that group.
The overclaim collapsed two failure modes that this lane exists to keep
distinct: a failed escalation leaks the group until editor exit, where
one more attempt is made, while a failed `shutdown()` force-kill leaks
it past exit with nothing left to try. Narrowed in the framing, the
handoff, the active-work ledger and the test commentary.
The corrected claim was asserted in three documents and pinned by
nothing, so it gets a pin: a failed escalation marks the entry, the
survivor stays alive across ticks, and `shutdown()` — with no fault
planned, so its force-kill really lands — still reaps it. Bitten by
adding the missing `!entry.killed` guard to that loop: the new pin
fails and the other five stay green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Not a new design round. Every bet resolved, and one acceptance turned
out to be satisfiable vacuously.
Acceptance 2's in-drain clause said "the live descendant's named late
output absent" without saying how the descendant stays live. It does
not, by default: `poll_one` TERMs the whole group on leader exit. The
first fixture's marker was therefore absent on both paths, and the pin
would have stayed green with the collapse fixed.
Bets 1 and 2 hold — every site took a directed outcome without
restructuring, and every consequence is reachable, so the lane does not
shrink. Bet 3 resolves the shutdown coupling as real and measured. Bet
4 is falsified exactly as its own clause anticipated: `ProcessEvent` is
keyed by `ProcessId` while the ledger is keyed by pgid and is
deliberately independent of managed records, every production consumer
polls per known id, and `take_all_events` has no production consumer at
all. Q#RL3 is answered accordingly and reporting is parked as its own
lane.
Also records the four sites and the two reusable lessons in
`docs/agent-handoff.md` — that a seam for a background loop has to be
directed, and that an absence assertion is only as good as its
fixture's ability to produce the thing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Refine the framing's in-drain test seam: a one-shot probe error cannot
survive the 50 ms quiescence interval, so the test-only override lasts
for one drain context and remains per-supervisor. Name the in-drain
SIGKILL as a non-observable local fact rather than adding an untestable
seam, and synchronize the revision-three ledger record.
The revision-2 commit shipped the framing alone. Its ledger edit was
generated by a script that aborted on a line-wrap mismatch before
writing, so `b881bd4` left the lane entry describing revision 1 — three
silent failures instead of four, no in-drain twin, no site-directed
seam, and the withdrawn "Bet 1 ships alone" staging.
Synchronized now: four sites including `final_drain_runtime` and its
distinct reader-cancellation consequence; journey step 9 rather than 8;
the `any_running()` precondition on the shutdown coupling; and the
first PR scoped as seam plus tests.
The failure mode is worth naming, because a partial write is exactly the
shape that leaves a document confidently wrong: the commit succeeded and
reported one file changed, which is easy to read as success when two
were intended.
Three blocking and two major findings, all accepted, all verified in the
code first.
- **§0 named the wrong journey step**, and contradicted Q#RL5 in the same
document. The ledger arms only for `spec.group`, which spawn rejects
for PTY mode, so no terminal reaches it. The only production
`group = true` caller is compile mode, making this journey step 9
(build/test) plus general pipe-process cleanup.
- **A fourth site was missed.** `final_drain_runtime` collapses every
probe errno to "dead" while no tick runs, discards its SIGKILL result,
and sets its own flag. It is not identical to the persistent ledger —
a later tick can retry that, this decision is terminal for the drain —
and its consequence differs: a false "dead" cancels the readers, so
the failure mode is truncated output rather than a leaked process. It
also constrains the seam, being a free function with no `&mut self`.
- **The staging contradicted itself**: "Bet 1 ships alone" against an
acceptance requiring failure-path tests, under one-branch/one-PR. It
was also wrong on its own terms — a seam with no tests does not show
it reaches the intended production calls. The first PR is now seam
plus behaviour-preserving tests, still diagnosis-only.
- **A generic seam is the wrong shape.** `shutdown()` calls
`self.signal(*id, SIGKILL)` before its ledger force-kill, so one
shared "next kill errno" would be consumed by the wrong call and the
test would pass while proving nothing. The seam is now site-directed
with four independently addressable outcomes, expressing more than one
pending result because the coupling test needs two at once, and with
fixture cleanup stated rather than assumed.
- **§1.3 overstated the loop coupling.** Early exit needs the ledger
empty AND `any_running()` already false. The precondition is now in
the ground truth and in Bet 3's fixture requirement.
Framing only. No code.
Unparked from PR #200's §5, which retired the premise justifying the
ledger's leniency and deliberately changed no disposition.
Scouting found three silent failures where #200 named two. A probe error
of any errno drops the entry and cancels escalation; a failed escalating
SIGKILL is recorded as a success and never retried; and `shutdown()`
discards its own force-kill result identically — on the path written
specifically to stop a leak at editor exit.
Two facts shape the lane. `shutdown()`'s final loop terminates when the
ledger empties, which happens through the same silent drop, so the probe
error that hides a leak can also end the cleanup loop early — the probe
cannot be made strict in isolation. And all three paths are untestable
today: `tick_reap_ledger` and `shutdown()` call nix directly and consult
no injection seam, unlike `signal()`'s `forced_kill_errno`, so every one
of the five existing ledger tests exercises only the success path.
Diagnosis first; no disposition change is proposed. Stage A of the signal
lane had three tolerance rules rejected across three revisions, each for
concluding something about one entity from something about another, on
this same data structure.
The framing states plainly that none of the three has been observed:
#200 saw an explicit SIGTERM fail in `signal()`, not a ledger call.
Framing only. No code.
`docs/active-work.md` auto-merged without conflict — this branch's lane
entry sits above the folding lane, clear of the six blocks #199 removed.
Verified rather than assumed: all lane headers checked afterwards.
The merge surfaced a stale statement on `main`. The process-signal lane
still read "PR #200 open, four review rounds closed, held for review",
which stopped being true when #200 merged as `a2a92bb`. Its arc is
complete — Stage A #176 and Stage B #200 both landed, and the items its
§5 parked become their own lanes — so under the rule #199 established
(a lane retires when its ARC is done, not when a PR merges) it is
removed and recorded under "Closed since the last snapshot".
Doing that here rather than deferring it: this PR already edits the
file, and leaving a known-false statement on `main` to keep a PR
single-purpose is the wrong trade. The alternative was letting it stand
until the reap-ledger lane, which is scoped next and inherits from the
very §5 list being retired.
Review round 5: one blocking, one major. Both documentation-only; the
runtime fixes from round 4 are unchanged.
**The pre-kill sample was overclaimed.** Acceptance 4 said the measured
group "describes the target that was attempted". It does not. `getpgid`
and `kill` remain separated by the same read-then-act window §1.5 is
built on, so the sample can be stale by the time the signal is
delivered. Moving it earlier removes a POST-HOC reading; it does not
make the reading contemporaneous. The criterion now says it records
pre-kill evidence about the attempted target, and adds an explicit
sentence forbidding any acceptance from claiming otherwise.
This is worth naming rather than quietly editing: the framing's whole
spine is that this telemetry establishes less than it appears to, and
round 4 fixed a real ordering defect and then inflated the fix's meaning
inside the document that governs it.
**`95897f7`'s commit message carries the same overclaim.** It is not
amended — force-pushing a branch under review to rewrite history would
hide the error rather than correct it. This message is the correction,
and the two read in sequence.
**A dead doc comment was documenting a constant.** The original
pre-Stage-B acceptance-1 block survived the rewrite and came to rest
above `const BASH`, so the file explained a string constant with the
sentence this entire lane exists to remove — "here they are asserted to
agree only because nothing has moved the terminal", describing a test
that no longer exists. Removed; only the BASH explanation remains.
Verified the overclaim is gone from the whole tree, not only the cited
line: no hits in `docs/` or `src/`.
Gates: 11 gates, 4,471 tests, zero failures.
Review round 4: three blocking, one major. All accepted.
**`measured_group` was sampled after the failure.** It ran inside
`signal_failure_report`, after the `kill` and after `observe_leader`,
while the framing and the function's own doc both said before. A
concurrent group change would have made the diagnostic report
post-failure state as evidence about the attempted target. It is now
sampled in `signal` before the kill and passed into the report, so the
field describes the target that was attempted rather than the state the
failure left behind.
**The Linux corroboration did not exercise the production lookup.** Its
helper read `portable_pty::process_group_leader` — the accessor this
lane stopped using on the signal path — so `pty_foreground_group` could
have fallen back on every call with every test still green. Forcing it
to always fall back demonstrates the hole exactly: the corroboration
fails, and the injected pin PASSES, because the injected tests supply
the group themselves and structurally cannot detect a broken lookup.
The helper now calls the production lookup, and the corroboration forces
only the kill so the report is built from a real terminal read. The
residual limitation is recorded rather than left to the green: on macOS
`pty_foreground_group` has no end-to-end coverage, because the platform
cannot produce the precondition.
**The framing did not update its own acceptance contract.** Revision 5
recorded Bet 1's falsification in the revision history and in the bet,
and left the normative criterion demanding the real-shell rewrite — the
"implementation quietly diverges from the contract" shape this project
already recorded as a lesson on #191/#188. Acceptance 1 now describes
the injected pin, 1a adds the corroboration and its macOS limitation,
and 4 states the sampling order. The ledger is synchronized: revision 6,
four commits, 4,471 tests, bash armed on Linux only.
**`TargetSource`'s doc had the wrong classification.** Two of the four
variants target the leader pid, not one, and the pid-versus-group split
does not line up with PTY-versus-pipe — which is why the fallback needed
its own variant rather than reusing `LeaderPid`.
Gates: 11 gates, 4,471 tests, zero failures.
CI falsified framing Bet 1. Both macOS legs reported
job control never moved the terminal off the leader
(leader=8542, foreground groups observed: [8542])
with the terminal staying on the leader for the entire 10s bounded wait.
Linux diverges reliably — 20/20 locally and green on both ubuntu legs —
so this is a platform difference rather than a flake, and rerunning past
it would have been wrong.
The framing named this outcome and prescribed the response, so that is
what ships rather than an improvised fix:
- The divergent case is pinned by INJECTING the foreground group at the
`signal_target` seam. Deterministic, runs on every platform. The
injection seam widens from failure-only to either outcome; the branch,
target choice, leader observation against the real child, and report
construction all remain production code.
- Verified still discriminating: the `leader_pid`-substitution mutation
fails it, `target=-1707909` against an expected `-1707910`. That was
the whole point of the original rewrite and it survives the fallback.
- The real shell is retained as corroboration in
`job_control_really_diverges_the_foreground_group`, Linux-only. It
skips on macOS by PLATFORM CHECK rather than by arming: the
precondition genuinely does not hold there, so running it would assert
a false claim about macOS instead of finding a bug.
- Framing revision 5 records the falsification and states exactly how
the injected pin is weaker — it proves the target is read from the
lookup rather than substituted from the leader; it does not by itself
prove any real shell produces that divergence.
`PMACS_REQUIRE_BASH` moves to Linux-only. The earlier reasoning for
arming both platforms — macOS is where the failures happen, so Linux-only
leaves it dark where it matters — was right about the diagnostic and
wrong about this test, which cannot produce its precondition on macOS at
all. Arming it there made a missing binary fatal for a test that can
never run. The measurement is recorded in ci.yml and the README so it is
not re-derived.
Gates on this tree: 11 gates, 4471 tests, zero failures.
One blocking and two major findings, all accepted.
- The isolated gate still inherited `PMACS_STATE_HOME`, which wins over
`XDG_STATE_HOME` in `user_state_dir` (`src/state.rs:47-68`), so
controlling the four XDG storage variables did not isolate state on a
machine that exports it. The contract now names the exact five
variables rather than counting roots.
- Q#TI5 still called self-spawn the durable regression guard after
revision 3 had accepted that it proves behaviour, not continued
adoption. It now names both guards and their different jobs: self-spawn
is the behavioural proof, the checked source inventory is the adoption
proof.
- The active-work lane still said revision 2 and still prescribed an
isolated `XDG_CONFIG_HOME` alone; both are synchronized, and the stale
config-only gating note elsewhere in the ledger is corrected to say it
isolates the observed symptom rather than the gate.
Framing only. No code.
`docs/active-work.md`'s snapshot header was the only conflict; main's
absorption version supersedes this branch's. `docs/agent-handoff.md`
auto-merged, and both sides were verified present afterwards rather than
assumed: the absorption's `4cd4a7b` anchor and its new landed-lane
entries, and this branch's macOS occurrence with its run link.
This lane's ledger entry is updated from "revision 4 awaiting review" to
its actual state — implemented, gated, PR open — since the framing was
approved and both implementation commits have landed on the branch.
The full gate suite is re-run on the merged tree rather than inherited
from the pre-merge head. The absorption is docs-only, so a clean merge
is very likely correct — which is exactly when inheriting a green would
be most tempting and least justified.
Three blocking and two major findings, all accepted. Two were verified
by running the thing rather than reading it.
- **Journey isolation had no executable mechanism.** Cargo launches each
integration-test binary with the caller's environment, and a binary
cannot re-point its own roots before its tests run — the `set_var`
prohibition applies to itself. Revision 2's "isolated by its launched
environment" therefore assumed the external wrapper this lane exists
to delete. Now named and pinned: each journey test re-execs
`current_exe()` with `--exact`, a marker, and controlled roots; the
child runs the real body against the ambient `EditorState::open`, so
the production-entry-point ratchet is untouched while the roots are
contained. The same helper serves the hostile-environment check.
- **One self-spawning test is not a ratchet.** It proves the seam works
and cannot notice a raw `EditorState::new()` added to another binary
later. A checked source inventory, falsifiable by adding an ambient
constructor, is now acceptance 12.
- **The root list and the gates disagreed with the audit.** The scope is
now explicit — bootstrap STORAGE roots only (config, data, state,
cache). `HOME`'s non-storage semantics are excluded by decision, not
omission: `expand_tilde` resolves user-entered `~` and
`find_file_acceptance` pins it deliberately, so redirecting `HOME`
would retarget a user-facing feature. `XDG_RUNTIME_DIR` addresses
sockets, not stored data.
- **The gate instruction itself was insufficient**, and this is the
finding with immediate consequences: isolating only `XDG_CONFIG_HOME`
stops the reads and leaves the write path open. Every local gate run
in this repository today had that hole.
- **The count was one high and the ledger overstated it further.** 65
files call the constructor; 66 mention it. The 66th, `m5_6_acceptance`,
mentions it only to say it deliberately does not use it — making it
the third place in the suite documenting the `cfg(test)` gap. The
ledger's "all 96 test files load the real config" was false.
- **The recovery command did not work**, verified by running it:
`git worktree add <path> <remote-only-branch>` fails with `fatal:
invalid reference` after a bare fetch. Replaced with the explicit
tracking-branch form.
Framing only. No code.
Four blocking and two major findings, all accepted, all verified in the
code before acceptance. The lane's scope changes: it is about ambient
roots, not about init.lua.
- The read-only assumption was already false. `EditorState::new`
materializes bundled packages unconditionally and before config
loading, into `XDG_DATA_HOME` or `$HOME/.local/share`, and
`materialize_all` creates directories. Confirmed on the development
machine: `~/.local/share/pmacs/builtin-packages/` exists with v0.1.0
and v1.0.0. This upgrades the lane from "local gates lie" to "tests
write into real user data".
- The population count was wrong and its stated method did not match the
command that produced it: 18 from a grep for `Editor::new`, which does
not match the real constructor `EditorState::new`. 66 of 96 files
construct an editor directly.
- File-level classification cannot work: 5 files are both in-process and
spawned. Classification moves to construction sites.
- The seam must cover `EditorState::open`, which calls `Self::new()`
directly, while `journey_acceptance` requires that exact public entry
point to avoid a dead-production-path test. Resolved by isolating
journey through its launched environment rather than a different call.
- Isolated construction must still return `is_init_complete() == true`.
Config loading and `set_init_complete()` share one block, and
`m8_2_acceptance.rs:75` documents its dependence on integration-test
construction being init-complete — the `cfg(test)` gap is load-bearing
in that one respect.
- Revision 1 both proposed and parked a hostile-config CI leg. Resolved
in favour of a test-binary self-spawn, which travels with the test
rather than the workflow file, and which now also asserts the hostile
root is unmodified afterwards.
- The lane is recorded in `docs/active-work.md`, which revision 1 omitted
despite the volatile-work protocol requiring it.
Also documents six ambient roots where the shared daemon harness sets
two, and why setting HOME only isolates a root whose XDG variable is
unset — the harness's apparent adequacy is a property of one developer's
environment.
Framing only. No code.
Eight PRs merged on 2026-07-29/30 (#188, #190, #191, #194, #195, #196,
#197, #198) and `docs/active-work.md` had drifted to 1,854 lines, most of
it describing work already on `main`. Rule 4 permits removal once a
lane's facts reach `docs/agent-handoff.md`; this does both halves in one
commit so the precondition is satisfied rather than deferred.
Handoff §1: the head-of-`main` anchor moves from `6c9e765` to `4cd4a7b`
and its date from 2026-07-28 to 2026-07-30. New entries for the three
implementation PRs that had no record — #190's pre-filesystem delete
refusal, #191's Stage 1 adoption of the generated-buffer contract, and
#196's dired Stage 2a — plus #188 recorded as landed rather than
proposed.
Ledger: six merged lanes removed (781 lines). Two lanes are NOT removed
and are rewritten to their remaining plan instead, because their arcs are
unfinished: generated-buffer immutability (Stage 1 merged, Stage 2 not
started) and bottom-panel (Stage 2 complete, Stage 3 ahead). The
bottom-panel block said so in its own text — "this lane is not removed at
2B-3's merge" — so a removal keyed on "the PR merged" would have
discarded live planning. The header now states the rule as ARC-done
rather than PR-merged.
Two method notes are recorded in the ledger because they nearly cost
real work:
- A census by PR number is a proxy, not a measurement. Counting `#NNN`
in the handoff reported that five of these lanes had no record at all.
Counting by content found most already documented, with the real gap
being the implementation PRs while their framings were recorded. The
absorption written from the first count would have duplicated existing
entries.
- #194 and #195 keep their lessons in §3 and §5 rather than §1, which is
why a number search finds them once each. Sufficient under rule 3:
durable knowledge needs a home, not a required section.
Docs only. No code, no protocol change.
Integration tests read the developer's real `~/.config/pmacs/init.lua`.
The suite is green in CI and deterministically red on any machine with a
real config, attributed to whatever branch is checked out — 11 of 67 in
`compile_mode_acceptance` on 2026-07-30, 67/67 with an isolated
`XDG_CONFIG_HOME`.
The mechanism is that `src/editor.rs:770` guards config loading with
`#[cfg(not(test))]`, which is set only when compiling the crate's own
unit tests. An integration test links pmacs as an ordinary dependency, so
the guard is inactive for all 96 of them. The hazard was identified and a
mitigation was written; its scope does not match the threat.
The obvious fix is unavailable: `std::env::set_var` is unsafe and the
crate forbids unsafe, which the repo already knows —
`Installer::root_override` exists for exactly this reason.
Framing only, awaiting review round 1. No code changes.
Framing acceptances 2, 3, 4, 5, 7 and 8. Evidence collection only: no
tolerance rule, no change to which process is signalled, no disposition
change.
Three distinct failures previously rendered as one string.
**The PTY fallback is now named.** When a PTY's foreground-group lookup
yields no group, the target falls back to the leader — and until now that
rendered "leader-pid", identical to a pipe child that never had a
terminal. `portable-pty::MasterPty::process_group_leader` collapses every
failure into `None` before pmacs can see it, so the errno was gone too.
pmacs now performs the query itself and reports four distinct outcomes:
no master fd, a failed duplicate with its errno, a failed `tcgetpgrp`
with its errno, and a non-positive answer.
Doing that without `unsafe` is the interesting part. `nix::unistd::
tcgetpgrp` needs `AsFd`; `MasterPty` exposes only `Option<RawFd>`; and
every std route between them is `unsafe`, which this crate forbids.
`filedescriptor::OwnedHandle::dup` takes any `AsRawFd` through a safe
blanket impl and returns an owned handle that IS `AsFd`, so a
lifetime-tied view implementing one safe trait is the whole bridge. The
borrow is what makes it sound: the view cannot outlive the master, so the
descriptor cannot close underneath it.
**The report names the signal.** A failed SIGUSR1 and a failed SIGTERM
were the same text. Note this is a reporting gap only — every failed
`kill` returns before the fatal-signal branch, so failed signals are
disposition-identical whatever they are. A separate control pins that the
fatal/non-fatal difference is real for calls that SUCCEED, which is what
gives the first test its meaning.
**`measured_group` is a real observation.** `expected_group` is
`-leader_pid`, and on the spawn-group path the target is `-leader_pid`
too, so the report printed the same number three times and their
agreement was arithmetic rather than evidence. `getpgid` supplies the one
field that can disagree. It establishes no identity — it is read inside
the same read-then-act window, and no portable mechanism closes that for
a group.
Bites, each by an actual revert, all observed to fail:
- collapsing the PTY fallback back into a bare "leader-pid";
- dropping `signal=` from the report;
- making the measured group restate the pid it was handed;
- replacing the job-control fixture with a plain `sleep`, as a positive
control on the divergence fixture itself.
All four exact-string sites were updated individually, never by a blanket
rewrite: a wholesale rewrite of expected strings is how a format
regression hides. `:2501`'s first-call disposition pin is retained and
updated for the new format rather than replaced.
`nix`'s `process` feature is now declared explicitly. It already arrived
transitively — nix's own `signal` feature depends on it — which is stable
but invisible, and a real requirement resting on another feature's
internals is one refactor away from vanishing. `filedescriptor` is
declared directly for the same reason: pmacs now calls its API.
The reap ledger's comment claiming "EPERM cannot happen for our own
children" is corrected. Its bounded-growth policy is unchanged, but the
justification was wrong: the probe targets a group, and owning the
spawned child says nothing about a group unless the child is still a
member — which nothing measures. The handoff records this together with
the limit of the evidence: the occurrence does NOT establish that the
child itself received EPERM.
Integrate canonical main, restore PTY errno capture through the safe
filedescriptor duplication bridge, narrow the macOS occurrence to what
it actually measured, and make the job-control fixture foreground and
discriminating.
Record the portable framing lane and exact recovery commands in the
active-work ledger.
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, in the same shape
as #191's: `main` inserted the generated-buffer Stage 1 lane immediately
above the bottom-panel header this branch had rewritten. The resolution
keeps both.
Each side's newer text wins where that side owns the fact: `main` carries
the corrected #188 status (MERGED/APPROVED, replacing "OPEN, PROPOSED —
do not implement"), and this branch carries the bottom-panel lane's 2B-3
state and the newer snapshot date, replacing main's "2B-2 MERGED; 2B-3 IS
NEXT" and its 2B-2 boundary paragraphs.
Verified: no conflict markers; every line absent from either parent is a
deliberate supersession by the other, enumerated and checked one by one
rather than counted; all three lane headers present exactly once.
`docs/active-work.md` was the only conflicting file. #196 added the dired
Stage 2a lane at the position this branch had used to relabel the #188
framing lane header; the resolution keeps both, changing neither side's
wording.
`src/editor_core.rs` auto-merged. Both lanes touch it, so a clean
textual merge is not evidence of a clean semantic one — the gate suite
is re-run in full on the merged tree rather than inherited from the
pre-merge head.
Resolution verified for line loss in both directions: the resolved file
differs from `main` only by this branch's own authored edits, and
differs from this branch only by additions taken from `main`.
Review round 2, three findings.
setsid is util-linux, not coreutils, and the standard `cargo test --lib`
gate must not hard-fail on a tool the README does not declare -- a
minimal or BusyBox container would fail without ever testing pmacs. The
hard assert becomes skip-unless-armed via PMACS_REQUIRE_SETSID, which is
the pattern the silent-skip lane already established, so the test cannot
quietly report `ok` having never run where the tool is guaranteed. CI
arms it on Linux; README declares it. Both arms verified against a PATH
with setsid genuinely removed: unarmed skips with its message, armed
FAILS with the diagnostic.
The durable causal account was wrong, and this corrects it in the
framing, the handoff and the ledger. basedpyright's console script runs
bundled node through `subprocess.run` and WAITS
(nodejs_wheel/executable.py:50, verified in the installed 1.39.6). It
does not exit at spawn. What orphans node is pmacs: `shutdown()` SIGTERMs
the recorded pid -- the Python wrapper -- which dies without forwarding
the signal, leaving node at PPid 1 holding the pipes. The refutation was
already in hand: the initialize handshake succeeds, which a wrapper that
exited at spawn could not have done, and the PPid 1 observation was taken
after shutdown had killed it.
The fix is unaffected -- the deadlock and its bite are unchanged -- but
the parked follow-up changes target: not "tolerate servers that
self-orphan" but "stop orphaning them", i.e. signal the process group
rather than a wrapper pid that swallows the signal. Framing section 5 P2
restated.
Also corrects a stale CI-ordering claim: the handoff said pyright must
stay unarmed until the timeout lane lands, but #195 is this PR's base and
gave every job a timeout-minutes. The one live reason is that CI does not
install basedpyright at all. The ci.yml comment asserting the job has no
timeout-minutes was stale for the same reason and is rewritten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Integrated main (#195) first so the lane block is written on top of it
rather than conflicting with it — the one conflict this file always has,
paid at the merge that was happening anyway.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Review round 1 widened two contracts this document had stated narrowly, and
a narrow statement is what let the implementation drift inside it:
R7-1 — the stable-probe decision was written about the geometry DECLARATION,
which let painting and hit-testing keep using the document-dependent advance.
Three grids, one asserted. It now covers all three consumers, resolved once
and cached behind the declaration so they agree by construction.
R7-2 — the x=0 full-width contract was stated about `total.cols` and read as
a claim about the declaration alone; it governs the band's content rectangle
too, remainder included.
R7-3 records that splitting the advertised baseline from PROTOCOL_VERSION
makes the VersionMismatch server field load-bearing rather than incidental.
R7-4 names criterion 54's fixture and why it drives the real display="panel"
adopter opt-in instead of opening a terminal and moving it.
A2B-3 and criterion 48 gain the halves that were implicit and therefore
skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
The four findings were one shape — a failure that left state wrong and
told nobody — so the lane records them as one lesson rather than four
bugs: every one was a `pcall` or a discarded return value, and each
looked like defensive coding.
Also records the round-1 pin that passed with its own bug restored
(acceptance 53's attribution assertion was satisfied by the deleted
path's basename appearing elsewhere in the same message), the refreshed
gate numbers, and that `main` was re-measured after the round and had
not moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Round 2 facts for the lane-4 entry: the dash/bash divergence that
falsified the `<&0` form and how the positive control caught it, the
eleven-suite Bet 2 result, and the evidence that acc28 on macos/lua54 was
a flake -- a rerun of the same job on the identical head, not an
assumption.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
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
Handoff section 1 gains the 2B-3 bullet and the protocol anchor moves to
v21 with the rule that matters stated once: advertise the baseline,
negotiate up from the frontend's AttachRequest, and reserve moving the
advertised version for a change that cannot be expressed additively at
all. 2B-1's forward-looking constraint is marked discharged rather than
deleted, because its acceptance still passes unchanged and that is the
evidence.
COHERENCE section 14 grades the bottom/side panel primitive as complete on
BOTH frontends rather than 'Stage 2 pending its own framing', section 20
P5 follows it, and the section-19 protocol bullet records that the v21
family is live in production without an incompatible handshake change.
The active-work lane is rewritten to the shipped slice, including the
rejected activation alternatives and why the server-first shape forces
each one out, and the one-way compatibility window it leaves open.
The 2B-2 acceptance suite's header said production keeps panel_capable
false for every semantic session. That is no longer true, and its
assertions did not change — which is the point, so the header now says so
rather than being quietly left stale.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Rides this branch rather than a standalone ledger PR: with several PRs
open, a lane written on `main` for work that lands elsewhere
re-conflicts on every merge.
Records the measured merge-base as pasted output, what 2b and 2c still
owe so the split boundary is auditable, the two re-pinned m4 rows, the
one framing claim found wrong, the two bites that were vacuous as
specified and why, the gate numbers, and the §16 ownership warning
against starting Journey Stage 1b while this is open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
One conflict, `docs/active-work.md`, with three strands rather than the
usual one: main gained #190's lane, this branch carries its own Stage 1
lane and a relabel of the #188 framing lane, and main had removed the
documentation lane while this branch still had it.
Resolved by construction. Main's file taken whole; this branch's Stage
1 lane reinserted at its own position ahead of the bottom-panel lane;
this branch's relabelled framing lane ("MERGED AS PR #188") kept in
place of main's stale "OPEN, PROPOSED" version; main's removal of the
documentation lane preserved.
Verified against both parents rather than by inspection: the Stage 1
block is byte-identical to this branch's, the documentation lane is
gone, no conflict markers survive, and the update-protocol rule 6 seam
check finds no double blanks.
Note for whoever absorbs next: main now carries three lanes describing
merged PRs (#190, #188, #194). This merge keeps this branch's more
accurate labelling of the #188 one but does not remove any of them ---
rule 4 permits removal only once durable facts reach
`docs/agent-handoff.md`, and none of those three PRs touched it.
Pure insertion: main's ledger taken whole with this lane's 3a block
placed before the parked lane. Verified against both parents --- zero
lines removed relative to main, the lane-3a block byte-identical to
this branch's, no conflict markers, and the update-protocol rule 6 seam
check clean.
Deliberately NOT absorbed here, because absorption is not mechanical
and this PR is approved on its current content. `main` now carries
THREE lanes describing merged PRs: the resource-op delete guard
(#190), the generated-buffer immutability framing (#188), and this
arc's own silent-skip arming (#194). Rule 4 forbids relabelling any of
them and permits removal only once their durable facts reach
`docs/agent-handoff.md` --- and none of the three PRs touched that
file, so all three absorptions are genuinely owed rather than
overlooked.
Two belong to other arcs. The third (#194) belongs to this one, and
its durable fact is not yet written down anywhere: that
`PMACS_REQUIRE_*` arms an otherwise-vacuous skip, and that
basedpyright stays unarmed until the reader-join hang and the CI
timeouts both land. That wants a handoff bullet, which is content
rather than a merge resolution.
P3 --- required status checks are name-coupled to job names, and a
required context that no longer exists does NOT fail. It leaves every
PR pinned on "Expected --- waiting for status" forever, which is `main`
becoming unmergeable by policy rather than by a red run. Three of this
lane's own deferrals will do exactly that: the macOS matrix trim
removes two contexts outright, and nextest or the serial/parallel split
rename or add jobs.
The rule is now in the ledger entry --- any job rename, removal, or
matrix change updates the branch-protection required-checks list in the
same motion --- and it is recorded HERE deliberately, because this is
the single entry that both enabled protection and named the lanes that
will invalidate it. Arming the warning anywhere else would separate the
trap from the thing that sets it.
P4 --- the rewritten top comment said "everything else keeps 25 against
a sub-4-minute observed max" and dropped the clause noting that
`m6-perf-gates` keeps its own tighter 15. Restored. Worth the fixup in
a change whose entire subject was comments matching reality.
Beyond the PR, and taken here rather than deferred: `TEST_IMPROVEMENT.md`
on `main` still said "no branch protection on `main` (verified via API:
404, so every job is advisory)" and listed §5.1 as open. Both went
stale during this session, and THIS lane is what made them stale, so it
carries the correction rather than leaving it for whoever touches the
file next. Struck through in both places rather than rewritten: the 404
was a true reading at audit time, and the document is the arc's scoping
record, so what changed is more useful than a clean-looking present
tense. Note also that protection shipped wider than §5.1 proposed ---
all 12 contexts required, not the cheap-jobs-only starter --- which the
correction states.
Verified: YAML parses; the seam check from update-protocol rule 6 finds
no double blanks; `git diff --check` clean.
P1 --- the ceiling was justified against the wrong number. Revision 1
cited "~14.6 min, ample headroom", which was one reading quoted as a
property, and this ledger's own rule applies to it: a census is a
reading, not a constant. Re-measured over two windows --- 17 min max
over 25 runs, 15.8 over 12, both macOS/luajit, every other job under
4 --- so a flat 25 was about 1.5x the observed tail, not "ample".
Two facts shape the fix. `timeout-minutes` counts EXECUTION, not queue,
so the 33-minute wall-clock run in that window executed its longest job
in 17 and no run in observed history would have been killed by either
value. And the real exposure is the case no window contains: a cold
cache. A stable-toolchain bump invalidates Swatinem's key on every leg
at once, and a cold macOS debug build plus suite is the plausible way a
HEALTHY run overruns --- presenting as four legs timing out
simultaneously the day after a Rust release.
So the test job takes 35 (~2x its observed max) and the rest keep 25
(~6x theirs), and the diagnosis is written into the workflow BEFORE the
event: simultaneous four-leg timeouts after a toolchain release are a
cold cache, not a hang; a single leg timing out beside passing siblings
is the hang case these ceilings exist to catch. 35 still beats the
360-minute default by an order of magnitude, so the basedpyright
arming this gate unblocks is unaffected.
P2 --- §5.1 was missing from both lists, and review was right that the
omission matters. But its premise had gone stale, which is worth
recording rather than quietly working around: branch protection is ON.
It was enabled earlier in this session, and I re-verified against the
API rather than trusting either the review or my own memory of doing
it:
{"enforce_admins":false,"force_push":false,
"required_checks":12,"strict":false}
Recorded in the ledger as DONE with the settings and the reasoning for
each --- `strict` off so a PR need not rebase every time `main` moves,
`enforce_admins` off so the user keeps an override. This also settles
the concurrency comment, which justifies exempting `main` pushes by
appeal to "the branch-protection record": that record exists, so the
justification is real rather than aspirational, and no softening is
needed.
P3 --- the double blank line before the parked lane, third PR running.
Fixed, and added to the ledger's own update protocol as step 6, since
fixing the instance three times has not stopped it: a block ending in a
blank line inserted above a heading already preceded by one leaves the
seam, and it survives review by sitting beneath the level anyone reads
at. The rule now names the check.
Verified: YAML parses; ceilings are 25 except test at 35 and
m6-perf-gates at its tighter 15; the seam check finds no double blanks
anywhere in the ledger; `git diff --check` clean. Workflow and ledger
only.
Keep both active-work lanes while taking the silent-skip arming and
generated-buffer framing changes from current main. The resource-op lane
retains its round-2 fixes and updates its recorded merge-base.
Record the path-normalization and partial-first-operation fixes, their
acceptance criteria and bite pre-images, and the green round-2 gate
results in the active-work ledger.
Lane 3a of the testing arc --- the three cheap, deterministic items of
`TEST_IMPROVEMENT.md` §5-6. The larger ones (nextest, the
serial/parallel split, a parallel canary leg, the nightly cron, the
macOS matrix trim) are deliberately NOT here: each changes what CI
certifies or how it runs, and each deserves its own decision rather
than riding in on a timeout patch.
`timeout-minutes` on every job (§5.2). Measured before changing rather
than assumed: SEVEN of eight jobs had none and inherited GitHub's
360-minute default; only `m6-perf-gates` had one, at 15. So a single
hung test burnt six hours --- times four on the test matrix --- and
reported nothing useful at the end of it. Set to 25 against a measured
~14.6 min critical path (macOS/luajit), which leaves ample headroom for
a slow runner while catching a hang in under half an hour.
This is the gate that has to exist before `PMACS_REQUIRE_PYRIGHT` can
ever be set. Lane 2 left basedpyright unarmed *because* this did not
exist; the two decisions are the same decision, half a lane apart.
`concurrency` with `cancel-in-progress` (§6.1), scoped to pull
requests. This project rebases heavily --- the ledger re-conflicts on
nearly every merge --- so branches take several pushes while earlier
runs are still going, and macOS minutes are both the expensive ones and
the critical path. Pushes to `main` are deliberately exempt:
`github.event.pull_request.number` is empty there, so the fallback keys
those runs by SHA and none can cancel another. Cancelling a `main` run
would leave the branch-protection record ambiguous about a commit that
has already landed, which is the one place the saving is not worth
having.
`-p pmacs-protocol` clippy (§5.7). The root-package clippy never
covered it --- the workspace default member is only `pmacs` --- so a
warning introduced through a protocol-only change would reach `main`
unseen. Verified passing locally BEFORE proposing it, so it cannot turn
CI red on arrival.
The timeout rationale is stated once above the job list rather than
copied onto each job: the first draft duplicated a seven-line comment
across seven jobs, which is the same degraded-copy shape this arc keeps
removing elsewhere.
Verified: YAML parses; all eight jobs carry a timeout (seven at 25,
m6-perf-gates keeping its tighter 15); `cargo fmt --all --check`,
`clippy -p pmacs-protocol` and `clippy -p pmacs-gpu` all exit 0;
`git diff --check` clean. The diff touches `ci.yml` and the ledger and
nothing else, so no code gate is affected.
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.