--dry-run only reads, so it does not wait for the pool lock while another call holds it.
Without --dry-run it asks, then says what it removes as it goes:
why column says why each slot is eligible, rather than quoting one threshold at all of them: idle 41d for a slot nothing has used in that long, named for a slot you named, and no recorded age, nothing on disk for an entry with no checkout, no output base and nothing to age it by.
Deleting one slot
Name a branch, a slot id or a slot’s directory — the first two are on screen inaspect worktree list — and how long it has been idle is not consulted. Naming it is the decision.
This also runs automatically during
aspect worktree add, at the default threshold. Ordinary use keeps the pool trimmed; this command is for doing it now, or at a tighter threshold.prune refuses rather than guessing, with the needs_force token and exit 1:
What it will not delete
Uncommitted work. A free slot should hold no checkout. If one does and git reports work in it, or cannot read it at all, the slot is kept and reported rather than deleted — the bookkeeping is what is wrong, not the work. A repository of its own at the slot’s path — a.git directory rather than the .git file a linked worktree has — counts as a checkout git cannot read (UR), since nothing here can vouch for its branches and history; so do a plain file at the slot’s path, and a checkout whose git reads another directory as its work tree (core.worktree set in the worktree’s own config). A slot whose directory is gone while git still keeps commits only its HEAD reflog reaches, refs only it has, an operation in progress or a staged index for it (VG) is kept too. Only naming the slot with --force=all overrides that.
A slot someone is working in. Only free slots are considered, whatever their age, and whether you named one or not — except a stranded slot, which can be named. A lease means a checkout that may have uncommitted work in it, and neither a timer nor an argument is grounds for deleting that — aspect worktree release ends the lease first.
A slot being set up. An aspect worktree add claims a slot before it checks a worktree out into it, and deleting the output base in between would break a command that is still running. Such a slot reads setup in aspect worktree list.
A slot whose Bazel server is still running. Unlinking an output base from under a live server leaves it writing into directories that no longer exist. Such a slot is also the warmest thing in the pool, which is the whole point of having one, so it is reported and kept:
Slots stranded by a clone that is gone
A slot belongs to one clone, becausegit worktree add is scoped to a single object store, and records it by path. Move, rename or delete that clone and the next add, release, path or prune in a clone of the repository takes back what it can:
- A free slot goes to that clone, warm output base and all, and from then on ages like any other free slot. It holds no checkout, so it is nobody’s in particular.
- A leased slot goes only to the clone whose git still lists its checkout — the moved clone itself — and
git worktree repairpoints the checkout back at it, so the lease and its work carry on there. When the slot has moved too, recovering the lease takesgit worktree repairrun by hand. A slot moved away on its own whose git records hold none of the commits only its HEAD reflog reaches, refs only it has, an operation in progress or a staged index has its git registration dropped, so the moved copy’s git link breaks; its files are kept.
aspect worktree list tags such slots clone moved. prune says what it took over, and prune --dry-run what it would, rather than listing them for deletion:
- The moved clone’s free slots are recorded for that path and hold no checkout, so they go to the new clone, warm bases and all: a free slot is anybody’s.
- Its leased slots stay the moved clone’s. Only the moved clone’s git lists their checkouts, so they go back to it, and the new clone, which cannot read them, is never handed them as its own. Such a slot is recorded for the new clone’s path while the new clone’s git has no worktree there. From the new clone,
add,releaseandpathrefuse it withslot_stranded, andaspect worktree listtags itanother clone's. The nextadd,release,pathorprunerun from the moved clone takes it back.
git worktree repair <path> from here may make it readable. Failing both, aspect worktree prune <slot> --force=all deletes it once you have copied out anything you need, refused while the session holding it is still running.
What is left is stranded: a leased slot whose clone is no longer at its path and that no clone here lists — deleted, or moved somewhere this clone cannot see — and, outside any clone, where nothing is taken over, any slot of a clone that is gone. A stranded slot is deleted by naming it, whatever its age — and so is a slot recorded for this clone’s path that this clone’s git does not list. It takes --force=all when its checkout holds work, and a checkout git cannot read always does, which is what a leased slot of a clone that is gone has. A leased one whose session is still running is refused with held_by_another_session, --force=all or not: a session at work in it says the clone moved rather than went, and a command run from where it is now takes the slot back. One whose process was never captured cannot be shown to have stopped, so it is refused the same way unless --force=all is given. Both are asked before whether a Bazel server holds the slot. Whether the holder runs is asked again once a prompt is answered: a named stranded lease whose holder started running again meanwhile is not deleted, and prune exits 1.
From outside any clone, prune takes a slot id, a unique prefix of one of at least four characters, or a slot’s directory, as aspect worktree list --all prints it, and finds the pool on disk. That is the way to delete slots for a repository you no longer have a clone of. With nothing named it refuses, since there is no pool to sweep:
--force=all, after you have copied out anything you need.
A second clone of the same repository that still exists keeps its own slots: it shares the pool directory but not the slots in it, and reclaiming them is not this clone’s call. Naming one says so rather than pretending not to find it:
Refusals
Flags
string
A branch, a slot id or a unique prefix of one (four or more characters), or a slot’s directory to delete, as
aspect worktree list shows them. How long it has been idle is not consulted — naming it is the decision. Leave it out to work by idle time across every free slot.int
default:"30"
Delete free slots untouched for at least this many days, when no slot is named.
0 considers every free slot.“Untouched” is the newer of two things: Bazel’s last activity in the slot’s output base — the newest mtime among the files Bazel writes as it works, the evidence
aspect gc measures a base by — and the slot’s last lease, taken or ended. It is what aspect worktree list shows as last used, and never reaches back before the slot was created. Because a lease counts here and not to gc, a slot leased recently whose base Bazel has not touched in a month can have that base removed by gc while prune keeps the slot: it goes cold, not away.boolean
default:"false"
Report what would be deleted and delete nothing.
false | true | all
default:"false"
Delete without asking.
true, which bare --force means, is required when stdin is not a terminal, so a script or an agent has to opt in rather than have a prompt answered on its behalf. all also deletes a named slot whose checkout holds uncommitted work, which is otherwise refused with the files listed — the same two grades as release --force.string
default:""
The session asking, as given to
add with the same flags. Needed only when the lease was taken with them, so this command recognises it as yours; detected automatically otherwise.text | json
default:"text"
text writes the report to stderr and leaves stdout empty. json writes one document to stdout.JSON output
candidates is what a run would delete, taken_over the slots of a moved or deleted clone it took over (or, with --dry-run, would), kept what it spared and why, and declined says whether you were asked and said no — which an empty pruned on its own cannot tell you. candidates is reported whether or not anything was deleted, so a dry run says what it would do rather than only that it did nothing. A candidate’s idle_days is null for a slot with no recorded age, and leased is the branch a stranded slot still holds a lease on, empty for a free one. A leased candidate’s last_branch may be empty, since the slot’s last branch is recorded when a lease ends: read the name from leased.
Relationship to aspect gc
The two reclaim different things, and neither makes the other redundant.
aspect gc never treats a pooled slot as orphaned. A free slot looks exactly like an abandoned worktree, because the worktree is gone on purpose and that is what keeps the server warm, so the rule that reaps orphans skips pool slots — except one nothing will lease again, its directory gone and its pool holding no record of it. It does still collect them by idle time: a pooled base unused past --base-max-idle-days (30 by default) is removed like any other idle base, which leaves the slot in place and reading cold.
aspect worktree prune removes the slot: its worktree, its registry entry and its output base together. That is why it deletes the base itself rather than leaving it for gc — a prune that reclaimed no disk and told you to go run something else would not be a prune.
aspect output-bases lists every Bazel output base on the machine, pooled or not, and aspect gc reclaims the idle ones.
