Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5689,7 +5689,7 @@ and `enforce_admins` governs **protected branches**. Re-enabling it would refuse

## 1057. Rule 3d has no occupancy signal, so it cannot tell an abandoned worktree from a live one

> 🔢 **Filed 2026-08-05 — not started.** Value **4/10** · Difficulty **5/10** · _fill-in_. #1041 fixed rule 3d's *false* claim; this is the *missing* one underneath it. The rule refuses every governed worktree that is not the caller's own, because it has no way to ask whether anyone is actually using it. The refusal is right by default — a needless refusal costs a message, a wrong allow deletes a live session's work — but it is unactionable for a caller cleaning up a worktree it created itself, who must escalate to a human for something it was entitled to do.
> 🚧 **PARTLY SHIPPED 2026-08-06 — the REMEDY slice is in; the occupancy body is NOT, and this item stays open for it.** Value **4/10** · Difficulty **5/10** · _fill-in_. What shipped is the half nobody had noticed: rule 3d refused correctly and then handed the caller a command that **throws**. `remove.ps1 -Name` resolves only to `<repo-parent>/<repo-leaf>-<name>` — `new.ps1:79-81` *asserts* that shape, so the sibling family is the only one it can produce — and `prune-merged.ps1` excludes anything with a `.claude/worktrees/` path segment **outright**, by its own header, with `-Name` unable to reach them either. Census on this clone: **45 sibling worktrees, 8 Claude-managed, 4 other — and all six live sessions sat in the 8**, so the remedy failed for the population that actually reaches the rule. Verified against the **installed** gate, not only the repo copy. The deny now branches on worktree family and, for anything the two scripts cannot serve, prints a literal `git -C "<primary>" worktree remove "<path>"` plus a line saying why `prune-merged.ps1` does not apply; the sibling family keeps `prune-merged.ps1`, which is dry-run by default and consults occupancy, so the fix is not "stop naming the scripts" — a test pins that. Writing the test enlarged the defect: the own-tree branch printed the literal placeholder `-Name <directory-name>` for **both** families, so it named the tool correctly and still could not be pasted; the gate has the resolved path and now uses it. **This is a remedy-text change only** — which worktrees rule 3d refuses is untouched, and nothing security-relevant reads the classification, which is what makes a misclassification cheap here and expensive in rule 3c. **Same defect as #1032, one rule over** (there, rule 3b printed a `new.ps1` command `new.ps1` refuses to run). **Still open, and unchanged below:** rule 3d has no occupancy or authorship signal, so an *unoccupied* worktree is still refused rather than allowed — the three structural options, the measured 0-of-24 occupancy result and the fail-closed constraint all stand as written. See also **#1064**, a measured rule 3d fail-open confirmed 2026-08-06, deliberately not touched here. Original filing follows. #1041 fixed rule 3d's *false* claim; this is the *missing* one underneath it. The rule refuses every governed worktree that is not the caller's own, because it has no way to ask whether anyone is actually using it. The refusal is right by default — a needless refusal costs a message, a wrong allow deletes a live session's work — but it is unactionable for a caller cleaning up a worktree it created itself, who must escalate to a human for something it was entitled to do.

**Cluster:** Session-drift controls / refusal accuracy. **Priority:** P3. **Verdict:** build (medium). **Severity:** no data loss, and no security effect — the rule fails *closed*. The cost is a correct refusal the reader cannot act on, and the standing invitation to route around a guard that says no to legitimate work.

Expand Down Expand Up @@ -6553,12 +6553,22 @@ Killed at the cap (`Tests (pytest)` step conclusion `failure`, all at 36:0x):

**Scope note — the two failures on this leg today are DIFFERENT and must not be merged into one cause.** PR #253 is a genuine timeout (no test failed) and PR #256 (`fix-1013`) is an assertion failure in `tests/test_connscale_cpu_probe.py` and `tests/test_connscale_smoke.py`, whose own comment already records this leg failing twice in one job on 2026-07-30. Only the first belongs to this item.

