Rove

Managing worktrees

Windows worktree actions match native and Git path spellings consistently. Containment checks compare directory segments, including names such as ..cache, and protect the caller's own worktree from removal.

The Worktrees page is a cross-project audit and cleanup tool for local git worktrees. It helps you decide what has landed, what still carries local work, and which working directories are safe to remove.

For the difference between a Task, Worktree, Terminal Tab and Split, start with Concepts. To create or adopt a task, see The TUI.

Branch naming

A managed task created without an explicit branch derives its branch name from the task title, following the repository's own naming convention: Rove scans the repo's existing branches (local + origin) and matches the dominant style: a type prefix like feat//fix//chore/ when that's what the repo uses, or a bare kebab slug otherwise (also the fallback for an empty repo). Name collisions get a short -2/-3 suffix, and a name that would clash with an existing branch's folder (fix beside fix/login, or feat/x beside feat) is skipped or flattened (feat-x). Generated names never contain Rove branding. An explicit --branch on creation, update --branch afterwards, and b on a task row in the sidebar override this entirely. A branch still on its new-task placeholder is renamed once, automatically, when the task gets a real title (skipped if the branch already has an upstream); after that Rove never touches it again.

The title slug keeps whole words within 32 characters. If the first word alone exceeds 32 characters, only that word is hard-cut to the limit.

Where worktrees live

