Repository navigation
[Bug]: Merged-worktree cleanup retains worktrees after squash merges #14742
Description
Activity
Note
Grok responding on behalf of Julius.
Triage
Thanks for splitting this out so cleanly, @tris203. I confirmed the behavior on
main(99e08526). It's the limitation that shipped with merged PR #11598 and is described in the storage guide, not a regression: merge cleanup only counts a worktree as merged when its head is an ancestor of the primary remote's default branch, so after a squash merge the worktree is kept.Where it happens
The
worktreeUnchanged || worktreeOnMergebranch inapps/server/src/storageCleanup.tsfetchesrefs/remotes/<primary>/<default>and runsmerge-base --is-ancestor. A non-zero result returns right away, beforebranchPullRequestruns, so the merged pull request is never checked. The Git side checks out too. Aftergit merge --squash,git diff --quiet feature mainexits 0 butgit merge-base --is-ancestor feature mainexits 1.A focused fix that fits the scope requested on #14651
Treat an otherwise eligible worktree as merged when a fresh pull-request read for its branch meets all of these:
- the pull request is
merged - its head branch is that branch
- its base is the project's default branch
- its head SHA equals the checkout's current
HEAD
The squash commit has a different SHA, so tree equality isn't the proof. This check also covers rebase merges where the local head is still the pull request's head.
What it should leave alone:
- It applies only to Delete merged worktrees. The unchanged-worktree rule stays an ancestry check against the default branch, with no new target resolver or setting. That broader change is the separate problem fix(server): clean up worktrees after squash merges #14651 bundled in.
- Pull requests merged into a release branch or a stack parent don't qualify.
- A later local commit, a different head branch, a closed-unmerged pull request, a stale snapshot, a missing head SHA, or a failed lookup all fall back to the current ancestry result. One catch:
branchPullRequest(viatoStatusPr) doesn't carry a head SHA today, and the GitHubstate: alllist omitsheadRefOid, so the read needs to include it. - All the existing clean-checkout, idle, ownership, ignored-file, and final policy and
HEADchecks stay, and branches and thread history are kept. - Update the storage guide sentence that points squash merges to the inactivity rule. Tests should cover an exact-head removal, a later commit, a non-default base, a missing head SHA, a failed lookup, and unchanged-only cleanup still ignoring squash evidence.
If you open a replacement PR, linking it from #14651 would help connect the two.
- the pull request is
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
on Oct 2, 2026 - added 2 commits that reference this issue
on Oct 2, 2026
Before submitting
Area
apps/server — automatic worktree cleanup, shared by all clients.
Context
The merged-worktree cleanup rule retains a clean worktree after its PR is squash-merged because its original head is not an ancestor of the default branch. This is a known intentional limitation, not a regression: the original implementation, #11598, explicitly retained squash/rebase merges without ancestry proof, and the current guide documents it.
This issue requests triage of the focused squash-cleanup repair identified in #14651's closure. That PR separately changed ancestry to use a PR target instead of the project's default branch; that separate change is outside this report.
Steps to reproduce
T3 conditions:
The Git condition can be reproduced independently, without a GitHub repository or T3 server:
Observed in an isolated Git reproduction: tree difference exit
0(identical trees), ancestry exit1(feature head is not an ancestor).Expected behavior / requested triage decision
An otherwise eligible worktree whose current exact head was the head of a freshly confirmed merged PR should be eligible under the merged rule after a squash merge.
Please establish whether that evidence is sufficient and the accepted scope before a replacement implementation. The minimal reproduction merges into the default branch. In particular, should a linked upstream PR in a fork-only checkout qualify, and should a PR merged into a release branch or an unmerged stack parent qualify, or should the first version be limited to the default target?
Keep the separate unchanged-worktree rule's default-branch meaning and existing ancestry fallback. No broader PR-target resolver or new cleanup setting is requested here.
Actual behavior
The cleanup gate fetches the primary remote's default branch, then requires the checkout head to be its ancestor. A squash merge normally fails that test, so evaluation returns before querying whether the PR merged. The worktree remains unless another cleanup rule qualifies it.
The requested change must preserve the current clean-checkout, activity, ownership, ignored-file and final policy/HEAD checks. Later local commits, a different PR head branch, stale merged state, missing head evidence or lookup failures must not satisfy a new exact-head path. Branches and thread history should remain as they do today.
Impact
Minor bug or occasional failure: completed squash-merged worktrees retain disk usage unless another rule removes them. This is a limitation of the merged rule, not data loss.
Version or commit
Upstream
mainat99e08526e5ec84f294940cba5929841518c52fec.Environment and verification limits
Linux, isolated local Git repository plus source tracing against the commit above. The Git ancestry failure was reproduced; an integrated T3 cleanup run and a live GitHub squash merge were not performed for this report. The T3 outcome above is established by the source gate, not a claimed application test.
Workaround
The existing guide recommends inactivity-based cleanup for squash merges. That is a separate policy with different timing, so it does not make the merged rule itself effective.