A light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.
fix(worktree): unlock-retry on locked cleanup + startup orphan sweep (#3707) (#3719)
* fix(worktree): unlock-retry on locked cleanup + startup orphan sweep (#3707)
Two root causes fixed:
1. **In-session cleanup blocked**: `executeWorktreeWaveCleanupPlan` now attempts
`git worktree unlock <path>` then retries `git worktree remove --force` when the
initial single-force remove fails on a locked worktree. Previously every cleanup
after a successful merge was silently blocked.
2. **Cross-session orphan accumulation**: new `reapOrphanWorktrees` helper sweeps
`.git/worktrees/*/locked` at startup. It reaps entries where the pid is dead,
the branch tip is an ancestor of the default branch (ancestry guard prevents data
loss on squash-merge repos), and the lock mtime is older than 5 minutes (race
guard). Wired into `quick.md` and `execute-phase.md` startup blocks guarded by
`USE_WORKTREES != false`.
SDK: adds `worktree.reap-orphans` query command (routes through gsd-tools.cjs).
Tests: 11 real-fs tests covering unlock-retry, dead-pid reap, live-pid skip,
unmerged skip, fresh-mtime skip, idempotent double-call, and structural wiring.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(changeset): add Fixed fragment for PR #3707 (worktree orphan cleanup)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(worktree): fix test portability on Windows + macOS for bug-3707 reap tests
- worktreeMeta helper: replace /\/\.git$/ with /[/\\]\.git$/ so the
gitdir path suffix is stripped on both Windows (backslash) and Unix.
- worktreeMeta helper: normalize CRLF→LF before splitting porcelain
blocks, fixing block parsing when git emits CRLF on Windows.
- reapOrphanWorktrees: replace single 'main' rev-parse with a
[defaultBranch, 'main', 'master'] candidate loop so test fixtures
without a remote origin (where branch may be 'master') don't bail
early. Intentionally excludes 'HEAD' to prevent false reaping when
HEAD is detached or on a feature branch (Codex adversarial finding).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(worktree): CI green — macOS symlink path, Windows test helper, pid portability, EPERM liveness
Four fixes to get macOS + Windows CI from red to green:
1. **macOS symlink mismatch** (worktree-safety.cjs): `reapOrphanWorktrees` now
builds a canonical→listed path map from `git worktree list --porcelain` using
`fs.realpathSync.native`. Uses the listed path (as git knows it) for
`git worktree unlock/remove`, not the gitdir-derived path. Fixes the
`/var/folders` vs `/private/var/folders` discrepancy on GitHub macOS runners
where `git worktree unlock <realpath>` was silently failing because git's
list stored the unresolved symlink path.
2. **Windows path separator in test helper** (test file): `worktreeMeta`
`.replace(/\/\.git$/, '')` → `.replace(/[/\\]\.git$/, '')`. On Windows,
git writes backslash separators in the gitdir file; the Unix-only regex was
causing `Cannot find .git/worktrees/<name>` for all Suite 2 tests.
3. **Non-portable PID in tests** (test file): All `'999999'` dead-PID literals
replaced with `deadPid()` helper that spawns a real short-lived child, captures
its PID, and returns it after exit. Eliminates flakiness on Linux systems where
`pid_max` can reach 4194304, making 999999 a live PID.
4. **EPERM fail-closed in isPidAlive** (worktree-safety.cjs): `catch { return false }`
→ checks `err.code === 'EPERM'` and returns `true` (alive). On Windows and
cross-user scenarios, `process.kill(pid, 0)` throws EPERM for live but
inaccessible processes; treating that as dead would reap a live worktree.
Adversarial review via codex confirmed:
- Squash-merge repos: fail-closed (CONCERN, not BUG — by design, not data-loss)
- canonicalToListed map: SAFE (fail-closed on realpathSync error)
- Concurrent reapers: SAFE (both prune; second gets skipped: remove_failed)
- Startup blocking: CONCERN (no global cap, 10s/call × N worktrees) — tracked,
not fixed here (requires separate perf work)
- gsd-sdk missing: SAFE (quick.md checks and fails fast with guidance)
All 27 local tests + Docker (holodeck) green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(worktree): address codex adversarial findings — fail-closed default branch + CRLF map
Two fixes from codex adversarial review of PR 3718:
1. **Default branch resolution (data-loss risk)**: `reapOrphanWorktrees` now
uses `refs/remotes/origin/<branch>` exclusively when a remote is configured.
If `origin/HEAD` is absent but a remote exists, we bail out (fail-closed)
rather than falling back to a local `main`/`master` that may not be the
real integration branch. The `main`/`master` fallback is only used when
there is provably no remote (local-only test fixtures).
2. **CRLF normalization in canonical-path mapper**: The `worktree list
--porcelain` output was split on '\n\n' without normalizing CRLF first.
On Windows, git emits CRLF, which caused block-splitting to fail and
left the canonicalToListed map only partially populated, weakening the
symlink/path-mismatch fix introduced earlier.
3. **Windows 8.3 short-path fix (test helper)**: Both `beforeEach` blocks
now call `resolvedTmpDir()` which pre-resolves `os.tmpdir()` via
`fs.realpathSync.native` so temp paths avoid RUNNER~1-style short names
that git stores in long form, causing worktreeMeta path comparisons to
fail on Windows CI.
All 11 real-fs + 16 unit tests green locally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(worktree): adversarial findings + macOS CI path-mismatch fix
## Root cause (macOS CI fail)
`reapOrphanWorktrees` stored `worktreePath` (gitdir-derived, real path via
git's symlink resolution, e.g. `/private/var/folders/…`) in results, while
the test's `wtDir` used the unresolved symlink form (`/var/folders/…`). After
reaping, `canonicalPath(wtDir)` can no longer call `realpathSync.native`
(directory gone), so it falls back to `path.resolve` — which returns the
symlink form — causing the `result.find()` comparison to miss.
## Fixes applied
### Source — worktree-safety.cjs
1. **Finding 1 (fail-closed PID check)**: Non-parseable lock content (e.g.
`"Locked by claude-code agent-xxx"`) is now treated as ALIVE with reason
`lock_owner_unknown`, not as dead. Previously it fell through as dead.
2. **Finding 1b (EPERM safe)**: `isPidAlive` call wrapped in try/catch; any
thrown error (EPERM = process exists but cross-user on Windows) → ALIVE.
3. **Finding 2 (startup warning)**: `cmdWorktreeReapOrphans` now writes a
one-line stderr warning when ≥1 entry is skipped or when reaper throws,
while keeping exit-zero so workflows don't break.
4. **Finding 3 (default-branch discovery)**: Local-only fallback now tries
`init.defaultBranch` config and HEAD symref before `main`/`master`, so
repos configured with `trunk`, `dev`, etc. get correct orphan detection.
5. **macOS path fix**: Result entry for reaped worktrees now uses `gitKnownPath`
(from `git worktree list`) instead of `worktreePath` (from gitdir file),
ensuring the caller always sees the path git uses for the worktree.
### Test — bug-3707-locked-worktree-cleanup.test.cjs
6. **macOS CI fix**: Pre-compute `wtDirCanonical = canonicalPath(wtDir)` before
calling `reapOrphanWorktrees` so the comparison works after removal.
7. **Gap 1**: New test — Claude Code lock format (`"Locked by claude-code …"`)
must not be reaped; asserts `status=skipped, reason=lock_owner_unknown`.
8. **Gap 2**: New test — `isPidAlive` throwing EPERM → must not reap.
9. **Gap 3**: New test — repo with `init.defaultBranch=trunk`; merged worktree
must be reaped (verifies trunk is discovered as the integration branch).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(test): raise waitForStoppedAt timeout 2 s → 5 s for Windows/Node22 CI load
Subprocess write latency exceeds 2 s on loaded windows-latest/Node22 runners
(test duration was 6181 ms); 5 s gives sufficient headroom without changing
any production behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> T
Tom Boucher committed
ca2644a71abaed249594f2c63b458c18da278f4b
Parent: ab24d80
Committed by GitHub <noreply@github.com>
on 5/20/2026, 7:22:12 PM