Skip to main content
New and experimental. File bugs and suggestions to aspect-build/aspect-cli/issues.
Every slot in this repository’s pool, what holds it, and how warm it is.
The first line is the pool’s own directory, written as a template whose variable is the slot column — so any row’s path is the heading with that row’s slot substituted in. Reading the tables is then the same exercise as reading git worktree list, which this deliberately resembles. The line under it counts the slots and how many are taken and free, which answers whether you can get one before you read a row. Run from a clone, a free slot of another clone of the repository, or of one that is gone, is counted apart as free but not this clone's, since it is not yours to take; outside any clone there is no “yours”, and every free slot counts as free. Slots come in two tables, because what is worth knowing about a slot somebody holds and one you could take are different things. A taken slot is held, and the question is by whom; a free slot is available, and the question is how warm it is and how long it has sat. Each heading sits at the left margin, outside its indented table, so it stands apart even without color; on a terminal it is highlighted and the column headers are dimmed. Color follows stderr, where the report is written, and is off under NO_COLOR. Within each, columns run from the slot itself outward, which keeps the fixed-width ones on the left so the numbers read as a block and the ones that vary in width fall at the end. Rows are ordered by what is happening: in use first, warmest down to cold, then the slots you could take, most recently used first. A column no slot fills is left out. agent is the exception: it is shown even when nothing is running, because a column that disappears would never tell you the pool records which session holds a slot. notes carries only exceptions, so it does disappear. What the columns and their values mean lives in aspect worktree list --help, which the footer points at, rather than under every listing.

Getting back to a session

A session id is printed whole, in its own column, so it can be handed straight to the harness — an abbreviated UUID identifies a session only to someone who already knows which one it is, and --resume will not take it. The command that returns to a session is in --verbose, in a section per session. That is also how the sessions which held a slot earlier are reachable, which a table of present holders could not express. One case is surfaced without --verbose, because it is the one you act on: a slot whose holder has stopped and whose session the harness still has. A closed terminal is almost always what that means, and the work in it is waiting to be picked up rather than finished with, so the listing offers the way back in rather than the way to take the slot away.
The cd is there because claude --resume <id> resolves within the current directory’s project, so the bare command would only work where you already happened to be standing. It is harmless for the harnesses that do not need it, which spares you having to know which those are. Where a harness marks its presence without naming a session — Gemini CLI and Cursor both do — the session cell is left blank, and --verbose prints - for it. That is a statement rather than a gap: the answer is unavailable, not empty. No command is offered for a session the harness’s own store was read and found not to have, since one that resolves nothing is worse than none at all. Slots held by a person report no agent; there is no session to name.

One repository at a time

A pool belongs to one repository — slots are keyed by the repository your clone points at — so this lists the slots of the clone you are standing in and never another repository’s, the same way git worktree list reports one repository’s worktrees. aspect worktree list --all is the other way to widen the view, and the one that stays within the pool: every pool on this machine, each under its own heading. It works outside a repository too, since “where are my agents” gets asked from anywhere.
One thing it cannot do is check another pool against git. git worktree list answers for one object store, so only the pool belonging to the clone you run from is reconciled; the others report the branch their own records hold, and the listing says so beneath the tables. What is current for every pool is anything read from disk — whether a slot’s directory is still there, and how warm its output base is — because none of that goes through git. aspect output-bases is the machine-wide view of a different thing. It walks the Bazel output user root rather than any one pool, so every pool’s slots appear in it, each attributed to its repository and showing what it holds or last held, beside every other output base on the machine. A pool whose builds went to a different output user root holds state neither command can see; --all says so when this workspace is the one that moved its root. A pool’s slots get their own heading there, each row carrying the repository and the lease:
A leased slot reads leased · <branch>, followed by its holder when an agent took it. A slot whose clone is no longer at its recorded path adds owner gone: leased · <branch> · owner gone · <holder>, or free · last <branch> · owner gone. aspect output-bases reads no git, so it cannot tell a moved clone from a deleted one; this command’s clone moved and stranded notes do.
aspect output-bases only knows a slot that has been built in, since an output base is the thing it enumerates. A slot nobody has built in yet has nothing for it to find, and only this command will show it.

Every lease a slot has held

