Commit Graph

500 Commits

Author SHA1 Message Date
Levi Neuwirth 02645a71b0 docs(journey): frame Stage 1b-3 — say something when the editor opens
The last of the 1b split. Journey step 4 and `COHERENCE.md` §18: a
fresh `pmacs` greets the user with an empty buffer, an empty status
line, and no indication that `M-x` exists — the sole discovery
affordance in the product is knowing to press it.

Three findings shape the design:

- The existing step-2 ratchet pin collides with any welcome. It asserts
  `status.is_empty()` while its own message says "reports no error" —
  the same predicate only while nothing writes a non-error status at
  startup. The framing corrects the assertion to its message's claim
  and names that as the one existing assertion the stage changes,
  rather than burying it in a diff.

- `C-h` is not free. It is bound to word-delete because non-kitty
  terminals cannot disambiguate Ctrl+Backspace from Ctrl+H — both
  produce byte 0x08 — so rebinding it to a help prefix would break
  Ctrl+Backspace on every legacy terminal. §2's step-4 row calls it an
  oversight; it is a deliberate trade with a stated rationale. The
  help-prefix decision is deferred to the discovery arc with the
  constraint recorded so it is inherited rather than rediscovered.

- A `*help*` buffer mechanism already exists over `src/help.rs`, so the
  welcome can point at something real. Its two gaps are recorded rather
  than inherited silently: it writes with delete/insert instead of
  `set_generated_contents`, and it is found by name.

The welcome deliberately does NOT use `set_generated_contents`: that
lifts read-only, discards history and marks the buffer generated, all
wrong for the buffer step 5 requires the user to type into immediately.

Step 4 stays Partial — the row names a welcome, a cheat sheet and
`C-h`, and this closes the first plus a minimal second — so this is the
first 1b stage with no landed-evidence obligation on merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-31 14:28:19 -04:00
Levi Neuwirth 732aa282cc docs(journey): keep keybinding audit state aligned with PR #203 2026-07-30 21:45:28 -04:00
Levi Neuwirth 0b1716b5cd fix(journey): canonicalize the compile-directory expectation; unflip the grades
Review round 1 on #203, two blocking findings, both accepted.

The compile-directory assertions used the suite's lexical `canon()`,
but `pmacs.project.detect` canonicalizes before walking
(`canonicalize_or_passthrough`, `src/project.rs:509-511`), so the
compile cwd is filesystem-canonical. On macOS `/var` is a symlink to
`/private/var` and the two spellings disagree — both macOS legs failed
while Ubuntu, where `/tmp` is not a symlink, stayed green.

Fixed with a `detected_root()` expectation, and pinned by a fixture
that launches through an explicit **symlink** so lexical and canonical
paths disagree on every platform. That matters more than the fix: the
original bite ran only on Linux, where nothing could make the two
differ, so no amount of local mutation testing would have caught this.
The new pin is the only one that goes red when the lexical expectation
is restored.

Second: `COHERENCE.md` §2's step-9 row was flipped to **Works** and the
handoff said Stage 1b-1 was **LANDED**, while this PR is open. §25 is
explicit that grades change only with landed evidence, never
aspirationally. Both now describe the real state — the row stays
Partial and names #203 as the open PR that closes it, §20 and the arc
list say "in flight", and the handoff says IMPLEMENTED with the PR
number.

The flip is not dropped, it is owned: the active-work lane records the
four places to change on merge, because an unowned doc flip is exactly
how this ledger's drift starts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-30 21:38:47 -04:00
Levi Neuwirth a62dbfb4d3 Merge remote-tracking branch 'githubsucks/main' into journey-stage1b1-compile-defaults 2026-07-30 21:13:19 -04:00
Levi Neuwirth 6bee82c1a5 feat(compile): make building discoverable — journey step 9
Implements `docs/journey-stage1b1-compile-defaults-framing.md`
(approved at revision 2). Lua, tests and docs; no Rust change and no
protocol change.

`C-c c` now runs `compile.run`, and the first prompt is prefilled from
the detected project kind through `pmacs.compile.defaults` — seeded
`rust = "cargo build"` and extensible from `init.lua`. `_last` still
wins, so a session that has compiled keeps its own command.

The prompt CAPTURES its directory rather than re-resolving it. Sharing
one resolver between the prompt and the run is necessary and not
sufficient: `pmacs.minibuffer.read` is asynchronous and nothing freezes
the active window while a prompt is open, so two calls to the same
resolver at two different moments are still two different answers — the
user could be offered `cargo build` for A and handed a run in B by
clicking away mid-prompt. This is Journey Stage 1a's `commit_to`
discipline on a smaller seam.

`pmacs.compile.defaults` is public and assignable, so the lookup is
guarded: a throwing `__index`, a non-string entry and a non-table
replacement all degrade to the pre-stage empty prompt and never
prevent compiling.

Only `rust` ships seeded. Rust has one answer; npm/yarn/pnpm,
make/cmake, and `go build` versus `go test` do not, and a wrong prefill
costs more than an empty one.

