Commit Graph

7 Commits

Author SHA1 Message Date
Levi Neuwirth 30e976ae1e
docs,test: record the fourth managed_retry occurrence, and unblock the nested suite
**The gate's own run reproduced a REGISTERED signature**, and it is
recorded as a fourth occurrence rather than waved through: same
selector, same `gpu`-step flavor, all three required fragments verified
against the durable log. Three isolated re-runs were green, which this
file's rule says establishes intermittence only.

**This lane is code-neutral for `pmacs-gpu` but NOT
environment-neutral**, and that distinction is the entry's point.
Occurrence 3 excluded "the added GPU test is the mechanism"; this
occurrence adds nothing to that binary at all, which corroborates the
exclusion independently. But the lane moves `TMPDIR` off `/tmp`, taking
every `tempfile::tempdir()` in the run from **tmpfs to btrfs** --- and
the failing test runs a handshake against a **one-second deadline**. A
slower filesystem under a timing-bounded test is a plausible mechanism
that did not exist in occurrences 1-3. Booking this as "the usual
flake" when the observing lane changed the conditions the flake is
sensitive to is exactly the reasoning this registry exists to prevent.

Also: the suite's own roots move to a short base. Rooting them under
the ambient `TMPDIR` put a NESTED gate's TMPDIR near 70 bytes, which
legitimately tripped its own SUN_LEN guard --- the suite failing on a
configuration it created rather than on the behaviour under test. And
the marker row's `.then(..).unwrap_or_else(..)` chain is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-13 16:43:05 +02:00
Levi Neuwirth 1bd52b7f0d
fix(gate): review round 1 --- the propagation row proved nothing, and two guards were wrong
**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
2026-08-13 16:34:21 +02:00
Levi Neuwirth d84df23aa7
fix(gate): isolate TMPDIR per invocation
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
2026-08-13 15:55:19 +02:00
Levi Neuwirth 677fd25bcd
test(gate): assert the real plan's (name, command) pair, not one half
The lane exists to guarantee two things --- that the crdt sweep is
preceded by the build producing its binary, and that a build failure is
attributed to `build-crdt` rather than to `sweep-crdt`. It shipped with
neither guaranteed, because NEITHER WITNESS COULD SEE A NAME.

  --print-plan renders `emit_plan | cut -f2-`, so the ordering test
  compared commands and never saw the names beside them.

  --self-test hardcodes the string `build-crdt` inside its OWN synthetic
  plan, so it proves things about the runner and nothing about the real
  emitter.

Review demonstrated the consequence: renaming the real build step to
`sweep-crdt` left both tests passing --- a plan that would report a
build failure under the sweep's name, sitting green, which is exactly
the misattribution the separate step exists to prevent.

--print-plan-named prints emit_plan VERBATIM: the same `name<TAB>command`
text the runner reads back from PLAN_FILE. The new assertion compares
WHOLE LINES against it, so name and command are pinned together and a
rename of either step fails. The sweep's own pair is asserted too ---
asserting only the build's name leaves the identical hole open in the
other direction.

WHY A RENDERING AND NOT A SEAM. PLAN_FILE stays uninjectable: a test
that supplied the runner's plan would turn its `eval` into a general
command executor, the same class of defect this script's own review
caught in --acceptance and fixed with a parse-time refusal. Re-deriving
the plan test-side would be a second implementation of the thing under
test, which is the failure being repaired one level up. A distinct mode
rather than a --with-names modifier leaves --print-plan's contract ---
runnable lines --- exactly as it was, and defines no flag combination
that has no meaning.

--self-test STAYS. It witnesses the runner: failure naming, the FAILED:
list, log paths, non-zero exit, and continuation past a failure via the
sentinel. That is a different thing from attributing the real step, and
what it may no longer do is stand in for it.

A second test pins that the two renderings are one plan --- the stripped
one is the named one minus its names --- so a later edit giving either
mode its own text is caught rather than leaving an assertion on a name
the runner never uses. It also pins the `name<TAB>command` shape the
runner's `IFS=<tab> read` depends on.

