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.
Framing §3 Bet 1 and acceptance 1. Committed alone, per the branch plan:
this bet decides whether the diagnostic is worth extending at all, so its
result belongs in history before anything depends on it.
The previous version of
`a_group_directed_kill_failure_reports_target_and_leader_separately`
spawned `/bin/sleep` on a PTY and asserted the same pid three times,
conceding in its own doc comment that the values "are asserted to agree
only because nothing has moved the terminal". The entire premise of the
diagnostic is that the terminal's foreground group and the spawned leader
are different entities, and no test exercised a case where they were.
The fixture now drives job control: `bash -m` runs a foreground job in a
fresh process group and hands it the terminal, so `tcgetpgrp` reports a
group that is not the leader. The trailing `; :` is load-bearing — with a
single simple command `bash -c` execs in place, which would leave the
leader owning the terminal and silently restore the agreement.
The bounded wait is also load-bearing rather than defensive. A probe of
this fixture observed the foreground group as the leader FIRST and only
then as the job's group, so measuring immediately would pin the
non-divergent case. The fixture additionally asserts the diverged group
still holds a live member, so a divergent number cannot come from a dead
group.
Falsified in both directions by substituting `leader_pid` for the
`tcgetpgrp` result in `signal_target`:
- the new test FAILS — target=-1020100 (leader) against the expected
-1020103 (foreground group);
- the OLD test, restored verbatim alongside the same mutation, PASSES.
That pair is the finding: the previous acceptance pinned the
substitution as acceptable.
`/bin/bash` is declared as an optional test dependency and armed with
PMACS_REQUIRE_BASH on BOTH CI platforms, not only Linux — the signal
failures this diagnostic exists to explain have so far occurred only on
macOS. The guard tests the exact path the fixture spawns rather than
`which bash`, because a system with bash on PATH but not at `/bin/bash`
would pass a `which` guard and then fail the spawn.
Verified: both arming arms exercised against an absent path (unarmed
skips, armed fails by name); 20/20 repetitions of the test; `cargo fmt
--check`, `cargo clippy --workspace --all-targets -- -D warnings`, and
`cargo test --lib` (1877 passed) all clean; ci.yml parses.
No production behaviour changes.
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
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.
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.
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.
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.
Two follow-ups on this session's audit work, both on freshly-merged code.
F-004 hardening (regression fix). The AltGr text-input strip
(`is_layout_text`) gated on *any* command modifier, so it fired on
Alt-alone. On macOS the Option key is reported as Alt and emits printable
text for most letters (Option+x → "≈"), so every GUI Meta binding (M-x,
M-f, M-b, …) was stripped to a self-insert. Tighten the gate to the true
AltGr signature — both Ctrl and Alt (the LCtrl+RAlt the OS synthesizes on
Windows) — so Alt-alone forwards as a Meta chord again while Windows AltGr
still inserts. Strict narrowing of when we strip: no-op on Linux/Windows,
unblocks macOS Option-as-Meta. Test flips the Alt-alone € assertion and
adds the macOS Option+x case.
CI coverage (F-001 residue). `workspace_default_members` is only the root
`pmacs` package, so `cargo test` skipped pmacs-protocol — the shared wire
format the daemon, TUI, and GPU all depend on. Add
`cargo test -p pmacs-protocol --all-targets` to the test job (its ~12
encode/decode + transport-framing tests). All three first-party crates now
run in CI (root pmacs, pmacs-gpu via the render job, now pmacs-protocol).
Validated: fmt clean; clippy -p pmacs-gpu --all-targets clean; pmacs-gpu
tests 42 pass (render tests on the local adapter, PMACS_REQUIRE_GPU=1);
pmacs-protocol 12 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
The first run showed lavapipe installed and visible to vulkaninfo
(DRIVER_ID_MESA_LLVMPIPE), but the render tests still found no wgpu
adapter and — correctly — hard-failed under PMACS_REQUIRE_GPU. Cause:
VK_ICD_FILENAMES pinned an ICD path that doesn't match the runner, which
overrides the loader's default discovery and hides every ICD. Drop it and
rely on default discovery (which vulkaninfo uses); also `ls` the icd.d
dir for future debugging.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
Audit F-014. pmacs-gpu had 40 tests but none exercised the wgpu
composition path — layout/render regressions passed silently, a human
eyeball the only gate. This adds the first tests that actually render a
frame, headless, and read the pixels back.
Enabling refactor (least-invasive; not the Renderer-sub-struct split):
- `State.window`/`.surface` become `Option`; a shared `assemble(...)`
builds the window-agnostic half, called by both the windowed `new`
and a `#[cfg(test)] new_headless(w, h, text)` (compatible_surface:
None, returns None when no adapter). `request_redraw` is now an
Option-guarded helper across its 15 sites; `resize` guards
`surface.configure`.
- `render()` splits into a surface-acquire wrapper + window-agnostic
`render_to_view(&TextureView)`; `#[cfg(test)] render_offscreen()`
renders through the same path into a RENDER_ATTACHMENT|COPY_SRC
texture and reads it back (256-byte row alignment).
Two smoke tests through the real composition path: a full frame is
non-uniform (something composited); setting text changes the frame vs an
empty buffer. They skip when no adapter is present, except under
`PMACS_REQUIRE_GPU` (CI) where a missing adapter is a hard failure.
CI: a "GPU Render (headless)" job installs mesa-vulkan-drivers
(lavapipe) and runs `cargo test -p pmacs-gpu` — also the first time
pmacs-gpu's tests run in CI at all (the workspace test job covers only
the root package).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
Acts on the 2026-07-03 repository audit (tracked as
docs/repository-audit-2026-07-03.md).
F-001 — the pmacs-gpu clippy CI blind spot, closed:
- Fix the 3 workspace-clippy lints in pmacs-gpu/src/main.rs (two
cast_possible_wrap in minimap_jump_to via i64::try_from; a
format_push_string in compose_status_spans via write!).
- README documents `cargo clippy --workspace --all-targets` and
`cargo test --workspace`.
- CI's Lint job now also runs `cargo clippy -p pmacs-gpu --all-targets
-- -D warnings` — the root-package clippy never lints the flavor-
independent pmacs-gpu crate, which is how these (and recent) warnings
slipped through.
F-013 — refresh stale metadata: pmacs-gpu/Cargo.toml description and the
docs/pmacs-gpu-design.md status header, from "session 2: hello-world /
pre-implementation" to the real feature set + the per-feature docs.
F-015 — .gitignore editor backups (escaped `#*#`, `.local-bak`) and the
root `/pmacs` dev symlink, so they stop showing as untracked. Untracked
docs are left for triage per the audit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U