Adds eight step-9 rows to the journey ratchet and five module pins to
the compile suite. Corrects `COHERENCE.md`, which named a
`ProjectKind::Cargo` that does not exist — the variant is `Rust`, line
77 is its doc comment, and Lua only ever sees the tag string "rust".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-30 21:13:12 -04:00
Levi Neuwirth 7df13f2257 fix(process): scope the escalation claim to ticks, and pin the boundary
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
2026-07-30 20:41:03 -04:00
Levi Neuwirth 2a1cf9be2e docs(process): framing revision 4 — record what implementing found
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
2026-07-30 18:58:17 -04:00
Levi Neuwirth 44c3be1ed4 docs(journey): framing revision 2 — close review round 1
Two blocking, two major. All four accepted and verified in the code
before the doc changed.

Sharing one cwd resolver between the prompt and the run does not
prevent drift. `pmacs.minibuffer.read` is asynchronous, nothing freezes
the active window while a prompt is open, and `run` re-resolved at
accept time — so the prompt could offer `cargo build` for A and execute
in B. Two calls to the same resolver at two different moments are still
two different answers. The interactive command now captures
`context()` and passes its `cwd` through to `run`. This is Journey
Stage 1a's `commit_to` discipline on a smaller seam: capture at request
time, never re-derive from whatever is ambient when the async work
lands.

No pin crossed the accept boundary. N1-N5 compared values the prompt
and the resolver had already agreed on, so a wrong directory inside
`on_accept` — exactly the defect above — passed every one of them. Two
pins now accept: N3 switches windows between prompt and RET and reads
the directory back two ways, and N4 accepts `cargo build` unedited in a
real Cargo fixture and asserts a clean exit.

N3's old falsifier was not discriminating: `project_root_of_active()`
already returns the innermost root, so re-detecting from it yields the
same kind and the pin would have stayed green. The mixed-fixture pin is
now N5 with a falsifier it actually catches, and says what it does not.

And §6.1 contradicted §3.1 — a Cargo workspace subdirectory holds no
`Cargo.toml` and correctly receives `cargo build`. The predicate is a
detected project, not a file in that one directory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-30 18:40:34 -04:00
Levi Neuwirth e26ddd5ec3 docs(journey): frame Stage 1b-1 — make building discoverable
`COHERENCE.md` §20 Priority 1 names Journey Stage 1b as the golden
journey's remainder and it had no branch, no framing, and no lane —
the only §20 priority with nothing in flight. This frames the first
third of it: journey step 9, build or test the project.

Scope is a global `C-c c` for `compile.run` plus a first prompt
prefilled from the detected project kind instead of empty. Lua, tests
and docs; no Rust change, no protocol change. LSP spawn guidance and
the welcome buffer are separate stages (§7).

Three ground-truth findings the framing rests on:

- `ProjectKind::Cargo` does not exist. COHERENCE names it twice; the
  variant is `ProjectKind::Rust` and line 77 is its doc comment. Lua
  matches the tag string `pmacs.project.detect` already returns, so
  the stage needs no new Rust primitive.
- The compile cwd is resolved inside `run`, after the prompt closes,
  so a suggestion built in the command's `fn` would obey a different
  rule than the run. The stage extracts one resolution and has both
  consume it.
- The last-resort cwd is `std::env::current_dir()` at call time, which
  in tests is the pmacs repo root — itself a Cargo project. Negative
  pins are built so that fallback is never reached.

Records one limitation rather than fixing it: after `pmacs <dir>` the
active buffer is dired's and pathless, so the cwd falls through to the
process cwd. That needs COHERENCE §8's execution-location model, not a
reach into dired's private handle table.

