Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

- **Config deprecation-layer correctness**: Three independent fixes — structural migration now preserves unrelated `[ci]` keys and unparsable deprecated sections instead of discarding them; `config update` aborts on an approvals-copy I/O failure instead of silently dropping approvals; deprecated template-variable renaming rewrites the parsed TOML tree instead of doing a raw text replace that corrupted occurrences inside escaped strings. ([#2783](https://github.com/max-sixty/worktrunk/pull/2783))

- **Orphaned fsmonitor daemons are reaped**: `wt remove`'s background internal sweep now terminates (`SIGTERM`, then `SIGKILL` after a short bounded wait) any `git fsmonitor--daemon` whose IPC socket no longer resolves to a live worktree. This reclaims daemons orphaned by paths that bypass `wt remove`'s synchronous daemon stop (plain `git worktree remove`, manual `rm -rf`, or a crashed `wt`), which otherwise accumulate until reboot and can hang `wt list` when wedged. A daemon serving a live worktree is never reaped.

- **Data-safety and correctness fixes**: Six independent single-file fixes — `wt step relocate --clobber` refuses to overwrite an existing backup; `wt remove` re-checks cleanliness immediately before a forced submodule removal (time-of-check/time-of-use); `wt switch` re-discovers the base worktree via a fresh `Repository` after `worktree add` instead of reading a stale cached list; `wt switch pr:<N>` derives owner/repo from the forge remote rather than the primary remote and validates an empty Azure source branch at the provider boundary; user-controllable branch operands are guarded with `--`. ([#2784](https://github.com/max-sixty/worktrunk/pull/2784))

### Documentation
Expand Down
2 changes: 1 addition & 1 deletion docs/content/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ Use `-D` to force-delete branches with unmerged changes. Use `--no-delete-branch

### Other cleanup

- `wt remove` — in addition to the target worktree, sweeps `.git/wt/trash/` entries older than 24 hours in the background (eventual cleanup for directories orphaned when a previous background removal was interrupted)
- `wt remove` — in addition to the target worktree, runs a background internal sweep: deletes `.git/wt/trash/` entries older than 24 hours (eventual cleanup for directories orphaned when a previous background removal was interrupted), and terminates (`SIGTERM` then `SIGKILL`) `git fsmonitor--daemon` processes whose worktree no longer exists
- `wt config state clear` — removes all worktrunk data from `.git/` (config keys, caches, markers, hints, variables, logs, stale trash)
- `wt config shell uninstall` — removes shell integration from rc files

Expand Down
2 changes: 1 addition & 1 deletion skills/worktrunk/reference/faq.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 2 additions & 11 deletions skills/worktrunk/reference/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,9 @@ for pid in $(pgrep -f 'git fsmonitor--daemon'); do
done
```

Sockets listed as bare `fsmonitor--daemon.ipc` (no resolved path) belong to deleted worktrees — safe to kill:
Sockets listed as bare `fsmonitor--daemon.ipc` (no resolved path) belong to deleted worktrees. `wt remove` reaps these as part of its internal sweep: after the primary removal output, it sends `SIGTERM` (then `SIGKILL` after a short bounded wait) to every `git fsmonitor--daemon` whose socket no longer resolves to a live worktree. That covers daemons orphaned by `wt remove` itself and by paths that bypass it (plain `git worktree remove`, manual `rm -rf`, or a crashed `wt`).

```bash
for pid in $(pgrep -f 'git fsmonitor--daemon'); do
sock=$(lsof -p $pid 2>/dev/null | grep 'fsmonitor--daemon.ipc' | awk '{print $NF}' | head -1)
[ "$sock" = "fsmonitor--daemon.ipc" ] && kill -9 $pid
done
```

For a specific hung worktree, kill the daemon whose socket path matches it, or just `pkill -9 -f 'git fsmonitor--daemon'` and let the next `wt list` respawn the live ones. Disabling fsmonitor globally (`git config --global core.fsmonitor false`) avoids the class of problem entirely at the cost of some `git status` speed on large repos.

Daemons leak when a worktree is removed while its daemon is already unresponsive — `wt remove` calls `git fsmonitor--daemon stop`, but a daemon that can't answer its IPC can't be stopped through it.
The residual case the sweep deliberately leaves is a wedged daemon on a *live* worktree that is never removed: `git status` in that worktree blocks on the unresponsive IPC, but the daemon still serves a real worktree, so reaping it implicitly is out of scope. Terminate it manually: kill the daemon whose socket path matches the worktree, or `pkill -9 -f 'git fsmonitor--daemon'` and let the next `wt list` respawn the live ones. Disabling fsmonitor globally (`git config --global core.fsmonitor false`) avoids the class of problem entirely at the cost of some `git status` speed on large repos.

## PowerShell on Windows

Expand Down
24 changes: 24 additions & 0 deletions src/commands/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,30 @@ fn spawn_detached_exec_windows(
Ok(())
}

/// Repo-wide internal cleanup op, run fire-and-forget after `wt remove`'s
/// primary user-visible output.
///
/// This is worktrunk's opportunistic janitor: it reclaims resources orphaned
/// by interrupted or out-of-band operations. It runs on the `wt remove`
/// cadence (not a timer, not a user command, not a read-only command), and
/// after primary output so it can never delay a user-visible message. Every
/// step is best-effort and additive — failures log at debug level and the
/// `wt remove` operation proceeds regardless.
///
/// Steps:
///
/// 1. [`sweep_stale_trash`] — delete stale `.git/wt/trash/` entries left by
/// an interrupted background removal.
/// 2. [`worktrunk::git::fsmonitor::reap_orphan_fsmonitor_daemons`] — terminate
/// `git fsmonitor--daemon` processes whose worktree no longer exists.
/// Defense-in-depth for daemons orphaned by paths that bypass `wt remove`
/// (plain `git worktree remove`, manual `rm -rf`, a crashed `wt`); the
/// `wt remove` source itself stops the daemon synchronously.
pub fn run_internal_sweep(repo: &Repository) {
sweep_stale_trash(repo);
worktrunk::git::fsmonitor::reap_orphan_fsmonitor_daemons(repo);
}

/// How old a `.git/wt/trash/` entry must be before [`sweep_stale_trash`] deletes it.
pub const TRASH_STALE_THRESHOLD_SECS: u64 = 24 * 60 * 60;

Expand Down
Loading
Loading