Both new tests stay on the no-gates paths, so the suite stays cheap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 18:02:36 +02:00
Levi Neuwirth 053866c7f6
test(gate): name the ordering failure instead of panicking on a range
Found by mutation-testing the assertion rather than by reading it.
Emitting `build-crdt` AFTER `sweep-crdt` does fail the test --- so the
position criterion was never vacuous --- but it failed by panicking
inside the slice with

    begin > end (427 > 282) when slicing `cargo fmt --check ...`

which names neither step and reads as a bug in the test. A gate test
whose failure has to be decoded is a gate test nobody trusts, and this
suite exists precisely to be trustworthy about the gate.

An explicit ordering assertion ahead of the slice says what is wrong:
the build must run before the sweep, because a sweep that builds its
own precondition afterwards has already failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 16:42:09 +02:00
Levi Neuwirth f55ce54627
fix(gate): build pmacs-gpu before the crdt sweep, and witness the runner
`scripts/gate --protocol` emitted `sweep-crdt` with no build step. The
crdt sweep spawns `pmacs-gpu` as a process, and nothing in a
`cargo test` run produces that binary --- `pmacs-gpu` has no `tests/`
directory, so cargo never uplifts its bin to `debug/pmacs-gpu`. On a
cold target directory the sweep therefore fails twelve
`gpu_invocation_acceptance::crdt::*` tests on "build pmacs-gpu before
this acceptance suite".

