Doctor and reap
Two verbs bracket a round. rota doctor checks the machine before it starts.
rota reap clears what the round left behind after it ends.
rota doctor
rota doctor # one line per check: status, name, detail
rota doctor --json # {"ok": true, "data": {"ok": bool, "checks": [...]}}
It is read-only. It never writes, never calls a usage endpoint (so it spends no quota) and runs
without .rota/, falling back to default config. Each line is pass, fail, skip or warn. Every fail
carries a hint: the one command or edit that fixes it.
Exit codes. 0 when every check passes, skips or warns. 1 when any check fails; --json still prints
the full result, so a caller reads data.ok. A missing tool is a failed check, not an error, so doctor
never exits 5.
| Check | Looks at | Skips when |
|---|---|---|
git |
git on PATH, and .worktrees/ gitignored |
never |
jq |
jq on PATH; the skills read fields from rota … --json output with it |
never |
host |
the host a round would run on: round.dispatch as named, or with it unset or subagent, herdr inside a herdr pane, else tmux inside tmux. herdr on PATH and 0.9.x, or tmux on PATH |
no host is detected (solo needs none) |
tracker |
the forge and the issue source together. The line reads forge github (from origin), issues file, naming the resolved forge (forge.provider, else origin; no remote is local) and issues.source. Fails when issues.source is github or gitlab but the forge is something else. Otherwise gh or glab on PATH and authenticated for the forge |
forge is local (no pull requests), or origin names neither GitHub nor GitLab and forge.provider is not set |
accounts |
every account in round.accounts has an existing dir whose credentials file holds an access token (on macOS the login Keychain entry is read when the file is missing, unusable or expired) |
no accounts configured. Warns when the orchestrator’s CLAUDE_CONFIG_DIR is not one of them |
hook |
herdr’s agent integration for each account (herdr integration status) |
the host is not herdr, or no account is configured |
statusline |
the effective statusline runs rota statusline dump |
hooks not installed (opt-in) |
stop-hook |
a Stop and a SessionStart entry marked # rota-hook, and the command resolves |
hooks not installed (opt-in) |
guard-hook |
a PreToolUse entry marked # rota-hook in project (.claude/settings.json) or user scope, and the command resolves; warns when Codex tiers are set (Codex workers are not guarded) |
hooks not installed (opt-in) |
switch |
with orchestrator.switchOnUsage on: the Stop hook and two accounts with a dir |
the key is off |
skills |
every installed skills root (user and project, Claude and Codex) matches the binary’s skill set, and has no missing or edited files | no root has a .rota-manifest.json (run rota skills install) |
config dir |
the global config dir in use and where it came from (ROTA_CONFIG_DIR override, XDG_CONFIG_HOME or the default). Warns when a relative ROTA_CONFIG_DIR was ignored |
never |
state dir |
the state dir that holds the project registry (projects.json) |
never |
external knowledge |
the path in knowledge.external is a git clone with a remote and a rota-knowledge.json. Fails when the path is missing, not a git repository or has no remote; warns when the policy file is missing (writes then default to push). See External knowledge |
nothing to report: the line appears only when knowledge.external is set |
labels |
tracker label definitions against the label registry, including configured names and project extras. Warns about missing or undescribed labels; hint rota labels sync previews changes |
no issue backend; an unavailable or name-only tracker (Jira, It’s a Plan) reports skip with its reason. The scan has a five-second limit |
codex |
codex runs, the default Codex home (or each codex account in round.accounts) logged in, herdr integration per home |
codex is not on PATH and no codex account is in round.accounts |
verify also appears only when something is wrong: it warns when test.full and test.e2e are both empty under a local verify (test.fullWhere is local), because the merge gate refuses to merge then (blockedBy: no-verify). The hint is run: rota config set test.full '[...]'. It never fails the run. rota init and rota round start print the same warning.
agents also appears only when something is wrong: it warns when a generated subagent file (.claude/agents/rota-*.md, and .codex/agents/rota-*.toml when Codex is configured) is missing or no longer matches the roles.* config. The hint is run: rota agents write. It never fails the run. See roles keys.
gitignore also appears only when managed ignore entries are missing, after agents when that warning is present. A complete managed .gitignore adds no row. A missing entry (including .rota/flakes.jsonl) reports warn, hints run: rota init, and leaves the exit status at 0 when no other check fails.
milestones also appears only when something is wrong: it warns when milestones live on the tracker but .rota/milestones/ still holds files from file mode, because no verb reads them. The hint is run: rota migrate milestone-files --apply. It never fails the run.
binary also appears only when something is wrong. Run from a checkout of rota’s own source (go.mod module github.com/l4ci/rota), doctor warns when the installed binary’s commit is not the checkout’s HEAD and HEAD changed non-test Go files under cmd/ or internal/ since it. The hint prints a rebuild command that keeps Version equal to VERSION, so the skills check still passes. rota round start prints the same warning, and rota round watch repeats it once per round. Elsewhere the check does not appear.
ports also appears only when something is wrong. For each live slot that holds a port block (see round.portBase), doctor lists the machine’s TCP listeners with ss -ltnp, else lsof, and prints warn ports when one inside the block belongs to a process whose working directory is outside that slot’s worktree, or whose owner cannot be read. It never fails the run; the hint names the config key that moves the range. Where /proc is absent (macOS) the process directory comes from lsof. When neither tool is installed or both fail, doctor prints skip ports saying the blocks were not checked.
disk also appears only when something is wrong. When the free share of the volume holding the project (else the working directory) is under doctor.minFreeDiskPercent (default 10; 0 turns it off), doctor prints warn disk, which never fails the run, and its hint names what rota left behind that would give space back: temp dirs a smoke or gate run leaked (rota-smoke.*, rota-gate-logs-*, older than an hour, in the temp root) and git worktrees whose directory is gone. rota reap lists the rest of the stale scratch worktrees. go-cache-disk is the same warning for the volume holding the Go build cache (go env GOCACHE), under the same threshold; a full cache volume fails a gate with no space left on device. Its hint is go clean -cache. See doctor.minFreeDiskPercent.
The hooks are opt-in, so statusline, stop-hook and guard-hook skip until rota hook install has written
something, and fail only on a partial or broken install. skills is opt-in the same way: it skips until rota skills install has written a manifest. switch cannot tell whether the orchestrator
runs under rota keepalive run. See unattended rounds. For codex, see
Codex workers.
Run it before rota round start, and again after changing accounts, hooks or the host.
rota reap
Rounds leave behind merged branches, worktrees no slot owns, tabs with no agent in them, and a
lease whose orchestrator died. rota reap finds them.
rota reap # preview: list candidates, delete nothing
rota reap --apply # delete the candidates that hold no work
rota reap --kind branch,lease # only these kinds
The default is a preview (warning: preview only; pass --apply). --apply needs no --confirm,
because everything it deletes is proven unowned and merged, and anything holding work is skipped.
| Kind | A candidate is | Deletable when |
|---|---|---|
worktree |
a .worktrees/<name> checkout with no live owner, or one git marks prunable |
it holds no work |
branch |
a local <agent>/<issue>-slug branch checked out nowhere and owned by no registered slot |
it is reachable from the base or origin/<base>, proven from git alone |
tab |
a herdr tab inside .worktrees/ whose panes run only plain shells |
always (tmux has no agent state, so it yields none) |
process |
a process under a tab with no agent | a real host lists none; only test fakes supply any |
lease |
the round lease, when its holder is gone | the staleness is proven again under the lease lock |
skillcache |
an extracted skill tree (rota/skills/<version>/ in the user cache) of a rota version other than the running one |
always; the running version’s tree and a tree being written are never listed |
What reap never removes.
- A running agent. A tab or process with a live agent under it is not listed.
- Work. A candidate with uncommitted changes, commits not on the base, or a branch not reachable from
the base is listed with
held: <why>and never deleted. There is no flag to override that. - A
stalledslot. Reap reclaimsdeadslots only: a worker in a long test run makes no commits and looks stalled, and reaping it would kill it. Reclaiming a stalled slot is an explicitrota round reclaim. - An
unverifiableslot. The host could not be asked (herdr unreachable, stale tab handle, pane not capturable), so nothing proves the agent is gone. Reap skips it; inspect the tab, then wait orrota round reclaim <slot> --force, which still refuses while the host cannot be asked at all. - A parked slot. A
park/<agent>worktree, clean or not, and anypark/*branch are never candidates. - A live or foreign lease. Only a stale one, whose holder is gone on this host.
Merged branches of adopted work. A slot adopted with
rota worker adopt is released after its PR
merges, and rota keeps the worktree and branch because another tool made them. When that branch is
merged but still checked out in a worktree outside .worktrees/, reap lists it as a branch with
held: checked out at <path>; remove that worktree first. Remove the worktree yourself (or let the
other tool do it), then rota reap --apply deletes the branch. rota worker gate --prune and
rota round wind-down --prune do both at release time.
An unreadable registry. rota reap and rota worker pool reap read .rota/workers.json to learn which
worktrees a slot owns. If the file exists but is not a JSON object, they refuse with reap refused: ...
fix or remove it and exit 5, before listing or deleting anything. Reading it as an empty pool would
treat every slot worktree as unowned. pool reap also keeps a slot registered when git cannot remove its
worktree, and names the failure.
Exit codes. 0 after a preview or a clean apply. 1 under --apply when a deletion failed (the
rest were still removed; the failures are in the output). 2 for an unknown --kind. 3 with no
project root. 5 when git fails or workers.json is unreadable.
Reap and rota round reconcile split the work:
reconcile reports drift between the registry, host, git and forge and repairs only the safe kinds;
reap deletes. After rota round wind-down, which deletes no branch,
run rota reap. It is not the verb for a registered slot: rota worker pool reap deregisters a named
slot, and removes its worktree and branch whether or not it holds work.