--verbose adds a block per slot — its paths, any server holding it, and the slot’s remembered holders, newest first — each block headed by the slot, which is what the tables above are keyed on. Then a section per agent session those holders name:
A slot remembers its last ten holders, and says how many of them it is showing. Each row says which branch was in it, how long they kept it and whether they still do, which agent session had it, and the project the harness filed the session under. project drops out for a slot none of whose holders could be placed. The sessions come last: each one’s harness, its project, the command that returns to it, and every lease it took, active first. Most leases come from subagents, which report their parent’s session, so the way back into a conversation is said once there rather than on every row it touched. A holder that let go of a slot is as resumable as the one holding it now, which is why its session is listed all the same. Where the harness’s store was read and does not have the session — 7f1e9a02 above — resume reads -, because a command that resolves nothing is worse than none. The output base line says why a base is absent, since the two reasons send you different places. (not created yet) is a slot nobody has built in, and the path is where a base will go. (reclaimed) is a base that was there once and has been collected — by aspect gc or a bazel clean --expunge — which the slot survives and rebuilds on its next build. The holder is recorded as the harness, the session and the directory it ran in. A finished session’s pid says nothing and is not kept; the directory is a different kind of fact — harnesses file a session under the project it was opened in, so it is what lets a past holder’s session be placed. Two things do not add to leases: taking a slot over with --take-over, which hands an existing checkout to another session without rebuilding anything, and a worktree a person created by hand, which the pool does not manage. A take-over does appear in the history, because the slot changed hands.

Where the output base is

Bazel derives a workspace’s output base as md5(workspace_path) under the output user root, so a slot’s fixed path is enough to find its state without asking a server — and bazel, aspect, an IDE or a script in that directory all reach the same base. The root is resolved as Bazel resolves it: startup --output_user_root= from the rc chain where a repo or your ~/.bazelrc sets one, else the platform default. A listing reads warmth under that root, and names it under --all when it is not the default.

Reading it

in use / free says whether a worktree is currently checked out in the slot. A free slot keeps its Bazel state and is what the next aspect worktree add reuses. setup is the third state: an aspect worktree add has claimed this slot and is checking the branch out into it. It lasts as long as that checkout, and the slot is not available to anything else meanwhile — including prune. The branch column shows the branch being checked out; where it shows something else, the notes add claimed for <branch>. If the add that claimed it is no longer running, the notes say add abandoned, a line under the table says so, and the next add from the clone that owns it reclaims the slot. age and leases are the pool’s return on the disk it is holding. leases counts the worktrees a slot has held, which is how many cold starts it saved; a slot reading 1 has never been reused, and a pool where every slot reads 1 is one that grew to your peak concurrency and has not turned over yet. last used is how long since the slot was last used: Bazel’s last activity in its output base — the same evidence aspect gc measures a base by — or a worktree taken or given back there, whichever is newer. The free table is ordered by it, so the slot you just released is at the top, and it is the number aspect worktree prune acts on, so it is shown rather than left to be guessed at. A slot that has held nothing is aged from its creation. Both it and age are shown in the coarsest unit that still distinguishes: minutes under an hour, hours under two days, then days, so a slot used ten minutes ago reads 10m rather than rounding to nothing. --output=json carries it as idle_ms, with age_ms beside the rounded idle_days and created_days. last branch heads the free table’s branch column: the branch that held the slot most recently, which is the signal for how warm it will be for similar work, and which aspect worktree add prefers to reuse when you ask for that branch again. It is history rather than somewhere to go — there is no worktree in a free slot — though prune accepts it as a way of naming the slot. notes carries only what is exceptional about a row: Rows are ordered by what is happening: in use first, warmest down to cold, then being set up, then — in the free table — the slots you could take, most recently used first. A slot id breaks ties, so a listing you are watching does not reshuffle between runs. The same order applies to --output=json. Text output is escaped. Every line printed for a person — tables, warnings, refusal messages — writes a character a terminal would act on or hide as a \uXXXX escape, or \UXXXXXXXX — eight hex digits — above U+FFFF, whether it comes from a branch name, a file name, a registry field or a hook’s error, so it is shown rather than obeyed. A suggested command that names something holding such a character quotes it as $'…', with each such character written as its UTF-8 bytes (\xHH), and ' and ! as bytes too, so pasted into bash — 3.2, macOS’s /bin/bash, included — zsh or ksh, it runs on the name as it is. These are the characters invalid_agent refuses in an id; a message’s own line breaks and tabs are kept. Under --output=json a refusal’s message is escaped the same way; other values, such as branch, agent and the files listed, are as they are, escaped only as JSON requires.

