rota

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 stalled slot. Reap reclaims dead slots 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 explicit rota round reclaim.
  • An unverifiable slot. 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 or rota 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 any park/* 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.

Edit this page on GitHub