Four blocking issues, all upheld. The first defeats the whole design if
left standing.
1. Boundary errors trusted unvalidated stderr. A helper exiting 1 with
NO token but the canonical "SIGINT is ignored" text would classify
as boundary error --- correctly --- and then tell the operator their
environment ignores SIGINT. A6 satisfied in the classification,
violated in the message actually read. Now: a validated pair's
stderr IS the diagnosis and is surfaced unchanged; a boundary
failure's stderr is untrusted, and the consumer emits its own
wording, omitting the child's or labelling it untrusted. New A6b
witnesses exactly that case (conformance row 23), with a mutation
for a consumer that surfaces it anyway.
2. The matrix did not prove exact-pair validation: no invalid status-2
pair existed, and the expected column collapsed validated
(2, :error) with boundary errors, so a validator accepting every
status 2 passed all twelve rows. The matrix is now a 23-case
cross-product distinguishing `error (validated)` from
`error (boundary)`, with (2, missing), (2, :safe), (2, :ignored) and
(2, unknown-version) all boundary. New A6c pins it.
3. Normalisation was internally inconsistent and not implementable
identically. "Strip one newline then trim ASCII whitespace" removes
further newlines, so TOKEN\n\n would have validated while the same
clause demanded single-line output --- and POSIX $() strips ALL
trailing newlines while Rust returns raw bytes, so the consumers
could not have agreed even on a correct rule. Replaced by one byte
grammar, stdout := TOKEN | TOKEN LF, with NO trimming, plus the
shell sentinel idiom `out=$(helper; printf x); out=${out%x}` so the
shell preserves what it must compare. Vectors added for extra
newline, leading newline, surrounding spaces, CRLF and doubled
token.
4. The ledger's old A7 assertion --- satisfied by disclosure, Linux-only,
no non-Linux unix reachable --- contradicted its own macOS record
twenty lines above. Marked explicitly as revision-12 history with
the live record named.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Five blocking inconsistencies, all upheld. The first was the worst: the
document specified a validated pair and then printed an algorithm that
emits no tokens and a consumer flow that proceeds on exit 0 alone ---
accepting 0 with a missing token, the exact defect revision 13 forbids.
1. The algorithm now emits exactly one token per arm on stdout with
diagnostics on stderr; the consumer flow is pair-validation with
explicit normalisation (strip one trailing newline, trim ASCII
whitespace, require exactly one line); and the outcome table is
keyed on pairs, with a fourth row for boundary error including
macOS's status 1 with no token. `safe` is validated like the
others --- a status arriving without its token did not come from
this helper.
2. A6a is SCOPED TO THE GATE. R-d never sees a shell status: the gate
goes through /bin/sh, which turns an exec failure into an exit
status, while Rust's Command returns a spawn error with no status
at all --- conformance row 12, not row 5. And macOS CI does not
compile R-d's test, which is crdt-gated while the macOS jobs build
without crdt. R-d on macOS is unexercised, and the framing says so
rather than implying coverage.
3. A7 is restated against measurement. It cannot still say no
non-Linux unix was tried when macOS ran and went red: five of six
helper/gate rows pass there, one defect is named, R-d is recorded
Linux-only, and the remaining portability claim is labelled a
contract argument.
4. "Both consumers use the same helper so they can never disagree" is
withdrawn --- true when the status WAS the verdict, false once each
consumer validates a pair independently in a different language.
Replaced by a twelve-case conformance matrix both validators must
agree on, including the macOS case and a normalisation case.
5. The token-to-stderr mutation is remapped from A2 to A1/A3, with
the reasoning recorded: with stdout empty every outcome becomes
boundary error, which still satisfies A2 as written since A2 only
requires "not the deadline message". A2 stays broad and A6 pins
which diagnosis appears.
The ledger is aligned: the mechanism is established rather than
hypothesised, the "stderr prints the raw status" claim is corrected ---
the number appears only in the catch-all, and this failure took the
other branch --- and revision 12 is marked superseded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
CI on 916007b: 12 green, 2 red, both macOS Test jobs, and exactly one
row --- gate_maps_an_unexecutable_helper_to_error_not_ignored, left
Some(1) right Some(2). The other five SIGINT rows pass on macOS.
This is the A7 portability finding the review pre-declared, and it is a
real one: the gate returned 1, meaning `ignored`, for a helper it could
not execute --- the exact conflation §7c forbids.
The cause is not established. The leading hypothesis is that the ABI's
1 is ambiguous by construction: 1 means "ignored", and 1 is also a
status shells hand back for assorted failures. On Linux an unexecutable
file yields 126 and the catch-all maps it to 2; if macOS /bin/sh
returns 1 instead, the two cases are the same number at the boundary
and no catch-all can separate them. That would call for verdicts
outside the range shells produce, which is a design change needing its
own revision --- not something to patch here.
This commit only makes the failure self-diagnosing: the assertion now
includes the gate's stderr, which prints the raw probe status it saw.
The first failure could not say which status produced it, because the
message discarded stderr.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two ledger findings, both mine.
The lane block still said "No PR" while its own header and a new entry
recorded PR #241.
And the self-referential checkpoint wording had gone false, which is the
same trap as naming a branch's own tip: "this entry's own commit adds
the A6 rows" was true when written at 167d830 and false by d64d300, and
"the entry's own commit adds only the gate record" was 7cef9ca. Every
event now carries its IMMUTABLE sha --- implementation 3206433, A6 rows
and bounded negative path 167d830, factual corrections c9cc8dd, gate
record 7cef9ca, PR record d64d300 --- and only the branch tip stays
symbolic, which is the one pointer that has to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Opened from gpu-probe-sigint-teardown into main after the quiet 8/8 gate
on c9cc8dd. Not merged; awaiting review rounds.
Docs-only, per the recording exemption that keeps gate evidence from
recursing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Full gate GREEN on the committed head c9cc8dd, all 8 stages, log
20260819T160220Z-2339958, started at load 3.90.
The preceding attempt on the SAME head is kept rather than dropped. It
failed 04-lib-crdt and 07-sweep on four wall-clock rows --- the
composition budget, the summary-flatten scaling row, dired's 200ms
budget and a lean4 progress notification --- none of which touches this
lane's change. Load average was 49.6 and an unrelated
./verify_task_state.sh run was compiling under a separate toolchain at
/usr/local/rustup, having started about three minutes in and
overlapping precisely the two failing stages.
That overlap is recorded as evidence of WHEN, not proof of WHY. This
lane already retracted one confident environmental attribution, so the
red run was treated as "not valid evidence" rather than explained away,
and the green run on the same commit is what settles it. Had any of the
four failed again on a quiet machine it would have been a real finding
on this branch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Both mine, both checkable against evidence already in the repo.
The ledger said 35 gate-acceptance rows. The suite has 33. The 35 was
git_status_stage1_acceptance's result line, which sits immediately
below gate_script_acceptance's in the sweep log; I read the wrong one.
The correction names the misread so the next reader can see how a
transcription from a sweep log goes wrong.
The framing header newly attributed revision 12's approval to 7752bcb.
It was 1fc0df6 --- as the ledger says and as 7752bcb's own commit
message says in its first line. Restored.
The full gate is re-run on THIS commit rather than on the tree that
preceded it; the previous run finished twenty seconds before 167d830
was committed, so it described an uncommitted tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Three findings, all upheld.
1. A6 was witnessed only for the helper. Both consumers now have real
-path rows.
Gate side, driven through a stub worktree --- a temp git repo holding
a copy of scripts/gate and a controlled helper --- so the gate's own
code path runs against each verdict without touching the checked-in
helper: a stub exiting 2 refuses with the ERROR wording and never
"SIGINT is ignored"; a NON-EXECUTABLE stub maps 126 to boundary
error 2 with its own wording. That second case is what the original
guard got wrong twice.
R-d side: the precondition is split into sigint_diagnosis() ->
Result, so the message is testable rather than reachable only
through a panic in a test that cannot run under the condition it
describes. The new row asserts safe proceeds, ignored says so and
says "NOT a teardown defect", error says "could not determine" and
never "ignored", and an unrunnable helper is undecidable at the
boundary.
2. The refusal row violated this suite's no-recursion constraint: it
invoked the ordinary gate, so a regression of the exact `if !` bug
would have launched eight real gate stages inside the gate suite.
It now uses --self-test, which drives the same runner over a
hardcoded synthetic plan, so the negative path stays bounded
whatever the guard does. under_ignored_sigint() also takes the
program and arguments POSITIONALLY --- `exec "$@"` --- instead of
interpolating them into script text, which broke for any path
containing a space or shell metacharacter, and every path here comes
from a tempdir or CARGO_MANIFEST_DIR.
3. The portable checkpoint is recorded: implementation at 3206433,
pushed, signed, clean, full default gate green 8/8 foreground. The
framing header no longer says implementation "may proceed" --- it
reports IMPLEMENTED. And docs/agent-handoff.md §3 gains the durable
rule: never start the gate or cargo test from a shell that ignores
SIGINT, `setsid nohup ... &` is forbidden, SIG_IGN is inherited
across fork and survives exec, the gate refuses with no override,
and scripts/check-sigint-deliverable answers the question directly.
35 gate-acceptance rows, 16 gpu_invocation_acceptance rows, full gate
green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Four findings, all upheld, and the first was a live bug I shipped.
1. R-b's non-zero handling was unreachable. scripts/gate runs under
`set -eu`, so the bare helper invocation killed the shell at exit 1
or 2 and neither `sigint_status=$?` nor the refusal messages ever
ran; an unexecutable helper would have escaped as raw 126/127 rather
than boundary error 2. Reproduced before fixing.
The first repair was ALSO wrong, and worse: `if ! helper; then
sigint_status=$?; fi` captures the status of the NEGATED condition,
which is always 0, so the gate printed the ignored diagnosis and
then ran the entire suite. The working shape is `helper ||
sigint_status=$?` --- failure handled, so `set -e` does not fire and
`$?` is the helper's own --- which is the idiom the helper already
uses internally. Statuses 1 and 2 pass through unchanged; everything
else, including 126/127, maps to 2 at the boundary and is never
reported as "SIGINT is ignored".
The guard also moved to immediately after the worktree resolves,
before any log directory, ambient root or tmpdir exists, so a
refused run leaves nothing behind.
2. The behaviour had no durable coverage, which is exactly why 27
passing gate tests missed both bugs. Four rows added: helper safe,
helper ignored, helper error (and never ignored), and gate refusal
before stage 1. Ignored-SIGINT is simulated with `trap "" INT`,
which is the real mechanism --- SIG_IGN inherited across fork and
surviving exec --- not a stand-in. Verified to bite: mutating the
gate back to either shipped bug fails
gate_refuses_to_start_when_sigint_is_ignored and nothing else.
3. The ledger now records the implementation, both bugs, the four rows
and their mutation check.
4. A7 is recorded SATISFIED BY DISCLOSURE, which is the fallback
revision 12 allows when no non-Linux unix is reachable. The earlier
"stays open" contradicted the approved contract and is withdrawn.
Tried: Linux x86_64, all three outcomes, all consumers. Not tried:
every non-Linux unix. Claimed: POSIX shell only, no /proc, no
sigaction --- labelled a contract argument, not a measurement.
The full default gate passes all eight stages foreground; it caught a
rustfmt violation in the new test code on the first attempt, which is
the guard-and-gate arrangement working as intended.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 12 is approved at 1fc0df6 after closing the controlled-arm
provenance, total-helper-ABI, and standing-ledger blockers. Record that
R-b plus R-d implementation may proceed under the replacement A1-A7
contract.
Make the second controlled-arm record portable without changing what it
claims: identify head 77b623c, transcribe the actual foreground and
background harness invocations, include the exact evidence-recording
harness, label the captured exit as cargo's, and carry both full binary
digests in both arm columns.
Turn the signal probe into an implementable shared ABI. The checked-in
helper owns classification and diagnostics: 0 is safe, 1 is inherited
ignore, and 2 is probe error. Preserve kill failure in the inner shell,
surface the helper's stderr unchanged in both consumers, and witness the
error outcome in both paths. Correct the mutation mapping so removing
the trap bites foreground success rather than the ignored-signal rows.
Synchronize the active-work ledger with the rerun head, total helper
contract, A1-A7 witnesses, and qualified portability claim.
Three findings, all upheld.
1. The arm provenance was malformed and over-claimed. The "fully
expanded" background command still contained <the fg command above>
and <log> placeholders; both table rows were one cell short of the
header, putting log prefixes under "binary hashes" and leaving the
digest column empty; and the full binary hashes had been read later
from reused paths, which cannot retroactively prove what each arm
executed --- the same provenance rule this document states in §7,
applied against my own record.
Rather than weaken the claim, the arms were re-run at head 77b623c
with FULL SHA-256 captured per run, immediately after each run,
before anything could rebuild them. Both arms: identical
0890b78c...4124c and ef6ff1c1...c696, dirty=0, fg exit=0 ok=2, bg
exit=101 failed=2 SigIgn=0x1007. Byte identity is now carried by the
capture rather than by inference. Commands are written out with no
placeholders, and the table cells line up.
2. The ledger still transported superseded operative instructions: a
"remedy not selected" heading, D0b still owed under A3, journey step
12(a) still assigned, and the old three-consecutive-run A2 contract.
All four now match revision 12's §8/§9 --- remedy selected, D0b
satisfied and not owed, journey steps NONE with gate trustworthiness
named instead, and A1-A7 replacing the three-run contract, which was
written for a flakiness that is now explained.
3. The helper contract was not total. The raw probe reaches exit 0 both
when the kill was a no-op AND when the kill itself failed, so a
broken probe would report "inherited SIG_IGN" and fail the gate for
the wrong reason. The helper now owns the classification and returns
one of safe / ignored / error; consumers consume the verdict and
never re-derive it. `error` is not folded into `ignored` --- it fails
the gate with a different diagnosis, because "your environment
ignores SIGINT" and "the guard could not run" are different
problems. A6 witnesses the distinct error outcome, A7 requires a
non-Linux unix exercise or an explicit statement of what was tried,
and A4 gains a mutation for collapsing error into ignored. R-b's
stale "needs an explicit override" is reconciled with §7c's no
-override decision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Third occurrence of the same process failure, and the one I had already
written the lesson for twice. ab43132's message said the ledger no
longer claims implementation-absent or mechanism-unknown. The ledger
script died on a stale anchor, and because I separated the steps with a
newline instead of chaining them, `git commit` ran regardless. Gating
one step is not enough when the next step is not gated too.
The ledger now records what the framing does: mechanism KNOWN, remedy
SELECTED as R-b + R-d via the portable probe, A3/D0b satisfied by the
controlled explanation so D0b is not owed, revision 12 awaiting
approval, D1/D2 done rather than "the next step", and the diagnostic
instrument named as the only implementation so far.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Four findings on revision 11, all upheld.
1. The operative contract still said the opposite of §4c. Bet 1 read as
open; §7 said the mechanism was unknown with D3/D4 pending; §8 kept
the old criteria and a conditional A5; §9 claimed a journey-12(a)
product repair; the ledger and the revision-10 paragraph still said
D1/D2 had not started. Each is now rewritten as executed, withdrawn,
discharged or superseded --- §9 in particular now records journey
steps touched: NONE, for the stated reason that no product behaviour
changes, with gate trustworthiness named as what the lane does
affect.
2. The causal evidence is now portable and cleanly reproduced. The
first capture came from d12.log, which finished five minutes BEFORE
afe3631 committed the diagnostic code and ran in the reused d0a-B
target --- inadmissible provenance, now marked as the first sighting
only. Replaced by controlled arms on committed head 38f2af4,
dirty=0, in this worktree's own target, with BYTE-IDENTICAL binary
hashes across arms (0890b78cca22ac1e, ef6ff1c15e11062a): foreground
exit=0 ok=2, background exit=101 failed=2 SigIgn=0x1007. The outer
invocation is recorded as a first-class column, since it is the
causal variable and every earlier "exact command" omitted it. The
historical foreground/background mapping is marked RECONSTRUCTED
from the transcript, not captured --- no pre-existing row carries an
outer-invocation field, which is precisely why the matrix stayed
confounded for nine revisions.
3. D4 was never executed, so bet 1 is WITHDRAWN BY SCOPE rather than
falsified, and A5 is RETIRED BY SCOPE rather than struck. Nothing
here shows a real wgpu session behaves correctly; what is shown is
that no observed evidence of a user-facing defect survives. The lane
is now gate/test correctness only.
4. The remedy is not selected. §7b evaluates four candidates --- runner
normalisation, an early gate guard, fixture isolation via pre_exec,
and a test-local precondition assertion --- with portability as a
selection criterion, noting /proc is Linux-only while the suite is
cfg(unix) and sigaction querying is unsafe. Likely R-b + R-d, but
nothing is chosen or implemented here. Revision 11's leap from
"pre_exec is unsafe" to "therefore an assertion" did not follow.
Also renames the meaningless african_close() helper (38f2af4).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 10 is approved at 4fba9f6 after aligning A3 with the D0b
contingency. The demonstrated D1/D2 mechanism may account directly for
the subset/full difference; otherwise D0b remains mandatory before the
lane closes.
Record that diagnostic-only D1/D2 are authorised but have not started.
No mechanism or fix is claimed yet.
Two findings, both upheld.
1. The portable provenance was corrupted and incomplete --- worse than
the machine-local pointer it replaced, because it looked verifiable
and was not. Every log digest had lost its leading hex character
(A#1 recorded as 1c0fe47d55d8f5e... where the value is
e1c0fe47d55d8f5e): the extraction started one byte late in
`logsha=<value>`. The captured /tmp and MemAvailable columns were
dropped, and the command block used ellipsed paths. All ten digests
are corrected, both columns restored, and the command is written out
in full with only two named placeholders.
Separately: `uptime` was NEVER CAPTURED. §7's condition list names
it; the harness kept the load averages from it and discarded the
elapsed time. It is now recorded as UNKNOWN for all ten runs, with
the condition list marked as only partially satisfied rather than
implied met. The classifications stand --- none depends on uptime ---
and D1/D2's harness must capture the whole list.
2. Retiring D0b materially changes the approved diagnostic sequence,
which made D0b mandatory before every other diagnostic. The document
still claimed revision 9, approved at 15c25ec, for a decision that
approval does not contain. Promoted to revision 10 and marked
AWAITING APPROVAL; D0a's execution and result are reported under
revision 9, and D1/D2 do not begin until revision 10 is approved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Three findings, all upheld.
1. The causal conclusion overreached, in the same way this lane has
overreached before. Uniform-red at both endpoints today proves only
that the two commits DO NOT DISCRIMINATE UNDER CURRENT CONDITIONS.
"Source hypothesis eliminated", "the interval cannot contain the
transition" and "unreachable by source" are withdrawn from the
framing, the manifest and the ledger: a historical regression could
be masked by a later environmental effect, or by a source/environment
interaction under which both commits now fail. Failing to
discriminate is not the same as not differing. "No bisect is
justified under current conditions" is what survives, and the
approved endpoint table's two uniform-same rows are corrected to say
the same thing.
2. D0b was still mandatory, and going to D1/D2 would have skipped an
approved step. It is now RETIRED AS A PRECONDITION with the reason
recorded: it existed to make the reduction matrix trustworthy so the
subset-vs-full comparison could locate the mechanism indirectly,
and D0a has since produced a reliable direct reproduction that D1/D2
measure against. Re-running ten reduction rows to sharpen an
indirect instrument while a direct one is in hand is the wrong order
of work. The obligation is NOT discharged: A3 still binds, so if
D1/D2 fail to account for why every subset passed, D0b runs before
this lane closes.
3. Provenance is now portable. The exact per-run command and a
transcribed ten-row table --- start time, class, red bins, load,
freeMB, daemon count, log digest --- are committed, rather than
delegated to a machine-local results.tsv. Raw logs stay local by
design. The transcription also surfaces something the delegation hid:
the leaked-daemon count climbs 72 -> 108, four per run, monotonically
while every run classifies identically. Recorded, not implicated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Ten runs under the approved contract: counterbalanced A B B A A B B A A
B, N = 5 per endpoint, clean detached worktrees at 7599661 and 724b785,
isolated target directories, the gate's build-crdt precondition then its
sweep-crdt command, dirty=0 verified per run. Zero voids, zero splits.
A (7599661) uniform-red. B (724b785) uniform-red. By the approved
endpoint table that is the both-endpoints-uniform-same row: the
difference is NOT captured by those two commits.
What it settles:
- No bisect of 7599661..724b785 is justified, and none will run.
7599661 passed inside sweep-crdt on 08-15 and fails 5/5 clean today,
so the interval cannot contain the transition.
- The onset window is demoted --- still a true observation, but not
reachable by source.
- A RELIABLE REPRODUCTION now exists: 10/10 today across two commits
at ~4 minutes per run. This is D0a's most useful product, because
D1/D2 no longer depend on catching a rare event.
What it does not settle: anything about the mechanism. One cheap
negative on "what else changed" --- no package activity in the window per
pacman.log, nearest on 08-18 --- and it is not pursued further, because
with a reproduction in hand direct measurement dominates archaeology.
A's three extra failing binaries are recorded rather than swept up:
a54_real_daemon_real_pty_and_headless_gpu_render..., a v21/v20 row
expected to differ at that older commit, and m6_1_pty_mode_lifecycle.
Two of the three are process/PTY-spawn rows, the same family as the
target. None affect classification, which reads only the two target
copies.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 9 is approved at 15c25ec after the portable manifest and compact
ledger summary preserve the endpoint direction required by D0a.
Record that approval durably before diagnostic implementation begins. The
mechanism remains unknown, no fix is proposed, and panel-mapping-generation
remains held until this teardown lane closes.
The portable manifest collapsed the two clean-split directions even though
the governing endpoint table permits a bisect only when 7599661 is uniform
green and 724b785 is uniform red. Preserve that direction explicitly, and
carry the same distinction in the compact active-work summary.
The inverted split remains a real difference, but it contradicts the onset
reading and therefore requires that reading to be re-examined before any
bisect.
Two D0a findings on revision 8, both upheld.
1. The classifier was not total. "Clean split" and "mixed" left five
outcomes unprescribed, and two of them are in the historical logs
already: 20260815T182846Z-708693 died compiling pmacs so neither
copy executed, and ...-2839374 / ...-830195 were red on unrelated
rows while both ctrl_c copies passed.
A run is now classified from THE TWO COPIES OF THE TARGET TEST and
nothing else --- green (both ok), red (both FAILED), split (copies
disagree), void (either did not execute). A sweep red only on
unrelated tests is therefore a green run, with the unrelated
failures recorded as evidence about environment stability. A split
STOPS the procedure, since two copies of one source disagreeing
within a run is its own defect. Voids are discarded and re-run on a
budget of 3, after which the environment is too unstable to classify
anything and D0a stops.
Endpoint verdicts are uniform green, uniform red, or mixed, and a
six-row table prescribes every combination: clean split permits the
bisect; an inverted split is a real difference that falsifies which
endpoint was believed good; both-uniform-green and both-uniform-red
each mean the difference is not captured by those commits; mixed at
either endpoint means intermittency under fixed source and forbids a
bisect. The manifest had attached "difference is not captured" to
the mixed case --- that conclusion belongs to the uniform-same rows,
and is moved.
2. Strict A/B/A/B does not make drift "hit both arms equally": B always
follows A and owns the final time point. Runs are now counterbalanced
AB BA AB BA AB, which removes systematic order confounding; the
residual last-slot asymmetry is accepted and stated rather than
claimed away.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Four findings on revision 7, all upheld.
1. The old one-run D0 rule survived in three durable places --- the
manifest, this branch's ledger, and the framing's own §4a --- each
still permitting a bisect when the endpoints merely "differ". That
contradicts the N = 5 clean-split contract added in revision 7. All
three now defer to that contract, and §4a's "needs only that the two
clean endpoints differ now" is marked as the superseded rule it is.
2. D0a still overstated its evidence, in three ways now fixed:
- "context-sensitive by construction, appearing only in the full
sweep" is downgraded to what has been OBSERVED so far;
- the historical 7/7 and 13/13 are stated as NOT endpoint-specific
rates --- of seven reds only F6 ran at 724b785, of the greens only
the last at 7599661, both with unknown cleanliness;
- five runs are named a PREDEFINED EVIDENTIARY THRESHOLD chosen so
the outcome cannot be argued after the fact, not something that
mathematically separates intermittency.
And the bisect now specifies its own classifier: every intermediate
commit uses the identical N = 5 protocol, and a mixed classification
ABORTS the bisect rather than being guessed, skipped, or rerun until
it agrees. A bisect with cheaper steps than its endpoints would
inherit the weakness the contract exists to remove.
3. The artifacts column is now exact per run, read from each log:
R1/R2 UNKNOWN (no log preserved), R3 -5d9105cb/-d4dae4f0, R4 and R5
-6b4b8223 only, R6 -91f51d0b/-6b4b8223. R8's citation was half2.log:1;
the executable lines are 438 and 459. The framing's last "not same
binaries" is now "not the same compilations".
4. (Held ledger, 5274d6b.) It named a stale ledger tip and two different
framing revisions on consecutive lines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Five findings on revision 6, all upheld.
1. The ancestry pair supports nothing causal. Revision 6 had already
retreated to "outcome is not determined by commit alone"; that is
withdrawn too, because different commits CAN deterministically
produce different outcomes --- this document's own fix-then-regression
scenario is an example. The two observations differ in commit AND
environment AND time, so they are simply NON-COMPARABLE. The held
ledger's "no source-monotonic cause does that" goes with it.
2. D0a was not a valid decision procedure: one unspecified run per
endpoint cannot establish a regression for a failure that only
appears in the full sweep. Now specified --- N = 5 full sweep-crdt
runs per endpoint, INTERLEAVED A/B/A/B so session drift hits both
arms, identical captured conditions including uptime/free//tmp/
leaked-daemon count, and a bisect permitted ONLY on a clean split.
A mixed result means intermittency under fixed source, and no bisect
is justified at all.
3. "Neither binary contains signal-handling code" is FALSE. The pmacs
binary does: install_signal_handlers (src/daemon.rs:628) registers
SIGINT and SIGTERM; it is simply not on run_gpu's path. A grep of
project sources also cannot exclude a runtime or dependency
installing a disposition. The established fact is narrow --- no
explicit installation on run_gpu's path --- and "whatever disposition
they hold was inherited" is restored to a HYPOTHESIS that D2 must
measure.
4. Artifact wording finished: no "artifact family", "reduction/
workspace artifacts" or "different binaries" remain. Every manifest
row now carries its exact Cargo suffixes read from its log, with a
stated caveat that those logs are machine-local and this manifest is
the portable transcription of them.
5. Held ledger pointed at revision 5; it now points at revision 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Three findings on revision 5, all upheld.
1. The ancestry argument overreached. 72da24a failing today while its
descendant 7599661 passed on 08-15 shows exactly one thing: outcome
is not determined by commit alone, since the observations come from
different environments at different times. Revision 5 said a source
cause was "positively discouraged", that the ancestry "says to
expect" equal endpoints, and that the change was environmental.
None follows. It cannot discriminate an environmental change, a
source/environment interaction, or a fix before 7599661 with a
regression before 724b785 --- and an ancestor OUTSIDE the interval
is irrelevant to whether the interval regressed, since a bisect over
7599661..724b785 needs only that the clean endpoints differ now.
D0a is unchanged as an action but is now stated as a decision
procedure with NO predicted outcome: endpoints differ -> bisect that
interval; endpoints agree -> ask what else changed across the window.
2. The byte-identity withdrawal was incomplete in both ledgers. This
branch's said the artifacts "are byte-different" and then withdrew
it two lines later, still said R9 ran "different binaries", and
still promised an "artifact family". The held ledger still said
"byte-different" and still called the window a bisect target with
revision 4's onset conclusion. Both now say "different Cargo
suffixes/compilations" throughout; historical byte identity is
UNKNOWN and is never claimed.
3. Provenance slips: R9's observation-table row listed only -6b4b8223
although it executed both -91f51d0b and -6b4b8223; R10's suffixes
are at log lines 3 and 24, not 3 and 4; R9's are at 3066 and 3087,
not 3066 alone. All corrected against the logs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Four findings on revision 4, all upheld. The third changes what the
lane should do next.
1. Section summaries still carried revision-3 language while the
manifest carried revision 4's. Framing and ledger now agree: seven
red runs (F1-F7), not five; the observation table is keyed on
compilation set rather than an invented "workspace artifact family";
and it is labelled an observation, not an isolated interaction.
2. The onset count was wrong. Per test copy across the 17 sweep-crdt
logs: 13 with both copies ok, 1 where NEITHER executed because the
stage died compiling pmacs (error[E0308]), and 3 with both failed.
Revision 4's "14 runs, 11 green, 3 red on other tests" mis-stated
both the count and the kind --- one of those runs never reached the
test. The two genuinely red-on-other-tests sweeps did execute
ctrl_c, and it passed.
3. D0a cannot be a source bisect, and the evidence argues against one.
Reflog and commit times put HEAD at 7599661 during the last green
(3c06176 landed 40s after it finished) and at 724b785 during the
first red (5174f73 landed 08:45:41, after that run ended 08:42:01;
the manifest had recorded F6 at 5174f73, which was wrong).
Cleanliness was captured at neither endpoint. And 72da24a is an
ANCESTOR of the passing 7599661 yet fails today --- no
source-monotonic cause produces that. D0a now reproduces the two
endpoints CLEAN, in isolated target directories, and a bisect is
justified only if they differ.
4. Manifest completed: R9 carries full argv rather than a recipe; R7
lists only gpu_invocation-6b4b8223, since R7 does not select
gpu_initial_target; R10 lists both -5d9105cb and -d4dae4f0.
Also withdraws "byte-different" everywhere. The bytes a historical run
executed are not knowable --- target dirs have been overwritten, and a
hash computed today is the current occupant's. Three levels are now kept
apart in the manifest: suffix (known), today's bytes at a path (known),
and the bytes a past run executed (UNKNOWN). Differing suffixes mean
differing Cargo metadata hashes, which is enough to void the comparison
and is all that is claimed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Four findings on revision 3, all upheld. Answering finding 1 turned up
something that reframes the lane.
THE ONSET. sweep-crdt appears SEVENTEEN times in this target directory's
gate logs. The ctrl_c failure appears in exactly the LAST THREE, and the
test passed --- both copies, "... ok" --- inside the stage before them.
Last green 20260815T185708Z, first red 20260816T063330Z, no reboot
between. The three earlier red sweeps failed on unrelated rows. So
"pre-existing on main" holds (F1 at 72da24a reproduces it) but "always
broken" was never established and is now contradicted. D0 gains a first
part: bisect that window. A test that passed fourteen times in this
stage and then failed three times running has a change behind it, and
that is worth more than further reduction --- which has isolated
nothing.
1. Both ledgers still carried the falsified R9 conclusions. This branch
listed --workspace unification and preceding tests as ruled out
while the section above described an interaction; said "five call
sites" immediately before correcting to six; and labelled the
framing revision 2. The held branch was worse: --workspace refuted,
R9 "same binaries", later packages not implicable, cause cumulative
across 37 binaries. All corrected and pushed (5b9abd8). §11 no
longer asserts the held lane is clean; it records a re-verified
checklist, since asserting that prematurely is what went wrong.
2. Manifest now carries complete argv for R7-R9 and F5 --- abbreviations
are not reconstructable invocations. F5 is disambiguated: the
framing cited gate ...-2144707 while the manifest cited ...-2375685,
two distinct real runs. Enumerating them gives F1-F7: the red count
is SEVEN, not five, each with its own log digest. F5 also carries an
extra failing binary the others do not.
3. "Workspace artifact family" conflated Cargo suffix with byte
identity and is withdrawn as a grouping. Demonstrated: F1 in the
main worktree executed the same suffixes -5d9105cb and -d4dae4f0,
but the bytes there are e0578039/00f06aeb versus the panel
worktree's 1b3cc86c/ede0c07d. Each run now records the suffix its
log shows and byte identity as UNKNOWN, since target dirs have been
overwritten and a hash computed today is not the hash that ran.
4. The interaction table is demoted to a description of what was
observed. Revision 3 disclaimed its inputs and then asserted a
finding from them, which cannot both hold. A3 no longer speaks of an
established "R9 paradox" --- there is none to explain, because the
comparison was never made; it requires D0 to recreate it first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Five findings on revision 2, all upheld. The first invalidates its
strongest claim.
1. R9 executed gpu_initial_target_acceptance-91f51d0b and
gpu_invocation_acceptance-6b4b8223; the failing sweeps executed
-5d9105cb and -d4dae4f0. Verified byte-different by sha256. Cargo's
target selection changes the fingerprint, so command shape changes
the executable. "Same binaries" is now "same target names and
order". What the evidence supports is an INTERACTION --- prior
targets alone green (R9), workspace artifacts alone green (R10),
both together red (F1-F5) --- so --workspace selection is not
sufficient by itself and NOT ruled out. The claim that other
packages "cannot be implicated" because their targets run after the
failure is withdrawn: later-selected packages can affect the build
graph and fingerprints before their tests ever run.
2. Both ledgers made internally consistent and portable. This branch's
asserted default-disposition death and then withdrew it further
down; the assertion is gone. panel-mapping-generation still carried
"119 binaries green one red", the >=8s arithmetic, the default-action
claim and the >6s selector --- corrected on its own branch and pushed
at 779a6bd.
3. Provenance is now a pushed document, docs/probe-sigint-evidence.md:
exact command, worktree, HEAD, cleanliness, artifact family, result
and log digest per physical run. R1 and R2 have no preserved log,
and revision 2 double-counted one log as both R2 and R6. Cleanliness
is UNKNOWN for every pre-manifest run and is not inferred. R1-R10
ran in the panel-mapping-generation worktree, not at main. D0 now
precedes every other diagnostic: re-run the matrix at main under a
harness capturing provenance AND the artifact hashes executed.
4. "The probe never blocks indefinitely" narrowed to "the event loop
wakes at least every 50ms". The stdin reader blocks in read_to_end
(:1109) and, once ready, the loop leaves only when stdin closes
(:1212), so the process is not bounded.
5. Launcher call sites: six under --features crdt (:509 :534 :544 :574
:725 :1097, inside #[cfg(feature = "crdt")] mod crdt). The other two
--gpu arguments are under #[cfg(not(...))] and compiled out.
Revision 2 said five while citing eight.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 1 rejected on five findings, all upheld.
1. The >6s selector could not have captured the failure. Both
reproducing binaries finish in ~5.19s INCLUDING the 5s timeout
(:3097, :3131), so the failing launcher lives about 5.1s. This also
falsifies my earlier retraction, which had argued the instance "must
live >=8s" --- so the "mechanism located" claim is NOT refuted by
that argument. It stays unproven for a different reason: the suite
spawns launchers from five call sites, so command line alone cannot
attribute one to this test. Key on the PID the test records.
2. Diagnostics rewritten to DISCRIMINATE blocked delivery, inherited
ignore, and an escaped process group: before-and-after snapshots for
test parent / launcher / probe, per-thread SigBlk from
/proc/<pid>/task/*/status, SigPnd/ShdPnd, and PID/PPID/PGID/SID.
Relatedly, "two processes with default disposition" is withdrawn ---
SIG_IGN is inherited across fork and survives exec, so absence of
handler code says nothing about runtime disposition, and inherited
ignore is the leading hypothesis precisely because the source is
silent. Revision 1 contradicted its own hypothesis.
3. Counts corrected: 119 green result summaries and TWO red binaries,
not "119 binaries green, one red". Reductions are now enumerated
R1-R10 and F1-F5 with command, run count and log each, preserved off
the tmpfs --- /tmp is a tmpfs and these were nearly lost mid-lane.
4. Acceptance contract corrected: A2 now requires three consecutive
green runs on the reviewed fixed head of this branch, not on main,
which is unobtainable before approval and merge; journey step 12(a)
"closing is clean" is named, since revision 1 reasoned from grade
movement which §20 warns against; and A5 is explicitly conditional
on D4, with bet 1 restated as a bet --- the witness uses a wrapper
and headless probe, not the real GUI path.
5. Portability closed: this branch now tracks
githubsucks/gpu-probe-sigint-teardown, and panel-mapping-generation
was pushed to 16cf3a2 so its retraction travels.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
`ctrl_c_on_launcher_group_does_not_reach_spawned_daemon` fails in gate
stage `sweep-crdt` with "child did not exit within 5s". It is
PRE-EXISTING on main --- 72da24a fails it in a clean worktree with its
own target dir --- so while it reds, no branch can present a green
sixteen-stage gate, main included. §5b is held behind this lane.
Framing revision 1, and it proposes NO FIX, because the mechanism is not
known. What it does instead is fix the shape of the problem so the next
attempt is not another guess:
- Ground truth, cited: neither binary handles signals. `run_gpu`
(src/main.rs:324) blocks in `command.status()` with no handler, and
grepping all of pmacs-gpu/src for signal machinery returns nothing.
The probe polls at 50ms. Two processes with default SIGINT
disposition should both die at once --- this deepens the puzzle
rather than explaining it, and the framing says so.
- Ruled out by measurement, with the method for each: load, tmpfs
(tested by experiment, not argument), leaked daemons, inotify,
--workspace feature unification, and any specific preceding test.
- The reduction paradox stated as the problem's real shape: 5/5 in
the full sweep, 0/N in every reduction, including all 37 preceding
targets plus the suite.
- One retracted claim kept as a warning, because it was mine: the
"mechanism located" report described a healthy teardown. The
sampler behind it caught 394 launchers with a 5s maximum lifetime
while the failing instance must live 8s or more.
The first step is diagnostic only: an instrument keyed on the FAILING
instance --- launchers outliving ~6s --- capturing /proc/<pid>/status
signal masks, since SigIgn survives fork and exec while handlers do not.
Acceptance criteria are written now so the fix cannot quietly become
"make the test pass": a demonstrated mechanism with a mutation-tested
witness, sweep-crdt green three consecutive times, the reduction paradox
explained or recorded as unexplained, and no deadline raised or test
skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
CI went red on three Test jobs with the helper's own panic. That is the
loud failure working as designed --- and the design was wrong.
`${#x}` counting characters is a property of the SHELL first and the
locale second. `bash` counts characters under a UTF-8 locale; `dash`
counts bytes under EVERY locale, having no multibyte handling at all.
`/bin/sh` is `bash` on Arch and `dash` on the Ubuntu runners, so
probing `/bin/sh` and naming a locale established nothing there: no
pair could qualify, and the row could not run.
The helper now probes `(shell, locale)` pairs and the row invokes the
gate THROUGH the qualifying shell. `/bin/sh` is tried first so the real
interpreter is used wherever it qualifies. This is not a contrivance to
get green: `#!/bin/sh` resolves to `bash` on Arch and on macOS, which
is precisely where a `${#VAR}` guard would miscount, and it is the
configuration the guard exists for.
Renumbered, because `M-G-8` was taken. Round 3 assigned it to the
canonical-traversal mutation and the ledger never recorded it, so the
locale exercise reusing the ID was a collision. Canonical `M-G-8` is
restored to the ledger; the locale legs are `M-G-9a-c`. Nine total.
9a mutant gate, probed pair -> row fails, boundary row still passes.
Re-run with /bin/sh EXCLUDED, covering the dash/CI fallback
path -> still fails.
9b SAME mutant gate, pair forced byte-counting -> row passes.
The defect reproduced rather than argued.
9c no pair qualifies -> panic naming shells and locales tried
Record corrections review asked for:
- framing said three rounds and revisions 6a-6c; history is rounds 1-4
plus this follow-up, and each round is now named for what it fixed
- framing SS2a claimed `${#var}` counts characters under UTF-8 with no
qualifier --- the same error as the helper's. It now states the shell
dependence and why the guard measures bytes explicitly.
- the helper's prose said every candidate comes from `locale -a` while
the code also tried two hardcoded spellings; the doc comment now
describes what the code does
Gates: all nine green under `env -u TMPDIR`, log 20260813T183646Z.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Review found the byte-versus-character witness asserting something
adjacent to its contract. It set `LC_ALL=C.UTF-8` and assumed the
locale took effect. Locale names beyond `C` and `POSIX` are
implementation-defined, so where that one is absent the shell falls
back to byte semantics --- and then the character-counting mutant
counts bytes too, agrees with the fix, and the row passes while
proving nothing. M-G-6 was killable here and unkillable elsewhere,
which is the same as not having it.
The locale is now chosen by BEHAVIOUR. Candidates come from `locale -a`
so the set reflects what is installed, and each is probed through the
same `/bin/sh` the gate runs under, asking `${#x}` on a two-byte
character and requiring `1`. No qualifying locale is a loud panic
naming what was tried, never a skip: a skip would be indistinguishable
from a pass, which is the failure mode this replaces.
M-G-8 proves the fix in three legs, because the hazard lives in the
environment rather than the code:
8a mutant gate, probed locale -> the row fails, and the
exact-boundary row still passes
8b SAME mutant gate, locale forced to `C` -> the row passes.
The defect reproduced rather than argued.
8c no candidate can qualify -> panic naming the candidates
Also marks framing revision 6 approved and records M-G-8 in the ledger.
Gates: all nine green under `env -u TMPDIR`, log 20260813T182020Z.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
**THE ANCESTOR WALK WAS WRONG TWICE OVER.** `for _anc in $(...)`
word-splits on IFS, so a gate root containing a SPACE was torn into
fragments and the real ancestor never tested --- the check passed on
exactly the path it should reject. And `dirname` walks LEXICAL
ancestry while `detect_project` canonicalizes, so a symlinked root hid
a marker the editor plainly sees. The walk resolves with `pwd -P` first
and iterates a quoted `while`; both shapes are verified by hand
(space-containing root refused, symlinked root refused at its real
path).
**THE 103-BYTE GUARD HAD NO WITNESS AT ALL** --- every other row runs
with a short root, so the guard is silent and a broken one looked
identical. Three rows now aim at it deliberately: boundary rejection
and acceptance, a MULTIBYTE root (each `é` is one character and two
bytes, so it is rejected only if the guard measures bytes), and
**rejection must reap both created areas**, which is the leak the early
trap exists to prevent.
**The `Cargo.toml`-DIRECTORY case was claimed and not covered**, and
the consequence is exactly as review predicted: reverting only the
language-marker arm to `[ -e ]` stayed green. The marker-type row now
drives all three shapes, and `M-G-5` --- that precise revert --- fails
it.
**Prose brought level with the implementation.** The framing, the
handoff and the ledger all said 108; the supported floor is **103
usable bytes**, Darwin's 104-byte array minus its NUL. The ledger also
still said `<pid>`, the superseded 21/30 reserve, and `M-G-1`.
**And the ruling said nested gates "do not pay" the reserve, which is
false and would have licensed exempting them.** They pay it in full;
the short layout merely gives them the headroom to satisfy an unchanged
production guard. Reworded, because the wrong version is the one a
future reader would act on.
**THE btrfs CAUSAL CLAIM IS WITHDRAWN.** The draft argued that a
one-second deadline plus a slower filesystem was a plausible new
mechanism for the fourth `managed_retry` occurrence. It does not
survive inspection: the deadline bounds the connection RETRY loop, not
the socketpair handshake that returned `BrokenPipe`, and the filesystem
work happens before it is armed --- the tempdir is created and never
bound. The environmental change is still recorded, as a CHANGE rather
than a mechanism, so a later occurrence can compare like with like.
Recording a mechanism the code does not support is worse than
recording none: the next occurrence gets measured against a story
instead of the evidence. TMPDIR stays disk-backed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
**THE PROPAGATION WITNESS DID NOT OBSERVE INHERITANCE.** The runner's
`eval` expanded `$TMPDIR` in the PARENT before `sh -c` ever started, so
the child received an already-substituted literal --- and an unexported
`TMPDIR=` would have passed the row unchanged. Single-quoted inside
`sh -c` now, so the CHILD expands it. **M-G-1b keeps the assignment and
removes only `export`: the row fails.** That is the mutation the
previous version could not catch, and the reason to prefer it over
M-G-1's blunter deletion.
**THE RESERVE WAS NOT THE MAXIMUM.**
`/.tmpXXXXXX/directory-target.sock` is 33 bytes
(`tests/gpu_invocation_acceptance.rs`), so paths of 76-78 passed the
30-byte guard and still blew the 108-byte limit during the CRDT sweep.
Reserve is 48 now --- the measured maximum plus ~45% headroom. And the
length is counted in BYTES: `${#var}` counts CHARACTERS under a UTF-8
locale while `sun_path` is byte-limited, so a multibyte path measured
short and passed a check it should fail.
**A MANAGED ROOT IS NOT INHERENTLY MARKER-FREE**, and assuming it was
rebuilt the original defect one directory up: a `.git` in `$HOME`, a
marker above `$HOME/build`, or a contaminated
`PMACS_GATE_TARGET_ROOT`. Placement under a directory the gate owns is
NECESSARY, NOT SUFFICIENT, and the old test proved only placement. The
gate now walks the ancestors and refuses, naming the marker it found.
`PMACS_GATE_ALLOW_ANCESTOR_MARKER` is the documented test-only escape,
beside `PMACS_GATE_TARGET_ROOT` in kind and risk: the behaviour tests
run under a tempdir whose ancestors they do not control, on a machine
whose `/tmp` carries this very marker, and their plans are synthetic so
no markerless fixture exists to re-root. **The check is witnessed by a
row that deliberately does not set it**, and M-G-3 (check removed)
fails that row.
**The guard leaked what it exists to manage.** It created both
temporary areas and exited before the trap was armed, so every
rejection left an AMBIENT and a TMPDIR behind. The trap is installed
first now; verified by rejecting a run and finding neither.
**`tmp/$$` with `mkdir -p` was not fresh.** PIDs are reused, so after a
SIGKILL it silently ADOPTS a leftover directory and the run inherits
another run's fixtures. `mktemp -d` fails rather than reuses.
**Prose corrected to match.** The handoff described
`<target>/gate-tmp/<stamp>-<pid>`; the implementation uses
`<gate-root>/tmp/<mktemp>`. Comments called the shared parent
per-worktree and pruned --- it is neither: `--prune` only considers
directories carrying an ownership marker, so the parent is skipped and
each run removes its own leaf.
**AND THE LANE CLAIMED A FRAMING EXCEPTION THAT DOES NOT EXIST.**
`AGENTS.md` says framing -> approval -> branch -> implement,
unconditionally; "the fix was already recorded as standing" is not an
exemption it grants. `docs/gate-script-framing.md` is amended as
**revision 6, AWAITING APPROVAL** --- a widening of §2's existing
isolation responsibility rather than a new feature, which is why it
amends that document instead of opening another. **This PR must not
merge before that revision is approved.**
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Discharges the standing fix recorded in `docs/agent-handoff.md` §1 and
assigned to this lane. Every gate invocation now gets a fresh,
disk-backed `TMPDIR` at `<gate-root>/tmp/<pid>`, exported once so every
stage and every process they spawn inherits it, reaped by the same exit
trap as the ambient root. **A gate run no longer needs a `TMPDIR=`
override.**
**A CHILD OF `/tmp` WOULD NOT HAVE WORKED**, which is why the obvious
cheaper fix was not taken. The hazard is an ANCESTOR marker: project
detection walks upward, so a fresh subdirectory of `/tmp` inherits
`/tmp`'s ancestors and the same stray `.git`. The directory had to move
somewhere the gate already owns.
**`SUN_LEN` shaped the layout, and the fix's own gate run is what found
it.** A Unix socket path cannot exceed 108 bytes, and the suites bind
sockets INSIDE `TMPDIR`. The first placement --- `$TARGET/gate-tmp/$STAMP-$$`
--- produced a 114-byte socket path and failed SIX daemon and attach
tests with "path must be shorter than SUN_LEN". It hangs off the gate
root (36 bytes) rather than the per-worktree target (60) now, with a
short name: 47 bytes, leaving 61 for fixtures. Running the real gate
rather than only the witnesses is what caught this.
**A startup guard turns that failure class into a named one.** Six
socket failures deep in a suite name a LIMIT, not a CAUSE; the guard
fails immediately with the path, its length, and what to shorten.
**Its reserve is measured, not round, and the first value was wrong in
the more embarrassing direction.** The longest suffix a fixture appends
is `/.tmpXXXXXXX/test.sock`, 21 bytes, so 30 leaves ~40% headroom. An
earlier "generous" 45 FIRED ON THE GATE'S OWN BEHAVIOUR TESTS: they run
the gate inside the gate, so their root sits under the outer run's
TMPDIR and the nested path reaches 71 bytes. A guard that rejects a
legitimate configuration is worse than the failure it prevents, because
it fires on every run instead of a rare one. Verified both directions:
still catches an 87-byte root, silent on the real one and on the nested
tests.
**Two witnesses, each mutation-checked.** `M-G-1` removes the export ->
the propagation row alone; `M-G-2` stops the reaping -> the cleanup row
alone. Propagation is observed in a SPAWNED CHILD --- the self-test's
first step reports its own `$TMPDIR` into its log --- because asserting
the variable inside the script would only prove the script can set a
variable. The cleanup row runs against the self-test, which FAILS on
purpose, so it also pins that the trap fires on the failure path, which
is the path a leak would actually happen on.
One witness of mine needed correcting twice, both times because it
asserted something adjacent to the contract: first `!starts_with("/tmp/")`,
which tested where the FIXTURE put its root and failed on correct code;
then `contains("/gate-tmp/")`, stale after the directory was shortened.
It now asserts the exact parent, `<root>/tmp`.
**Proved against the live hazard:** `/tmp/.git` is still present on this
machine, and `m4_24_bare_string_glob_stays_relative` --- one of the two
tests it reddened --- passes with no override.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Sixteen gates green with --protocol, under an isolated TMPDIR.
The crdt-gated wire suite is verified to have RUN --- 2 tests in the
crdt sweep, 0 in the default one --- rather than assumed, because a
suite that compiles to nothing reports ok and that is the shape this
project has been caught by before.
The mutation inventory names which row each one fails, including
M-1a-6: a mutation that targeted the wrong side of the append boundary
and so reported a sound pin as vacuous.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Docs only, and the first commit on the 1a branch. **No ruling changes.**
**1a needs no framing of its own** ---
`docs/gui-stage1-input-framing.md` already governs every Stage 1 slice,
and A1-A9, the eight Q#S1-9 precedence rules, §8's wire contract and
§11's gates are ruled there. What 1a does need is §2 to be TRUE, and it
was measured at `a994f37`, before 1-pre moved almost every GPU-side
coordinate in it. A framing whose ground truth points at the wrong lines
is how an implementation ends up arguing with the tree.
Re-measured at `4f77491`, and split by kind rather than renumbered
wholesale, because the two are not the same claim:
STILL TRUE `FrontendEvent`'s sixteen variants, `PROTOCOL_VERSION =
23`, IME's zero occurrences, the TUI wheel arms (1-pre
touched only `pmacs-gpu`), the handshake preceding the
window, and `Outbox::enqueue`'s closed-return.
MOVED `window_event` `:2734`/655 lines -> `:4450`/FOUR lines;
`translate_key` `:10975` -> `:12053`; "eight arms, rest
fall to `_`" -> three family decision functions over nine
variants. **1a edits `apply_keyboard` and
`translate_key`, not `window_event`.**
**TWO CLAIMS WERE WRONG AT BOTH ANCHORS, so they are corrections rather
than drift.**
**"`KeyEvent.text` is never read" is false, and was false at `a994f37`
too** (`:2800` there, `main.rs:3251` now): the AltGr rule reads it via
`is_layout_text(key.text.as_deref(), pmods)`. The true claim is
narrower and is the one that matters --- **`text` is never read as the
text a keypress INSERTS**, only as a discriminator separating AltGr from
a command chord.
That is load-bearing for A5 rather than cosmetic. §5's rule 2 already
exempts "printable Ctrl+Alt recognized by the existing AltGr rule", so
the precedence table depends on code the section claimed did not exist.
**1a widens `text` from discriminator to payload, and that is the actual
change of kind** --- which "text is never read" hid, along with the one
place the new payload must leave undisturbed.
**A4's exit site moved without its behaviour changing.** 1-pre routed
the idle-Escape quit through `EventOutcome`, so **A4 edits a branch and
a return type in `apply_keyboard` (`main.rs:3219`) and does not touch
`window_event`**, which holds the crate's only executable
`event_loop.exit()` (`main.rs:4452`). And **`EventOutcome` survives
A4**: a native close still returns `Exit`.
The lane records the v24 serialization constraint and the bump's blast
radius up front --- eight version-sensitive failures across six suites
on the last bump, of which CI showed one, because cargo stops at the
first failing target --- together with the rule for sorting them and the
pin that must never fire, `ADVERTISED_PROTOCOL_VERSION == 20`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The lane recorded this PR while it was under review, because an open PR
without a lane is the #171 defect: it drifted 153 commits while
invisible in the ledger. Review is done, so the lane goes.
**It had to go in a commit rather than "at merge".** GitHub merges the
tree as it stands, so leaving it would have landed the words *PR #238
OPEN* on `main` --- a lane advertising an open PR that closed in the
same commit, which is the exact staleness class this absorption exists
to clear.
Rule 4 takes it **outright** rather than rewriting it: a lane is
rewritten when its ARC survives its PR, and this one describes a docs
pass with nothing left behind it. Every reference to #238 and to the
`docs-absorb-237` branch lived inside the lane, so the deletion is
self-contained and leaves no dangling pointer.
What the lane recorded that outlives it is already elsewhere: the
recovery rehearsal and its floor checks are in "Repository authority",
and the GUI arc's state is in the Stage 1 lane and the handoff anchor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
**The absorption was incomplete, and the two holes were Stage 0's.**
`docs/agent-handoff.md` §6 still called Stage 0 "READY FOR PR (not yet
opened)" and the ledger's Stage 0 lane still handed out branch-recovery
instructions and called itself pre-PR --- for work that merged as #236.
A merged lane that still says "checkout the branch" sends a reader to a
tip that no longer moves. Both rewritten; the recovery lines are removed
rather than updated, because there is nothing left to recover.
**A LANE CANNOT REMOVE ITSELF AT MERGE, and saying so was wrong in a way
that would have shipped.** GitHub merges the tree as it stands, so
"removed at merge" would have landed the words *PR #238 OPEN* on `main`
--- a lane advertising an open PR that closed in the same commit. It
stays while the PR is under review, because an open PR without a lane is
the #171 defect, and **a final pre-merge commit on this branch takes it
out**. That obligation is now written into the lane itself.
**Q#GA5 was recorded backwards.** The P2 row said P2 was "gated by the
GUI arc reaching Stage 4b". The ruling is the reverse: **P2 is blocked
by nothing and may start now**; when the arc reaches Stage 4b, P2 must
already have started, and **no later GUI stage begins** until it has an
approved framing and an opened lane. The gate is on STARTING P2, not on
finishing it, and it binds the GUI arc rather than P2. Corrected in both
the P2 row and the GUI row, which had inherited the same inversion.
**`9a26ac8` was the previous last-EXERCISED anchor, not the previous
declared FLOOR** --- that was `e67ad07`, and the two had drifted apart.
Checking the exercised anchor in the floor's place verifies the wrong
claim, so the check is re-run and re-stated against `e67ad07` and
`d038f71`; `9a26ac8` is retained only under its own correct label.
**1-pre did not ADD the idle-Escape local quit.** The docs said "no
behaviour change" and then called the Escape quit a "1-pre change" and
"the one thing 1-pre added" --- which contradict each other and the
diff. It is **pre-existing behaviour that 1-pre preserved and isolated**
behind an `EventOutcome` return; **1a removes it**. Fixed in both files.
**§1a called the board current at `d038f71` while most of it dates from
2026-08-01**, and P5 still described #217 as a held PR although it had
merged. The board is now labelled **MIXED-ANCHOR and explicitly not
current as a whole**, with the drift named as evidence and a pointer to
`git log --first-parent`; the P5 row is corrected. Refreshing the rest
is a separate pass, deliberately not folded into a GUI absorption.
**The exact-head accounting was wrong --- one commit and 34 lines, when
it was two and 38 --- and the fix is to stop counting.** A count goes
stale on the very next commit, including the one carrying the fix, which
is how it broke in the first place. Both the lane and the PR body now
state the standing property instead: every commit after the gated tree
is markdown-only, checkable with `git diff --name-only 502308d..HEAD`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This file's standing rule is 'when a PR is opened, give it a lane', and
the correction behind that rule --- #171 drifted 153 commits while
invisible here --- applies to a docs PR as much as a code one. Written
with the branch's own commit rather than after the fact, which is also
what keeps a lane from re-conflicting on every other merge.
Records the eight-stage gate result, states the docs-only scope as
something verified with `git diff --name-only` rather than intended,
and notes that the lane is REMOVED at merge: it describes a docs pass,
not an arc, so rule 4 takes it outright instead of rewriting it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Docs only. No `src/`, no `pmacs-gpu/`, no 1a implementation.
**The recovery procedure was RE-RUN, not SHA-swapped, and that is what
licenses the rest of this commit.** Every documented step, from an empty
directory, on git 2.55.0: clone the canonical URL (which names it
`origin`, and `git remote get-url githubsucks` errors with `No such
remote`, exactly as the section anticipates); add the alias; fetch;
`git log -1 --oneline githubsucks/main` -> `d038f71`; confirm BOTH the
old floor `9a26ac8` and the new `d038f71` are ancestors, so advancing
the floor is valid rather than merely plausible; recover with the
three-argument `git worktree add <path> -b <local> githubsucks/<branch>`
form and confirm the upstream is actually set. The documented TRAP was
reproduced too: the two-argument form succeeds and leaves
`## HEAD (no branch)`, so that correction still holds on 2.55.0.
**This discharges a debt rather than inheriting one.** The previous
absorption advanced the base to `e67ad07` WITHOUT re-running the
procedure and recorded that as an outstanding obligation. It is settled
here, by the run above.
Canonical base, recovery floor, the `git log` threshold and the
last-exercised record all move to `d038f71` together --- a floor that
accepts an older commit than the base it declares passes on a tree the
rest of the file does not describe.
**The GUI lane is REWRITTEN, not removed.** Rule 4 removes a lane when
its ARC is done; the arc is Stage 1 as a whole and **five slices
remain**. The lane now records 1-pre as merged, names 1a as next and
NOT STARTED, and keeps the fact that 1a (v24 `TextInput`) and 1e (v25
`OpenTarget`) are the two protocol-bearing slices and are serialized.
Open-PR and branch-recovery wording for 1-pre is gone --- a lane that
still says "recover with `git checkout`" for a merged branch is how a
reader ends up on a stale tip.
The handoff anchor absorbs both merges, and §1 gains the arc bullet the
anchor promises. Its durable facts are the ones that will bind future
slices rather than the diff: a route carries the DECISION and only the
body gives the EFFECT; a harness that re-implements what it tests
witnesses its own copy; winit's `KeyEvent` is non-constructible outside
winit and this will bind every GUI input slice; the sentinel is the
success condition and the timeout only an error ceiling; four test
defects that were all one shape --- witnessing an absence the harness
had manufactured; and `cargo metadata`, not intuition, decides which CI
job runs a member's unit tests.
**One judgement beyond the stated scope, flagged rather than slipped
in.** §1a's board had no GUI-arc row at all, which would have
contradicted the anchor two sections above it. A row is added at the
position `COHERENCE.md` §20 Q#GA5 gives it --- after Priority 1, running
alongside P2-P5, interacting only at the P2 start gate --- and the P2
row now names that gate. The section's own dateline is corrected in the
same breath: the table was written 2026-08-01 and claimed zero open PRs,
so it now says which row is current and which are as-of that date.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
M24 makes the total 24 mutations, not 23. Stated as M1-M24 with the
split named --- twenty-three fail their own rows, and M6 is the P3
exception check, which must STAY GREEN. Writing it as a bare total was
what let the count drift twice: an exception check and a failing
mutation are both 'mutations' in the ledger's usual sense, so the
sentence has to say which is which.
'Gate passes on the final tree' contradicted the very next sentence,
which records that prose and doc comments changed after the run. It now
says final EXECUTABLE tree and explains why the distinction is drawn ---
'the gate was green' and 'the gate was green on exactly this tree' are
different claims, and only the weaker one is true here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two review findings, one of them a real defect.
**THE SENTINEL READ COULD HANG FOREVER.** `read_until_sentinel` blocked
with no bound, so a writer or encoder that regressed after `enqueue`
would WEDGE THE GATE rather than redden it --- and a hang is the worst
failure shape there is, because it looks like slowness until the job is
killed. A 30 s `READ_CEILING` is armed on the daemon socket.
The distinction is kept explicit in the code, because collapsing it is
how this fix would undo the design it protects: **the sentinel remains
the success condition and the ceiling is only an error ceiling.**
Arrival is still decided by the sentinel, so the harness never infers
"nothing was sent" from a duration --- the core-count assumption behind
PR #235's CI red is not reintroduced. The ceiling sits far above any
plausible drain, so reaching it means broken, never busy.
M24 proves it fires rather than trusting it: drop the sentinel enqueue
entirely and the row fails in under a second with a diagnostic naming
both candidate causes and the partial transcript, instead of hanging.
**THE STAGE 1a CONSEQUENCE WAS WRONG IN FOUR PLACES.** Every record
claimed A4 would leave `EventOutcome` with one variant, so the type
should go with the Escape branch. It will not, and it should not.
`LifecycleRoute::Exit` --- a native window close --- returns
`EventOutcome::Exit` too. A4 removes the KEYBOARD producer only, leaving
one `Exit` producer.
And **one producer is not one variant**: the type survives because
`dispatch_window_event` must still distinguish `Continue` from `Exit` on
every event it handles --- nearly all must not exit, and the close must.
What A4 actually changes is `apply_keyboard`'s signature. Corrected in
the `EventOutcome` doc, the Escape-branch comment, the framing and the
ledger; the framing's superseded paragraph is deleted rather than
patched, since it also carried the stale "two `event_loop.exit()`
call sites" count. **There is exactly one executable
`event_loop.exit()`**, in `window_event`.
Also: the sentinel-tag comment claimed four modifier bits and used
three. It now says three, wrapping every eight steps, and why that
suffices --- each sentinel is read before the next is issued, so a tag
only has to differ from its immediate predecessor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Checked with `cargo metadata`, not assumed: `workspace_default_members`
is the root `pmacs` package alone, so the `test` matrix and
`crdt-test` --- both bare `cargo test --all-targets` --- never compile
pmacs-gpu's unit tests. Only `gpu-render` runs them, and it installs
lavapipe, proves the adapter with vulkaninfo, and sets
PMACS_REQUIRE_GPU=1.
That matters because the harness's adapter assert is UNCONDITIONAL
rather than env-gated: it cannot be turned into a quiet ok anywhere, and
a future job that runs -p pmacs-gpu without an adapter fails loudly
instead of silently covering nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 10 argued a route-classification transcript satisfied P2
because a route 'names its local effect'. The wheel falsifies it: a
wheel route carries a delta, and whether that becomes a viewport
update, a panel event, a terminal event or nothing at all depends on
State. The route names the family; only running the body names the
effect. Retracted rather than quietly amended, since it was an argument
this document made and review overturned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
None changes a decision; all four were claims a reader would check and
find false.
**The durable diagnosis command did not run the pair it claimed.**
`m4_24_` is a PREFIX matching **18** tests, so the recorded invocation
would report roughly 16/2 contaminated and 18/0 clean --- not the 0/2
and 2/2 beside it. A reader following it would see a mostly-green run
and conclude the hazard was mis-diagnosed. Replaced with **four literal
`--exact` invocations, one test each**, every one of them executed
before being written down: `running 1 test`, `171 filtered out`,
contaminated `0 passed; 1 failed` panicking at `:5668:5` and `:6615:5`,
clean `1 passed; 0 failed` with no panic. The block now also says to
read the `running N tests` line, pointing at the libtest-filter bullet
two entries below --- which is the trap that produced this defect in the
first place.
**"The diff touches only `pmacs-gpu/src/main.rs`" -> "the whole
EXECUTABLE diff".** The branch changes six files, five under `docs/`.
The structural argument was always about linkage, not file count, but as
written it was simply false and the first `git diff --name-only` would
say so. Fixed in both the ledger and the handoff.
**"a headless test can drive every family"** contradicted the keyboard
exception three paragraphs below it. Now says every family whose event
winit lets a test construct --- all of them except keyboard --- and
points at `route_keyboard` for how far that reaches.
**`[KeyboardRoute::Press]` names a type that does not exist.** It was
renamed to `KeyAction` when the payload moved onto `Route::Keyboard`,
and this doc link was left behind pointing at nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
An open PR gets its number in the ledger at the moment it opens, per the
standing correction from #171 and #215 --- #171 drifted 153 commits while
invisible here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The `/tmp/.git` diagnosis, finished properly.
**Compared on SIGNATURE, not test name**, which is what the ledger's own
CI-triage rule requires. The contaminated leg panics at
`m4_acceptance.rs:5668:5` and `:6615:5` with `.received = ""` ---
byte-identical to the gate red's own signature --- and the clean leg has
ZERO panics. Same binary, same commit, one variable.
**All nine gates pass under an isolated `TMPDIR`**: m4 168/0/3 by the
mandated command, and the 117-target `--workspace --no-fail-fast` sweep
with zero failures anywhere, `m4_acceptance` running all 171 inside it.
**The marker is left in place.** It is foreign, isolating is sufficient,
and deleting it is not this lane's call. Recorded with the constraint
that matters: the isolated `TMPDIR` must be outside `/tmp` AND outside
every git worktree, because a child of `/tmp` is not isolated ---
`/tmp/.git` is still its ancestor.
**One hazard this cost a round, worth the bullet it gets.** A libtest
filter that matches nothing prints `0 passed; 0 failed; N filtered out`
and exits ZERO. This shell is zsh, which does not word-split unquoted
parameter expansions, so `NAMES="a b"; cargo test -- $NAMES` passes one
argument, matches no test, and reports green. The contaminated leg
"passed" that way and briefly looked like the hypothesis collapsing.
The fix is to read the `running N tests` line, which is the only place
a filter's actual reach is stated --- the same family as the
`feature-census` trap already recorded two bullets above, and the same
lesson as "a probe must fail loudly".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai