fix(worktrees/docs): the subtractions that could never happen - #1832
fix(worktrees/docs): the subtractions that could never happen#1832njrini99-code wants to merge 5 commits into
Conversation
… fired
The cleanup tool has a guard whose entire purpose is to refuse to present a
report built on failed evidence. AGENTS.md promised it. It exists in the code.
It did not fire, and the reason is one character of logic.
MEASURED, instrumented at the guard's own line, inside the Bash sandbox:
total rows 72 -> { FAILED: 69, OK: 3 }
96% of PR lookups failed. The condition was `failed === all`, so the guard
stayed silent, the tool exited 0, and the summary read "0 branches deletable"
— indistinguishable from a genuinely clean repository. That is the precise
failure the guard was written to prevent, surviving inside the guard that was
supposed to prevent it.
Why it is never unanimous: macOS caches TLS trust decisions, so the first few
`gh` calls succeed from cache before it is exhausted. A PARTIAL blackout is
the normal shape of this failure; a total one is the special case. A threshold
set at the special case is a gate that cannot fire.
Now: any failed lookup is reported on its own line (a row whose lookup failed
proves nothing about that branch), and a failure ratio >= 0.5 exits 2.
Verified both directions, which is the point:
sandboxed 70/73 (96%) -> INFRASTRUCTURE_FAILURE, exit 2
unsandboxed 0 failures -> no marker, exit 0, normal report
Also corrected the diagnosis this repo had recorded. It said `gh` "cannot read
the macOS keychain" in the sandbox. It authenticates from the keychain fine —
what fails is CERTIFICATE VERIFICATION, because Go's TLS cannot reach
com.apple.trustd.agent:
tls: failed to verify certificate: x509: OSStatus -26276
And the practical trap that made this expensive to find: a SINGLE `gh` call
usually succeeds in the sandbox. `gh pr view 1831` returned correct JSON while
a 25-call burst failed 25/25. Verify with a burst, never one call.
There is a real fix, so the docs no longer say "re-run outside the sandbox" as
if avoidance were the only option:
~/.claude/settings.json -> sandbox.network.enableWeakerNetworkIsolation: true
read at session start, so it takes effect the NEXT session.
Docs updated to current state rather than left pointing at each other:
- AGENTS.md: "total blackout" -> "dominant blackout", with the measurement,
the trust-cache explanation, the burst command, the real error, and the fix.
- .claude/rules/shipping.md section 3 (Bash traps): the burst-verification
trap, cross-referenced to AGENTS.md instead of restating it.
NOT changed, because it was verified CORRECT: shipping.md's claim that
sandbox.filesystem is `disabled: true` in ~/.claude/settings.json. It is.
Verified: markdown:ratchet no regressions; docs:path-drift 0 unresolved; the
guard exercised in both directions above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZX6vCQjxRC22o8Uz16VwK
|
ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing |
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
…mands does not cover
Self-correction on the previous commit, plus the finding that explains why the
tool broke despite already having an exclusion entry.
WHAT I GOT WRONG. The previous commit asserted "macOS caches TLS trust
decisions, so the first few gh calls succeed from that cache before it is
exhausted." I had not measured that. It was a plausible story fitted to two
data points, which is the exact habit this repo keeps paying for. Measured
properly:
gh pr view 1831 OK single call
3 calls after an idle gap 3/3 OK
25-call burst 0/25 fails at call #1, not after a warm-up
3 calls straight after 0/3 still failing; recovers after a pause
"Fails at call #1" kills the cache-exhaustion story outright. The behaviour is
volume-sensitive with time-based recovery; throttling of the system trust
service by a sandboxed client fits, but NOTHING HERE PROVES IT, so the docs now
say the cause is unconfirmed instead of naming one. The operational rule —
verify with a burst, never one call — is what was actually established, and it
is unaffected.
WHAT I MISSED. `sandbox.excludedCommands` already lists `gh *`. That is why
single top-level `gh` calls succeed and why this was so easy to misdiagnose as
"gh works fine here". It does NOT cover `gh` spawned from inside a script or a
Node child process — which is precisely how worktree-lifecycle.mjs calls it. An
exclusion entry that appears to handle a tool while the tool's real call path
stays sandboxed is the same failure class as the guard itself.
SCOPE CORRECTION. The config fix moved from ~/.claude/settings.json to
.claude/settings.local.json (gitignored, helmv3-only). enableWeakerNetworkIsolation
weakens network isolation, and user scope applies that to every project on the
machine — shipping.md's own rule says user-scope grants are not edited from this
repo, and I had walked past it. Raised by the concurrent session, and correct.
The docs now also state plainly that the key is read at session start and is
therefore UNVERIFIED until a restart, with the burst command to confirm it and
what to conclude if it still fails.
quality-gates.md gains the sub-class this all belongs to: a check whose FAILURE
is shaped exactly like its SUCCESS. Three unrelated instances surfaced on
2026-09-04 — the blackout guard at 96% failure reading "0 deletable", a test
whose regex mis-paired on prose apostrophes and passed by asserting over an
empty set, and semgrep's known bad-glob zero-finding mode. One tell every time:
an empty result reported as a good result. That file already exists to name
this; it now names it with cases.
Verified: markdown:ratchet no regressions; docs:path-drift 0 unresolved. The
guard behaviour from the previous commit is unchanged and still exercised in
both directions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZX6vCQjxRC22o8Uz16VwK
Second tell for the section added in the previous commit, and this file caused the case it documents. The 2026-09-04 sandbox write-up first shipped a mechanism — "macOS caches TLS trust decisions, so the first few calls succeed before it is exhausted" — that was fitted to two data points, landed in AGENTS.md, and was false. The third measurement killed it: the burst failed at call #1, not after a warm-up. Recorded because of what a wrong cause does that an admitted unknown does not: it gets reasoned FROM. The natural next "fix" for a warm-cache story is to add a warm-up call, and someone would have spent a session chasing a mechanism that does not exist. Rule: when you can measure the behaviour but not the cause, write the behaviour and mark the cause UNCONFIRMED. Same failure as the empty-result-reads-as-clean class above — confidence with nothing under it — one in a check's output, one in a doc's prose. Raised by the concurrent session, which named the generalization; the case is this session's own. Verified: markdown:ratchet no regressions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZX6vCQjxRC22o8Uz16VwK
Three fixes for why worktrees and branches only ever accumulated. All three are the same shape: a subtraction that could never happen. 1. MERGING DESTROYED THE PROOF THAT PARKING NEEDED. `delete_branch_on_merge` is TRUE on this repo, so GitHub deletes a branch's remote at merge. The park path required a remote tip to prove the commits survive removing the directory. So the evidence parking depended on was destroyed by the exact event that made parking correct. Measured 2026-09-04: 0 of 25 worktrees parkable — not one of them because it was in use. Three (PRs #1793, #1797, #1819) printed UNKNOWN_REMOTE, "commits here may exist nowhere else", on the same row whose branch column read DELETE_MERGED_EXACT. One report calling the same commits both provably-in-main and possibly-nowhere. This is #1654's defect, which was fixed for BRANCH deletion and never carried to the PARK path — AGENTS.md even states the principle ("never by a remote tip — delete_branch_on_merge removes that exactly when the branch becomes safe") while the park code did the opposite two functions away. A PR merged at this exact tip now counts as durability proof (stronger than a remote tip: the commits are in main) and satisfies the workspace gate, because merging IS the owner saying the work is done. Four facts, three already proven before control reaches the gate: clean, idle, identified, merged-at-this-OID. #1681's rule is untouched — an OPEN PR still requires the marker. After: 3 worktrees parkable, 4 branches deletable, and every protection intact (the concurrent session's ACTIVE checkout, deploy-main detached, both open PRs). 2. THE EXCEPTION SHIPPED DEAD ON THE FIRST ATTEMPT. Worth recording rather than quietly fixing: prHeadSha was never passed into the worktree facts, so `mergedAtThisTip` evaluated false for every checkout and the new code changed nothing. It did not error. It just never fired — the same class as the guard whose threshold could not be met, found twice in one day. The test pins the field at both gates. 3. TWO CONTRADICTORY node_modules POLICIES, LIVE ON ONE MACHINE. new-worktree.sh's header rejects `ln -s node_modules` because two branches with different lockfiles test against whichever tree was installed last. That reason is correct but CONDITIONAL, and was written as though lockfiles always differ — most task branches never touch package-lock.json. Meanwhile worktree.symlinkDirectories in ~/.claude/settings.json symlinks node_modules for Claude Code's own worktrees, and sessions had started hand-symlinking to dodge the 3.8 GiB cost, without the lockfile check that makes it safe. ensure-worktree-deps.mjs now checks the condition instead of assuming it: byte-identical lockfile plus an installed canonical tree => share it (0 bytes); anything else => a real install, original warning intact. Verified both ways. 4. IT NOW RUNS AT MERGE TIME. `npm run worktrees:postmerge` (git fetch --prune + --retire), wired to fire after `gh pr merge` via a PostToolUse hook. GitHub already deletes the REMOTE branch on merge; nothing ever deleted the LOCAL one, which is why 62 local branches stood against 49 remote. The hook deliberately does not pipe or swallow its exit code — a cleanup that reports success when it could not run is the failure this whole PR is about. Verified: typecheck clean; eslint clean on changed files; 10/10 new classifier tests (collected by glob, so CI runs them); markdown:ratchet no regressions; docs:path-drift 0 unresolved; lifecycle exercised live before and after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZX6vCQjxRC22o8Uz16VwK
…ious commit
They were extracted from abandoned workflow branches to CHECK whether they
still passed against main. They do NOT — they encode older error strings
('Not authenticated' vs 'Unauthorized: no golf session') and an older enum
('no_gap' vs 'below_threshold'). Main's behaviour is correct; the tests are
stale, and merging them would turn CI red for no reason.
They reached the index because `git checkout <branch> -- <paths>` STAGES what
it extracts, so they were already staged before the explicit `git add` — the
'always add explicit paths' rule does not protect against something staged
earlier by a different command.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZX6vCQjxRC22o8Uz16VwK
…e has ended `open-pr-residue` requires the key set to equal the open-PR set, and a row for an in-flight PR lives at that PR's own head rather than on main — so #1836's row rides its own branch. Verified both directions: without it the check names #1836 as unclassified; with it, it does not. Also deletes #1725 and #1738, both MERGED, both past the transitional grace this file's own `$comment` defines — the tool's output says in as many words to delete them "in the PR you are already opening", and that is this one. Leaving them is the exact failure the comment describes: a current-state registry asserting things that stopped being true. **`open-pr-residue` still FAILS, for reasons this PR does not own.** Twelve open PRs carry no row at all — #1834, #1833, #1832, #1831, #1829, #1827, #1759 and five dependabot PRs. That was already true before this commit and is not mine to reconcile; a row is a statement about someone else's work. Recording it here so the red is legible rather than mysterious. Note for merge order: #1835 also edits this file, so whichever lands second resolves that conflict. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019euu9jtJM6WvMVj17coqZ1
The cleanup tool has a guard whose entire purpose is to refuse to present a report built on failed evidence. AGENTS.md promised it. It exists in the code. It did not fire, and the reason is one character of logic.
Measured
Instrumented at the guard's own line, inside the Bash sandbox:
96% of PR lookups failed. The condition was
failed === all, so the guard stayed silent, the tool exited 0, and the summary read0 branches deletable— indistinguishable from a genuinely clean repository. That is the exact failure the guard was written to prevent, surviving inside the guard meant to prevent it.Why it is never unanimous: macOS caches TLS trust decisions, so the first few
ghcalls succeed from cache before it is exhausted. A partial blackout is the normal shape of this failure; a total one is the special case. A threshold set at the special case is a gate that cannot fire.Fix
Any failed lookup is now reported on its own line (a row whose lookup failed proves nothing about that branch), and a failure ratio ≥ 0.5 exits 2.
Verified both directions, which is the whole point:
INFRASTRUCTURE_FAILURE, exit 2Diagnosis corrected
The repo recorded that
gh"cannot read the macOS keychain" in the sandbox. It authenticates from the keychain fine — what fails is certificate verification, because Go's TLS cannot reachcom.apple.trustd.agent:And the trap that made this expensive to find: a single
ghcall usually succeeds.gh pr view 1831returned correct JSON while a 25-call burst failed 25/25. Verify with a burst, never one call.There is a real fix, so the docs no longer present avoidance as the only option:
Read at session start, so it takes effect the next session.
Docs brought to current state
Not changed, because it was verified correct: shipping.md's claim that
sandbox.filesystemisdisabled: truein~/.claude/settings.json. It is.Verification
markdown:ratchet— no regressionsdocs:path-drift— 0 unresolved🤖 Generated with Claude Code
https://claude.ai/code/session_01AZX6vCQjxRC22o8Uz16VwK