The hazard was never the red gate. Before per-worktree target
directories (#225) every worktree shared one, which nearly always
already held the binary, so the precondition was satisfied BY ACCIDENT
for the whole life of that arrangement --- a GREEN `--protocol` run
whose crdt sweep was decided by the state of the build directory rather
than by the diff.

Q#GR-1 SETTLED BY OBSERVATION, not by reading. On a disposable target
directory with `debug/pmacs-gpu` asserted ABSENT before each run
(recorded, not assumed), each sweep run alone from the same cold state:

  default  cargo test --workspace --no-fail-fast -- --skip basedpyright
           exit 0, 114 test targets green, and `debug/pmacs-gpu` was
           STILL ABSENT afterwards --- the default sweep never builds it
           and never needs it.
  crdt     cargo test --workspace --features crdt --no-fail-fast
           -- --skip basedpyright
           exit 101, exactly twelve failures, all
           `gpu_invocation_acceptance::crdt::*`, matching the signature
           handoff section 5 recorded.

So the step is conditional on `--protocol`, which the framing voted for
on an inference this run confirms rather than assumes.

Also observed, and worse than the twelve: `a54_real_daemon_real_pty_and_
headless_gpu_render_one_panel_hosted_terminal` reported `ok` in that
same cold crdt sweep. Its only path that does not spawn `pmacs-gpu` is
its skip branch, so a test whose whole purpose is real wgpu rendering
passed having rendered nothing. The missing build does not only fail
twelve tests --- it silently voids coverage in tests that report green.

A NAMED STEP, NOT A FOLDED COMMAND. `cargo build ... && cargo test ...`
would report a BUILD failure under the name `sweep-crdt`, a wrong
attribution in the one place this script exists to be trustworthy
about.

`--self-test` is how that attribution is witnessed at all. The existing
suite drives only no-gates paths, so plan assertions can prove a step's
name and order and NOTHING about what the runner does when a step
fails. The mode runs a HARDCODED three-line synthetic plan through the
real runner: a passing step, a failing one named `build-crdt`, and a
passing SENTINEL after it. The sentinel is load-bearing --- with the
failure last, an aborting runner and a continuing one produce identical
output, so the witness would pass on a runner doing the opposite of the
stated policy.

The plan is a literal inside the script. Making `PLAN_FILE` injectable
would work and would turn the runner's `eval` into a general command
executor --- the same defect this script's own review caught in
`--acceptance` and fixed with a refusal at parse time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-09 16:39:14 +02:00
Levi Neuwirth 4bc55e8dd2
build: scripts/gate — a target dir per worktree, and one gate suite (#225)
* build: scripts/gate — a target dir per worktree, and one gate suite

Parallel worktrees do not work on this machine, and the reason is one
exported variable: every checkout builds into one CARGO_TARGET_DIR, and
cargo takes an EXCLUSIVE LOCK on it. Two lanes building at once do not
run in parallel — the second blocks — and they invalidate each other's
artifacts, so alternating between them recompiles from scratch. Parallel
development under that arrangement is slower than serial.

MEASURED, BECAUSE THE FIRST PLAN WAS WRONG. The shared directory is
285G, which drove a proposal to add sccache so per-worktree directories
would not lose artifact sharing. That number is years of accumulation
across TWO projects (pmacs and levcs share it). Measured directly: a
cold `cargo test --workspace --no-run` is 80s and 19G. And sccache
across two target directories hits 50% on C/C++ and **0.00% on Rust** —
rlibs embed their target-dir path, so dependency artifacts are not
bit-identical between directories and `--extern` hashes cascade into
misses. There is no sharing worth buying back. sccache stays configured
and earns its keep on C/C++; it is not what makes parallel lanes work.

The script also owns the FIXED gates, because a procedure living only in
prose gets executed differently each time — twice in the session that
motivated this:

  - a sweep run with `--tests` instead of `--workspace`, silently
    dropping pmacs_protocol and pmacs_gpu, including protocol tests that
    same lane had just written;
  - a sweep piped through `grep` before anyone read it, so an
    intermittent red could not be matched against ci-red-signatures —
    a row needs its fragments. That is registry note U2, and then U3
    when it happened AGAIN.

Hence durable per-gate logs with the sweep paths printed. The remedy is
real: this lane's own run diagnosed its failures from the log without
re-running anything.

WHAT THE SCRIPT IS NOT AUTHORITATIVE FOR. Handoff §3 keeps policy and
keeps CHOOSING the touched acceptance suites, which arrive only via
`--acceptance`. No script can infer those from a working tree, and one
that guessed would report coverage it does not have.

THREE HAZARDS SPECIFIED RATHER THAN LEFT TO CHANCE:

  - `cmd | tee log` reports TEE's status, so a failing gate would exit 0
    and the suite would read green. `pipefail` is not POSIX.
  - `cmd > log; rc=$?` never reaches the assignment under `set -eu`
    (which scripts/bite already uses) — the shell exits at the failing
    command, so nothing prints which gate failed or where its log is,
    destroying the point of capturing it. The runner is therefore an
    `if` condition, the only `set -e` exemption.
  - CARGO_TARGET_DIR (env) OVERRIDES build.target-dir in config.toml, so
    a per-worktree config file silently does nothing. Only a
    per-invocation value beats it.

Pruning is dry-run by default, `--force` to delete, and refuses any
directory without a `.pmacs-gate-target` marker. "Live" means a git
worktree record carrying NO `prunable` line — git keeps listing a
worktree whose directory was deleted without `git worktree remove`, and
treating listed as live would make exactly the reclaimable directories
permanently ineligible.

ONE HONEST FINDING FROM MUTATION TESTING. Three mutations came back
vacuous, and all three are redundant defences rather than test holes:
git already returns resolved physical paths from both
`rev-parse --show-toplevel` and `worktree list --porcelain`, so canon()
is belt-and-braces; and the prune path guards the marker twice. Recorded
in the script and the tests so a later reader does not mistake a
"vacuous" result for a gap — or delete a defence because a test did not
notice.

VERIFICATION. 11 acceptance tests over the no-gates paths (running the
script for real inside the suite would recurse), each pointed at a
tempdir via PMACS_GATE_TARGET_ROOT so the real managed root is
unreachable — a prune bug is unrecoverable. Mutation-tested: `--tests`
in the sweep, an unconditional CRDT sweep, and pruning on a dry run all
fail their intended test.

Observed in a real run, which is how the framing said to confirm the
parts a test cannot: the failed-gate names and log paths print, the
ambient directory is created and reaped by the exit trap, and every log
appears. The run exits non-zero because of R8 — the pre-existing,
merge-base-confirmed listview failure — which means `scripts/gate`
cannot go green on this machine until R8 is diagnosed. That is a
property of the tree, not of this change.

Framing: docs/gate-script-framing.md (revision 4, approved).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* fix(gate): two ways the script could do harm, and four smaller defects

Review round 1 on #225. Neither blocking finding was a design gap ---
both were the implementation failing to honour its own framing, which
is the case a framing document cannot prevent by itself.

PRUNE COULD DELETE EVERY MANAGED DIRECTORY. §2.6 requires the live
worktree set to be ESTABLISHED. The code piped `git worktree list`
straight into awk and the caller masked the result with `|| true`, so
running from outside any repository produced an EMPTY live set --- and
an empty live set means "every managed directory is an orphan", so
`--prune --force` would have deleted all of them, live lanes' artifacts
included. The failure mode was silent and total.

Two refusals now, and they are deliberately redundant: not inside a
worktree, and the enumeration itself failing. `live_worktrees` captures
git's output and returns non-zero rather than emitting nothing, so
"I cannot tell what is live" is unrepresentable as "nothing is live".
An empty porcelain listing counts as failure too --- a repository always
has at least its own worktree.

--ACCEPTANCE WAS SHELL-INJECTABLE. The name is interpolated into a
command the runner evaluates, and nothing validated it, so
`--acceptance 'x; rm -rf ~'` would have run. Now an allowlist of what a
cargo test target can actually be named --- letters, digits, underscore,
hyphen --- refused at parse time, before any gate. Rejection rather than
escaping: there is no legitimate suite name that needs quoting.

FOUR SMALLER ONES:

  - Log directories carried a whole-second timestamp, so two runs in the
    same worktree within one second shared one and could overwrite each
    other's evidence --- reintroducing U2/U3 through a naming choice.
    The PID is now part of the name.
  - The ownership marker is DOCUMENTED as one line, so it is enforced as
    one line instead of read head-first. Acting on the first line of a
    file we did not understand is how a corrupted marker authorises a
    deletion.
  - The `prunable` test returned green when `git worktree add` failed,
    so the only coverage of that rule could silently never run. It now
    fails loudly.
  - Its cleanup ran after the assertions, so a panicking assertion would
    have left the real repository carrying a stale worktree record. Now
    a `Drop` guard.

MUTATION TESTING, HONESTLY REPORTED. The injection and marker fixes bite
individually. The two prune guards do NOT --- each alone satisfies the
outside-repo test, so mutating one at a time reads as vacuous. Removing
BOTH fails the test, which is what establishes that the test detects the
unsafe state rather than being blind to it. Recorded in the test so a
later reader does not delete one guard on the grounds that nothing
noticed.

ALSO: handoff §3's ambient-root caveat still said "until the
ambient-root isolation lane lands". #206 merged; the five variables are
now belt-and-braces for external and integration paths, and `scripts/gate`
sets them regardless.

R8 PROMOTED. `docs/ci-red-signatures.md` gains the reason it stops being
a catalogued curiosity: with the gate suite reduced to one command, R8
makes that command exit non-zero on a clean tree EVERY TIME, and a gate
that is always red is a gate nobody reads. `docs/active-work.md` gains a
lane. It is still not a regression from #223 or #225 --- the merge-base
control says so --- and the lane's first job is diagnosis, because a
change that made the assertion pass without explaining the prefix strip
would convert a visible failure into an invisible one.

15 acceptance tests. Observed run re-confirmed: failed gates named with
log paths, ambient directory created and reaped, distinct log directory,
exit 1 from R8 alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* docs: the #225 lane, and R8 diagnosed to a stray /tmp/.git

TWO LEDGER GAPS, both found by review.

and — the part that matters — an explicit GATE STATUS: NOT GREEN
section. `scripts/gate` exits 1 on this branch and on a clean `main`
because R8 fails m4_acceptance and therefore the sweep. That is a merge
blocker under the standing rule, and #225 is the worst possible lane to
grant a silent exception to: it is the lane that makes the gate suite
authoritative, and a tool shipping with its own gate red teaches the
opposite of what it exists to teach.

The lane also records that it was written after the PR existed, again,
because review asked again. Two lanes in a row now. The correction from
only evidence of that.

R8 DIAGNOSED, and the `TMPDIR` hypothesis was right:

  1. `display_path` (builtin/runtime/lsp.lua:2397) shortens a location
     against the DETECTED PROJECT ROOT before rendering it.
  2. `project.detect` walks UPWARD for a marker; from
     /tmp/.tmpXXXX/r.rs it reaches /tmp.
  3. This machine has a stray `/tmp/.git` — an EMPTY DIRECTORY, not a
     repository. The `.git` marker is directory-only, so an empty
     directory still matches.
  4. Root resolves to /tmp, the prefix is stripped, and the rendered row
     is exactly the observed `.tmpXXXXXX/r.rs:12:3`.

Controlled, not inferred: the same test with TMPDIR outside /tmp PASSES.

THE CODEBASE ANTICIPATED THIS BY NAME. src/project.rs:208 documents
`detect_project_within(start, markers, stop_root)` as existing "so a
stray marker in a temp-dir's ancestor (e.g. a developer's /tmp/.git)
can't leak into a fixture that lives below it." The mechanism exists;
this fixture does not use it.

So the row splits, and the halves need different fixes. The failure is
ENVIRONMENTAL — nothing about pmacs is wrong when a real project root
sits above a file, that is the feature, and removing /tmp/.git makes the
gate green immediately. The fixture being ENVIRONMENT-DEPENDENT is a
real defect, and bounding its detection is what retires the row.

PROVENANCE UNRESOLVED, and I am not going to assume in my own favour:
/tmp/.git is dated 2026-08-07 23:17, inside this session's window, and
may have been created by this session's own work — a stray git
invocation from /tmp would do it. The earlier merge-base control stays
valid as "this tree has it" but says nothing about WHEN the environment
acquired the marker, so "pre-existing" must not be read as
"long-standing".

Nothing deleted: /tmp/.git is outside the repository and I cannot
confirm I created it, so removing it is the user's call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

* docs: rebase onto the R8 fix; scripts/gate now exits 0

#226 (`dcb852e`) retired R8 by bounding the LSP fixture's project
detection. This branch rebases onto it, and the thing that was blocked
is now demonstrable: **`scripts/gate` exits 0** --- all nine gates green
in one command, the first time the tool has passed the suite it exists
to run. That is #225's own acceptance criterion, and it could not even
be stated while the script did not exist on `main`.

REBASE RESOLUTION, per the standing rule that #226's R8 documentation is
authoritative. Every conflict was in R8 text this branch wrote while the
row was still an open investigation:

  - two in `docs/ci-red-signatures.md`, both resolved to #226's retired
    row with this branch's pre-fix copy dropped;
  - the framing-doc pair --- e71e1bd added `docs/r8-fixture-boundary-
    framing.md`, 7cfba73 removed it --- both SKIPPED. They are net-zero
    here and `main` owns that file authoritatively; replaying the second
    would have deleted `main`'s copy, which is the one failure mode a
    mechanical "resolve each conflict in turn" would have walked into.

TWO STALE LANES REMOVED. This branch's "R8 --- NEEDS A LANE"
investigation block describes a diagnosis that has since happened and a
fix that has since landed. And #226's own lane arrived through the
rebase still saying "OPEN, HELD FOR REVIEW"; Rule 4 retires it now that
it has merged, its durable facts already being in the retired registry
row and the handoff section 6 census. Leaving either would have left the
ledger asserting that a merged fix was still an open investigation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 09:43:33 +00:00