## THIS ITEM SUPERSEDES #1084, AND THE TWO MUST NOT BOTH LAND UNRECONCILED
## THIS ITEM SUPERSEDES #1084, AND #1084 WAS DROPPED RATHER THAN LANDED

> ⚠️ **Forward-looking citation, flagged as such.** At the time of writing, **#1084 is not on `main`** — it
> exists only on `origin/claude/gate-3d-remedy-1057` (**PR #261**, open). So this cross-reference does not
> resolve yet and will only do so once #261 lands. It is stated anyway because the collision is invisible
> from any worktree reading `docs/BACKLOG.md`, which is exactly how it was nearly missed.
> **SETTLED 2026-08-08 — and the outcome is the opposite of what this block used to predict.** It
> previously read *"#1084 is not on `main`; it exists only on `origin/claude/gate-3d-remedy-1057`
> (PR #261, open), so this cross-reference will only resolve once #261 lands."* **#261 landed WITHOUT
> #1084.** The item was dropped during #261's merge resolution, on the owner's instruction, on exactly
> the grounds set out below. **#1084 never reached `main` and no longer will**, so a reader grepping for
> it will not find it — that absence is the resolution, not a gap. The number stays allocated and burned
> rather than reused.
>
> This item is therefore the survivor, and per the paragraph below the reconciliation is written into it
> here rather than left for a reader to infer. **Nothing further is owed to #1084.**
>
> *This block is retained rather than deleted because the collision was invisible from any worktree
> reading `docs/BACKLOG.md` — which is exactly how it was nearly missed — and whoever finds it by
> grepping `1084` deserves the outcome and not only the diagnosis.*

**#1084 is the same defect, found the same day, on the same leg and the same cap.** This item's kill table
literally contains #1084's originating run (PR #261, `31149117314`). It is **not** a duplicate in the
Expand Down
71 changes: 65 additions & 6 deletions scripts/hooks/worktree_gate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -722,10 +722,71 @@ What to do instead:
# leg, where the lookup returned nothing, $isSelf went false, and BOTH branches emitted the
# generic deny -- byte-identical, which is exactly what the non-vacuity test below asserts
# against. Platform-masked, and caught only because that test compares the two denies.
$victimTop = Get-ComparablePath "$(& git -C $victimRaw rev-parse --show-toplevel 2>$null)".Trim()
$victimTopRaw = "$(& git -C $victimRaw rev-parse --show-toplevel 2>$null)".Trim()
if (-not $victimTopRaw) { $victimTopRaw = $victimRaw }
$victimTop = Get-ComparablePath $victimTopRaw
$selfTop = Get-ComparablePath "$(& git -C $cwdRaw rev-parse --show-toplevel 2>$null)".Trim()
$isSelf = $victimTop -and $selfTop -and ($victimTop -eq $selfTop)

# WHICH FAMILY IS THE VICTIM IN? The remedy has to be one that can actually reach it, and until
# now neither of the two this rule named could (BACKLOG #1057):
#
# * `remove.ps1 -Name <dir>` resolves to <repo-parent>/<repo-leaf>-<dir>. new.ps1 ASSERTS that
# shape after deriving it, so the <primary>-<name> sibling family is the only one it can
# produce and the only one remove.ps1 can resolve; handed anything else it fails Test-Path
# and throws "No such worktree".
# * `prune-merged.ps1` excludes anything with a `.claude/worktrees/` path segment OUTRIGHT --
# its own header says so, and -Name cannot reach them either. That exclusion is deliberate:
# those are the trees a live session gets relocated into.
#
# Census on this clone 2026-08-06: 45 sibling worktrees, 8 Claude-managed, 4 other -- and every
# live session sat in the 8. So the refused caller was reliably handed a command that throws and
# a tool that reports nothing about their tree. Same defect as #1032, one rule over: there, rule
# 3b printed a new.ps1 command new.ps1 refuses to run. A refusal the reader cannot act on is the
# standing invitation to route around the guard, which costs more than the refusal buys.
#
# THIS BRANCHES THE REMEDY STRING ONLY. Which worktrees rule 3d refuses is untouched, and no
# security decision reads $isSibling -- which is what makes a misclassification cheap here and
# not in rule 3c. FAILURE DIRECTION, pinned by test rather than asserted: a junction or UNC
# spelling breaks the prefix match, classifies NOT-sibling, and the not-sibling remedy is a
# literal `git worktree remove <abs path>` that is valid for EVERY family, siblings included.
# The dangerous direction needs a non-sibling to SPURIOUSLY match `<primary>-<name>`, which an
# unresolved alias makes less likely rather than more.
$govLeaf = Split-Path $govWt.Display -Leaf
$sibPrefix = "$($govWt.Compare)-"
$isSibling = $victimTop -and $victimTop.StartsWith($sibPrefix) -and
-not $victimTop.Substring($sibPrefix.Length).Contains('/')
# NAME the directory rather than printing `<directory-name>`. The gate has just resolved the
# path; leaving the caller to substitute a placeholder into a command is a second chance to get
# it wrong, and it is the reason the own-tree branch was unrunnable for the sibling family too.
$sibName = if ($isSibling) { (Split-Path $victimTopRaw -Leaf).Substring($govLeaf.Length + 1) } else { $null }
$removeCmd = if ($isSibling) {
"pwsh -NoProfile -File $($govWt.Display)\scripts\worktree\remove.ps1 -Name $sibName"
}
else {
"git -C `"$($govWt.Display)`" worktree remove `"$victimTopRaw`""
}
# The sibling family KEEPS prune-merged.ps1 and that is not politeness: it is dry-run by default,
# it consults occupancy, and it re-reads its fence immediately before each removal. For the family
# it covers it is strictly better than a bare `git worktree remove`, so the fix must not become
# "stop naming the scripts" -- a test pins that it is still offered here.
$cleanupBullet = if ($isSibling) {
@"
* Cleaning up merged worktrees is a maintenance job with its own dry-run-by-default tool. Run it and
READ what it proposes before applying anything:
pwsh -NoProfile -File $($govWt.Display)\scripts\worktree\prune-merged.ps1
"@
}
else {
@"
* prune-merged.ps1 CANNOT help with this one, so do not reach for it: it skips anything under
.claude/worktrees and anything that is not a <repo>-<name> sibling, by design, and its -Name cannot
reach them either. If this tree really must go, that is the user's call and this is the command --
it is not yours to run:
$removeCmd
"@
}

if ($isSelf) {
Write-Deny -Rule "3d" -Detail "git worktree $wtVerb (own worktree)" -Reason @"
BLOCKED: 'git worktree $wtVerb $victimRaw' acts on THE WORKTREE THIS SESSION IS RUNNING IN.
Expand All @@ -741,8 +802,8 @@ What to do instead:
* Finish and COMMIT anything you still want. A commit survives the tree being deleted; a dirty tree
does not.
* Then ask the user, in these words: "I am finished in $victimRaw and it can be removed once this
session ends." Removal is theirs to run from the main checkout:
pwsh -NoProfile -File $($govWt.Display)\scripts\worktree\remove.ps1 -Name <directory-name>
session ends." Removal is theirs to run from OUTSIDE this tree:
$removeCmd
* If you only wanted to leave it, just stop using it -- an unused worktree costs disk, not correctness.
"@
}
Expand All @@ -757,9 +818,7 @@ uncommitted work in them. There is no undo, and the session using it finds out w
fails. That asymmetry is why the default is refusal even though the tree may well be abandoned.

What to do instead:
* Cleaning up merged worktrees is a maintenance job with its own dry-run-by-default tool. Run it and
READ what it proposes before applying anything:
pwsh -NoProfile -File $($govWt.Display)\scripts\worktree\prune-merged.ps1
$cleanupBullet
* To find out whether a worktree is still in use, look rather than delete:
git -C "$($govWt.Display)" worktree list
* If you are certain it is abandoned and must go now, that is the user's call, not yours. Say so:
Expand Down
Loading
Loading