Flags

boolean
default:"false"
List every pool on this machine rather than this repository’s, each under its own heading. Works outside a repository too.Only the pool belonging to the clone you run from is checked against git, since a worktree listing answers for one object store; the others report the branch their own records hold, and the listing says so. Anything read from disk — whether a slot’s directory is still there, how warm its output base is — is current for every pool.
boolean
default:"false"
Also print, for each slot, its directory, the Bazel output base its path derives to (noting when it is not created yet, or has been reclaimed), the pid of any server holding it, and every holder the slot remembers, then each session they name with the command that returns to it.
string
default:""
The session asking, as given to add with the same flags, so sessions[].current in --output=json marks the right one. Needed only when the lease was taken with them; 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

detached is the commit-ish a --detach slot is at, and branch is then empty — name carries the (detached at …) label the tables print. base and base_sha say where a branch add --create made started: the ref as given, and the commit it was then, which a later fetch does not move. state is leased, free or reserved, which the tables print as in use, free and setup. warmth is the hot, warm or cold the tables print, decided from server_pid and output_base_present. sessions is the --verbose section as data: one entry per agent session any slot’s history names, current marking the session asking, each with its leases. pools is always a list, of one without --all, so .pools[].slots[] reads the same either way and nothing has to branch on the flag the command was called with. Each pool carries reconciled, which says how far its rows were checked: true means git was asked and agrees, false means the branch and the lease are what that pool’s own records hold. owner is the git-common-dir of the clone that owns the slot. git worktree add is clone-scoped, so a slot belongs to exactly one object store. owned_here is that compared against the clone you ran from, which is what decides whether a slot is usable. agent is empty for a worktree taken by a person. source says where the identity came from: flag for --agent-kind / --agent-id; env for an environment variable — the vendor-neutral pair, a harness’s own, or the inferred AI_AGENT; ancestry for the process tree. pid_start is carried beside pid because pids are reused and a lease outlives the process that took it. session_state is one of running, resumable, gone or unknown, and it is a claim about the process recorded with that lease rather than about the session: one session can take slots from several processes, so the same session id can read running on one row and resumable on another. resume is decided separately, by whether the harness’s store holds the session — which is why a row can be running and still offer no command. process says whether the recorded process itself runs: running, stopped, or unknown where none was recorded, and empty when no agent took the slot. session_project is the directory the harness filed the session under, where that can be established. agent.cwd is only where the command ran, which is a candidate for the same thing and not a statement about it. had_base records that a Bazel output base was once seen for this slot. It is what separates a slot nobody has built in from one whose state was reclaimed, which a lease count cannot: a slot leased twice without a build never had a base. adoptable is true for a slot recorded for a clone no longer at its path that this clone would take over — the clone moved note. unlisted is true for a slot leased for this clone’s path whose checkout this clone’s git does not list — the another clone's note. Both are false for a pool this clone does not own, where git is not asked. stranded is true for a slot recorded for a clone no longer at its path that this clone does not take over — the stranded note. For a pool this clone does not own, where git is not asked, only a leased slot reads as stranded: a free one is false there, since any clone of the repository may take it over. branch_verified says whether branch came from git or from the pool’s own record. ownership_known says whether owned_here was established at all — for a pool this clone does not own there is no clone to compare against, so a false there means “not asked” rather than “somebody else’s”. One ordering differs from the table: a slot’s history is oldest first here, since that is the order it was written in, while the table shows it newest first. output_base is derived, not asked for: Bazel names a base md5(workspace_path) under the output user root, so the pool can compute it without starting a server. It is where the slot’s warm state lives. output_user_root at the top of the document is the root it was derived under.
This command takes no lock and only reads, so it is safe to run while another aspect worktree add is in flight. It may show a view from a moment ago, never a half-written one.