Adds the journey lane to the active-work ledger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-30 18:15:58 -04:00
Levi Neuwirth a9a1484ff7 docs(process): make reap-ledger seam falsifiable
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.
2026-07-30 16:41:10 -04:00
Levi Neuwirth b27b377f67 docs(active-work): synchronize the reap-ledger lane with revision 2
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.
2026-07-30 16:22:43 -04:00
Levi Neuwirth b881bd4c4e docs(process): framing revision 2 — close review round 1
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.
2026-07-30 16:20:09 -04:00
Levi Neuwirth 088f417e70 docs(process): frame the reap ledger's silent failures
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.
2026-07-30 16:13:02 -04:00
Levi Neuwirth 55b4897c07 merge: integrate main @ a2a92bb; retire the merged signal lane
`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.
2026-07-30 15:49:32 -04:00
Levi Neuwirth 262b7c0b6f docs(process): stop overclaiming the pre-kill sample; drop a dead comment
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.
2026-07-30 14:41:50 -04:00
Levi Neuwirth 95897f7563 fix(process): sample the group before the kill; make corroboration real
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.
2026-07-30 14:27:46 -04:00
Levi Neuwirth 66f73013fc test(process): take Bet 1's fallback — macOS does not diverge
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.
2026-07-30 14:10:39 -04:00
Levi Neuwirth e1df0cff4d docs(tests): framing revision 4 — close review round 3
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.
2026-07-30 13:57:02 -04:00
Levi Neuwirth 595676cd20 merge: integrate main @ b8e18f6 (ledger absorption #199)
`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.
2026-07-30 13:49:57 -04:00
Levi Neuwirth e9613c7d94 docs(tests): framing revision 3 — close review round 2
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.
2026-07-30 13:45:15 -04:00
Levi Neuwirth f2071ea022 docs(tests): framing revision 2 — close review round 1
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.
2026-07-30 13:36:23 -04:00
Levi Neuwirth e1cf1fc76b docs: absorption pass — six merged lanes retired, handoff brought current
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.
2026-07-30 13:23:32 -04:00
Levi Neuwirth 89ee2f9985 docs(tests): frame the ambient-config isolation defect
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.
2026-07-30 13:14:58 -04:00
Levi Neuwirth b27df705bb feat(process): make the signal diagnostic discriminating (Bets 2-4)
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.
2026-07-30 13:08:47 -04:00
Levi Neuwirth df50880762 docs(process): framing revision 4 — correct review assumptions
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.
2026-07-30 12:28:48 -04:00
Levi Neuwirth 77887d0683 Merge remote-tracking branch 'githubsucks/main' into process-signal-diagnostic-completeness 2026-07-30 12:21:12 -04:00
Levi Neuwirth 872e4aacad docs(process): framing revision 3 — close review round 2
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.
2026-07-30 12:09:34 -04:00
Levi Neuwirth 76cb13bda8 docs(process): frame the signal diagnostic completeness lane (rev 2)
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.
2026-07-30 11:39:40 -04:00
Levi Neuwirth 7a271f13f5 merge: integrate main @ 391d38a (PRs #197, #196, #191) into Stage 2B-3
`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.
2026-07-30 11:16:23 -04:00
Levi Neuwirth 12e2cff466 merge: integrate main @ c14f2de (PRs #197, #196) into the Stage 1 lane
`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`.
2026-07-30 10:13:37 -04:00
Levi Neuwirth c14f2de428
Merge pull request #196 from levineuwirth/dired-stage2-impl
dired Stage 2a: rename and delete reconciliation
2026-07-30 10:03:09 -04:00
Levi Neuwirth ed544fab41 fix(process): arm the setsid dependency, correct the orphan account
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
2026-07-29 22:54:06 -04:00
Levi Neuwirth 71db6e98f0 docs(active-work): record round 1 on the bottom-panel lane
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
2026-07-29 22:43:30 -04:00
Levi Neuwirth 02b54d8d73 Merge remote-tracking branch 'githubsucks/main' into bottom-panel-stage2b3 2026-07-29 22:41:38 -04:00
Levi Neuwirth a1fada45e8 docs(bottom-panel): record framing revision 7
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
2026-07-29 22:37:41 -04:00
Levi Neuwirth 46ac2074c3 Merge remote-tracking branch 'githubsucks/main' into process-teardown-stdin-deadlock
# Conflicts:
#	docs/active-work.md
2026-07-29 22:16:50 -04:00
Levi Neuwirth e11c3d4ab8 docs(active-work): record dired Stage 2a review round 1
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
2026-07-29 21:47:32 -04:00
Levi Neuwirth d5a1dc2660 docs(active-work): record the CI-falsified reproduction and the acc28 flake
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
2026-07-29 21:09:35 -04:00
Levi Neuwirth 9b1cf3d6c9 fix(process): reproduce the deadlock without a shell
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
2026-07-29 20:58:52 -04:00
Levi Neuwirth 36a37f6086 docs(active-work): add the lane-4 process-teardown deadlock entry
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
2026-07-29 19:29:31 -04:00
Levi Neuwirth eb9b36387c fix(process): close child stdin before joining reader threads
`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
2026-07-29 19:22:59 -04:00
Levi Neuwirth 8ee08d93e5 docs(bottom-panel): record Stage 2B-3 and close Stage 2
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
2026-07-29 19:12:36 -04:00
Levi Neuwirth d5fadf4120 docs(active-work): add the dired Stage 2a lane
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
2026-07-29 19:12:15 -04:00
Levi Neuwirth 7350cfdd8a merge: integrate main @ e003b81 (PR #190) into the Stage 1 lane
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.
2026-07-29 15:12:13 -04:00
Levi Neuwirth 5d32c1edc8 merge: integrate main @ e003b81 (PR #190) into the CI lane
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.
2026-07-29 15:10:22 -04:00
Levi Neuwirth 2554bfcbb9 review round 2: arm the required-checks name-coupling trap, restore m6
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.
2026-07-29 14:16:27 -04:00
Levi Neuwirth 63c5545979 review round 1: anchor the ceilings on observed execution, record §5.1
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.
2026-07-29 14:12:47 -04:00
Levi Neuwirth ef5bd89e84 merge: integrate main @ b7bf2c6 after PR 190 review round 2
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.
2026-07-29 13:49:06 -04:00
Levi Neuwirth 156567b5dd docs: record PR 190 review round 2
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.
2026-07-29 13:32:08 -04:00
Levi Neuwirth 708054e8d5 ci: put a timeout on every job, cancel superseded PR runs, lint the protocol
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.
2026-07-29 13:05:56 -04:00