* 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>
691 B
691 B
type, pr
| type | pr |
|---|---|
| Fixed | 3707 |
Locked worktrees now cleaned up correctly — executeWorktreeWaveCleanupPlan previously issued a single git worktree remove --force which Git refuses on locked worktrees, blocking every post-merge cleanup when Claude Code's agent runtime held a lock file. The fix attempts git worktree unlock then retries the remove. A new worktree.reap-orphans command (gsd-sdk query worktree.reap-orphans) sweeps orphaned locked worktrees from prior crashed sessions at startup — it reaps entries whose pid is dead, branch is merged into the default branch, and lock mtime is older than 5 minutes. Wired into quick.md and execute-phase.md startup. (#3707)