By default a managed worktree lands under ~/.rove/worktrees/<repo-basename>-<hash>/<slug>, where <slug> is a random animal name (-v2/-v3 on collision). Settings → General → Worktree location relocates the root (a $project_dir token expands to each task's project root). Remote (SSH) projects put worktrees under the remote project's own path at <project>/.rove/worktrees/<slug>.

Ignored directories are cloned in

On macOS a new local task's worktree gets the main checkout's node_modules (including packages/*/node_modules), .venv, target and .build, cloned copy-on-write on the same APFS volume, before .rove/init.sh runs. They are copies, not links: editing one changes only that worktree, and the disk cost stays near zero until it does. Turn it off with worktree.cloneIgnored or change the list per repo with .rove/clone-dirs; see Cloned ignored directories. Cross-volume, non-APFS, SSH and non-macOS setups behave as before.

Open and navigate the page

  1. Focus the task sidebar with ctrl+q.
  2. Press x.
  3. Use the up/down arrows to select a worktree.
  4. Press esc or q to return to the workspace.

The page lists non-main worktrees with a branch checked out from every saved local project (bare and detached-HEAD worktrees are skipped). Remote SSH projects are not included. It loads local git facts first, then fills in remote and GitHub PR signals; a slow or unavailable network therefore leaves useful local rows on screen.

Read a worktree row

Each row shows its branch and absolute path. These badges are evidence, not an automatic cleanup decision:

BadgeMeaning
roveThe path is inside a recognized Rove-managed root. It does not by itself mean a Task currently points to it.
dirtygit status --porcelain found modified, staged, or untracked files.
on remoteA branch with this name exists on origin.
not pushedNo branch with this name exists on origin.
remote unknownThere is no reachable origin, or the remote check failed or timed out.
PR openGitHub reports an open PR for the branch.
merged (PR)GitHub reports a merged PR for the branch.
in mainThe branch is zero commits ahead of the detected remote default branch.
PR closedGitHub reports a closed, unmerged PR.
staleNo stronger signal exists and the last activity is more than 14 days old.

dirty is git status --porcelain and nothing else, so a worktree whose only work is gitignored — a HANDOFF.md, a .scratch/ — carries no dirty badge and can still read in main. Deletion checks more than the badge does (git status --ignored as well), so such a row looks safe to clean here and is still refused at the delete, offering the two-stage force flow below. That is the gate working: the badge reports what git reports, and the refusal names the paths git will not.

The rove badge describes where the directory lives. Adoption describes whether Rove has a Task for that worktree. Either a Rove-managed or external worktree can be adopted through New task → Adopt Worktree or rove adopt. Only an adopted/tracked worktree can use the page's Land action.

Remote and PR badges are advisory. They never bypass the dirty-worktree gate, and remote unknown is not treated as permission to delete.

Land a tracked branch

Landing merges the selected task branch into the branch currently checked out in the project's base checkout.

  1. Make sure the base checkout is on the branch you intend to receive the work and has no uncommitted or untracked files.
  2. Select the worktree and press l.
  3. Confirm the branch and base-checkout operation.

The page uses a normal --no-ff merge. If the selected directory is not tracked by a Rove task, Land refuses. A dirty base checkout also refuses, as does a branch with no commits ahead of the base (naming the uncommitted files when the work was never committed), a detached-HEAD base checkout, and a base already on the branch being landed. On a merge conflict, Rove aborts the merge and reports the conflicted paths, leaving manual conflict resolution to you.

A successful land removes the worktree — it is spent once its branch is in — and the row leaves the page. The branch survives: git keeps the durable record. Removal after a land is never forced, so it is refused by everything a plain delete is refused by: the worktree is dirty, it holds gitignored work git status cannot see (a HANDOFF.md, a .scratch/), that ignored listing could not run, it is the base checkout, or it is the directory Rove itself is running from. The land still stands and the page reports why the worktree is still there. Deleting the task and deleting the source branch remain separate lifecycle decisions; from the CLI, rove api land --remove-worktree=false keeps the worktree.

Remove a clean worktree

  1. Select the worktree and press d.
  2. Confirm Delete worktree.

The row disappears while removal runs. If git refuses or the operation fails, the row returns. A successful removal deregisters and removes the working directory but keeps its git branch.

The page lists every registered worktree of a saved project, including directories Rove did not create. Two of those it refuses to delete (NOT_A_ROVE_WORKTREE): the directory a directory Task pins, and a project's own checkout. Both are yours, not Rove's, and neither can be re-materialized from a branch.

For a Rove-created worktree tracked by a Task, Rove clears that Task's worktree pointer. It does not delete the Task, its branch, or engine history. Opening the Task later may materialize a fresh worktree from the retained branch.

rove api remove-worktree --task-id ID [--force] is the same operation from a shell, for scripting a reclaim of idle checkouts. It runs this path — session teardown first, dirty refused without --force, salvage snapshot on every force — and adds two refusals a clicking human cannot trigger: it will not remove the project's own checkout (BASE_CHECKOUT) or the worktree the command is running from (CALLER_WORKTREE).

When git removes the worktree but not its directory

git worktree remove does two things — deregister the worktree's metadata and delete its directory — and they can fail apart. A path inside the tree that the current user cannot unlink (a chmod -w directory, a read-only dependency cache, a directory an external tool holds) makes the directory delete fail after the deregistration has already landed.

Rove reports that as what it is. The removal counts as done — git no longer knows this worktree, so no retry can advance it — and a notice names the directory left on disk and git's own reason. A task deleted this way is deleted, not parked in an error state, and the same line goes to ~/.rove/daemon.log next to the rest of the deletion trail.

Rove never deletes that directory for you: whatever made it undeletable may be something you want. Remove it by hand if you want the disk space.

On Windows the usual cause was Rove's own engine session: a process whose working directory is inside the worktree keeps that directory undeletable, and the removal used to start while the session was still exiting — and ending it reached only the shell, not the engine it had launched. A deletion now ends the whole process tree and waits for it to exit before git worktree remove runs. A Permission denied residue after that means something outside Rove holds a path in the tree (an editor, a running dev server you started elsewhere).

Force-remove a dirty worktree

Dirty removal is deliberately two-stage:

  1. Press d and confirm the ordinary deletion.
  2. The daemon checks the worktree and refuses because it is dirty.
  3. Rove opens a second Force delete worktree? confirmation naming the branch and warning that modified and untracked files will be permanently lost. It lists the files the refusal found — the first ten, then a count of the rest — so you can see what is at stake before confirming.
  4. Confirm only after preserving anything you need.

The first confirmation can never silently turn into a force deletion. The second confirmation is the boundary that authorizes data loss. The branch is still retained, but uncommitted and untracked files are not part of it.

Step 2 checks more than git status --porcelain reports. A gitignored HANDOFF.md or .scratch/ shows in no status output, so the check also asks git status --ignored and refuses on any ignored entry under the 64 MB per-entry budget below — the same budget the snapshot uses, so the delete refuses for exactly what the forced retry then rescues. The refusal names those paths, because git status will not. An ignored entry OVER the budget (a node_modules/, a build directory) is not treated as work: it neither blocks the delete nor lands in the snapshot.

If that git status --ignored cannot run at all, the delete is refused, not allowed. An empty list is this gate's permission to destroy the directory, so a listing that failed used to hand out that permission on the strength of having failed. The refusal says the listing failed rather than naming paths, and --force still overrides it — which is the safer order, because the forced path takes a salvage snapshot first.

Landing with --delete-branch

land --delete-branch deletes the branch with git branch -D, which removes the branch's reflog along with its ref — and the worktree removed just before it held the only other reflog for that branch. After a --strategy merge land that costs nothing: the merge commit on the base still reaches every commit. After a --strategy squash land it would, because the squash writes one new commit that has no link back to the branch's own commits.

So before deleting a branch nothing else keeps reachable, Rove writes its tip to refs/rove/salvage/<branch>-<timestamp> — the same namespace as the force-delete snapshots below, listed by the same command. rove api land returns it as branchAnchor. Recover the pre-squash history with:

git log refs/rove/salvage/<branch>-<timestamp>
git branch <recovered-name> refs/rove/salvage/<branch>-<timestamp>

No anchor is written when another ref already reaches the tip, which is the ordinary merge case, nor when no branch was deleted at all.

The delete needs the worktree to be gone first: git refuses to delete a branch a live worktree has checked out. A land that kept the worktree — because you passed --remove-worktree=false, or because removal was refused (dirty tree, gitignored work, an ignored listing that failed, base checkout, the worktree you are running from) — keeps the branch too, and reports it as branchKept with the reason. Clear the worktree and re-run.

Recovering work a force delete destroyed

Before a forced removal, Rove snapshots what the removal is about to destroy — modified tracked files and files you never git added — into a git ref in the owning repo.

One force path takes no snapshot, because it cannot: a directory whose git repo is unreachable at all. Snapshotting runs git inside the worktree, and reaching that path means there is no repo to run it in. --force there is a plain delete of a directory under a Rove worktrees root, with nothing to recover afterwards.

Files matched by .gitignore are included too, but only up to 64 MB per top-level entry. That threshold is the whole rule: a gitignored note or scratch directory is kilobytes and gets snapshotted; node_modules/ or a build directory is far larger and is skipped, because a snapshot carrying one is too big to be useful. An ignored entry whose size cannot be read is skipped. So a gitignored HANDOFF.md or .scratch/ is recoverable, and a gitignored 200 MB dist/ is not.

One thing a snapshot cannot hold: a submodule or a nested worktree. git add records those as a commit pointer rather than their files, so uncommitted work inside one is in neither the snapshot nor the commit that pointer names. Rove does not pretend otherwise — the audit line lists those paths as NOT captured, and the git restore commands below will not produce anything under them.

List the snapshots, newest last:

git for-each-ref refs/rove/salvage

Then inspect and restore from one, run inside the owning repository:

git show refs/rove/salvage/<branch>-<timestamp>
git restore --source=refs/rove/salvage/<branch>-<timestamp> -- path/to/file

The snapshot's exact ref is also written to ~/.rove/daemon.log beside the deletion's audit lines, with the recovery commands filled in — see TROUBLESHOOTING for how to find the deletion by task title and time.

The refs are ordinary git refs: they are not garbage-collected, and they are never pushed. Delete one you no longer need with git update-ref -d refs/rove/salvage/<branch>-<timestamp>.

Troubleshooting

  • Land says the worktree is not tracked. Adopt it as a Task first, or merge the branch manually.
  • Land refuses a dirty base checkout. Commit the changes in the base checkout, verify its current branch, then retry. Don't reach for git stash: the stash stack lives in the repo's common dir (.git/refs/stash) and is shared by every linked worktree, so a stash here can entangle — or be popped by — parallel tasks' work.
  • Land reports conflicts. Rove has already aborted the merge. Resolve the branch relationship manually before retrying.
  • A removed row comes back. The daemon operation failed or git still lists the worktree. Read the daemon log, correct locks or permissions, and retry.
  • Remote state says unknown. Check git remote -v, network access and gh auth status; the local dirty and branch facts remain valid.

On this page