The roost CLI

Every roost subcommand, the release-matched agent skill, every roost api verb, and how roost api authorizes itself.

Invoking it

roost <subcommand> [args]

roost --version and roost -v are aliases for roost version. An unknown or missing subcommand prints the usage list and exits 1.

From a source checkout the same entry point is bun apps/roost-cli/src/main.ts <subcommand> — which is how the fleet-update commands are documented, since they intentionally refuse to run from the standalone release binary (it contains no Git checkout).

Subcommands

Subcommand What it does
quickstart One-shot local install: validate --coordinator-url, then coordinator on its loopback bind, local worker, and browser pairing
coord Run the coordinator (server mode; used by the compiled binary)
worker Run the worker (server-side; compiled binary or supervised service)
keeper Run the keeper subprocess that hosts this machine’s PTYs
update Self-update a supported macOS or Linux binary from the latest GitHub release
version Print the Roost version
skill Write the exact release-matched ROOST agent skill to stdout; accepts no arguments and performs no installation
dev Start coordinator, worker, and web dev servers
test Run all tests in dependency order
deploy <host> Deploy a macOS or Linux worker from a source checkout
push Journaled update of the local coordinator and complete registered macOS/Linux fleet
keeper-refresh <host> --yes [--force-live] Re-spawn a host’s keeper — empty only, unless forced (destructive)
logs <coord|worker> Tail an app’s logs, --tail N (default: last 100 lines)
reset Nuke local state — database, keys, lock
state Print the state snapshot
cutover Migrate from the legacy coordinator.db to coordinator_v2.db
status Health readout: services, coordinator, the configured public URL, workers
doctor [--since] Anomaly digest from the error logs (default window 24h)
api <verb> Headless introspection and control (see below)
join Install and register a macOS or Linux worker; needs ROOST_COORDINATOR_URL and ROOST_BOOTSTRAP_TOKEN
add-machine Print a one-shot macOS or Linux enrollment command — --platform <macos|linux> [--label X]; the URL resolves ROOST_COORDINATOR_URLROOST_COORDINATOR_PUBLIC_URLROOST_WEB_PUBLIC_URL and refuses when none is set

--since accepts a number plus a unit, so 90m, 1h, 24h, and 7d are all valid. roost logs also warns when a log file has grown past 100 MB.

Notes on the destructive ones. keeper-refresh requires --yes, and it re-spawns only a keeper it can prove holds no channels: while any session on that host is live it refuses, leaving every PTY untouched. --force-live is the break-glass for a wedged keeper — it destroys every PTY that keeper hosts, so it prints exactly what it will end before acting, still requires --yes, and is refused when the keeper’s identity is unproven. reset deletes local state outright. push may use --targets to name the exact complete registered worker set, but cannot narrow the transaction to a partial fleet; --no-web retains the existing web bundle. See fleet for its convergence proof and rollback behavior.

Windows-specific host options that remain in the CLI are non-actionable in v0.5.0: no Windows package, installer, join script, manifest, or updater payload is published. Windows host install, enrollment, and update are paused, and a registered Windows worker blocks push. A Windows browser client remains supported.

Release-matched agent skill

roost skill writes only the canonical SKILL.md bytes bundled with that release. A source invocation reads skills/roost/SKILL.md directly; a compiled binary emits the byte-identical generated text embed. The command accepts no arguments and never edits agent configuration.

Install or update it manually. For OMP’s default user profile:

mkdir -p "$HOME/.omp/agent/skills/roost"
roost skill > "$HOME/.omp/agent/skills/roost/SKILL.md"

Restart OMP afterward so it discovers the file. For one project instead, write the output to .omp/skills/roost/SKILL.md. Updating the Roost binary does not replace either copy automatically; rerun the redirection when you choose to update the installed instructions.

roost api

roost api is the headless surface: it introspects and drives a live coordinator without a browser through the authenticated Connect service. It is useful both for scripting and for reproducing a UI bug from a shell; observed agent reads, waits, and fenced prompts use the same volatile status hub that feeds browser Sync.

Sessions and terminals

Verb Arguments
sessions — lists every session
spawn <workerFp> <folder>
kill <sessionId>
input <sessionId> <text> [--enter] — raw bytes after \n, \t, \r escape expansion; optional Enter
cells <sessionId> — structured scrollback rows
events <sessionId> [--secs N] — live wire-delta monitor, default 5 s
rename <sessionId> [title…] — an empty title clears the override
assign <sessionId> <workspaceId|-->-- clears the assignment
attach upload local files into a session and print each absolute path

Observed agent status and fenced prompts

Verb Arguments
agent-status <session> [--json] — reads one authorized session
agents [--json] — lists current status rows, sorted by session id
agent-wait <session> --until <comma-states> --timeout <duration> — waits on the exact current occupant
agent-prompt <session> <text> [--wait --until <comma-states> --timeout <duration>] — sends one occupant-fenced input

All four verbs use the CLI identity’s selected-dashboard authority. Missing and foreign sessions share the coordinator’s not-found response. The two read verbs use headered TSV; their source column renders an absent legacy source as legacy, so screen and legacy rows visibly retain promptable=false.

JSON is an explicit stable projection rather than a protobuf dump. Its exact keys are session_id, agent_id, state, message, status_epoch, occupant_id, source, revision, completed_revision, updated_at, and promptable; absent message, identity, and source values are null, and the revision/timestamp values are numbers. No PID is exposed. Epoch, occupant, and source are volatile observation and fencing state only: they do not identify a conversation or grant agent control. Roost owns no agent process, conversation, transcript, tool call, or approval model. See Agents and status for detection and precedence.

agent-wait accepts unique states from blocked,idle,working and an integral ms, s, or m timeout capped at five minutes. It first reads the current identified occupant, then performs one event-driven RPC. Output is exactly matched, timed_out, occupant_changed, or session_closed; every outcome except matched sets a nonzero exit code. The coordinator never polls or scrapes terminal output.

agent-prompt reads one promptable integration status, pins its exact epoch, occupant and revision, and calls SessionsPrompt. Text is the exact single argv value: it does not expand the backslash escapes accepted by input. It must be nonempty and at most 16,384 UTF-8 bytes. Quote shell whitespace as usual.

The three wait flags are all-or-none. States must be a nonempty unique comma-list drawn from blocked,idle,working; timeout uses the same integral ms, s, or m syntax and 1 ms through 5 minute bound as agent-wait. Output is:

input	<accepted|rejected|ambiguous>	<written_bytes>	<reason-or->
wait	<matched|timed_out|occupant_changed|session_closed>

The reason is at most 200 characters and written_bytes at most 16,397.

The second line appears only when --wait was requested and input was accepted or ambiguous; a definite rejection prints only the input line. Rejected or ambiguous input, or a non-matched wait, sets a nonzero exit code. Input and wait outcomes stay separate because an ambiguous PTY write may still be followed by an observed status transition. The CLI and coordinator never retry it.

This command does not address an agent through a hidden API. The worker accepts text only if the same integration process is still proved and its exact status is idle or working. At the final pre-beginInput check, a blocked, replaced, screen-only, closed, expired, or newer-revision target rejects before any keeper write; failure after admission can instead be ambiguous. The shared terminal encoder normalizes newlines and, when bracketed paste is active, strips ESC from the text and wraps it before appending one CR. input remains unfenced raw input with its existing escape expansion and optional --enter.

Workers

Verb Arguments
workers — lists workers and which are routable
worker-rename <fp|prefix|label> <newLabel>
worker-rm <fp|prefix|label> — deregisters the worker

Anywhere a worker is named you may pass its full fingerprint, a unique fingerprint prefix, or its label; an ambiguous match is refused with the list of candidates rather than guessed.

Workspaces

Verb Arguments
workspaces — lists workspaces
ws-create <workerFp> <name> <folderPath>
ws-update <id> [--name X] [--color Y]
ws-delete <id>
ws-set-sessions <id> <sessionId…> — at least one session id is required

Driving the live UI

roost api ui-state [--json] lists each browser page’s ephemeral authoritative active path; unresolved or client-only optimistic /s/:id is blank until admission/hydration schedules a fresh report. When an open route session identifies a folder, the report also carries a nonempty browser-owned folder key and typed portable LayoutDocumentV1 containing only admitted sessions. Human output prints the identity/path columns and a pane tree only when that document is present. JSON uses strict snake_case V1 in layout_document, or null off folder routes; runtime pane/split IDs, a second JSON layout string, focused pane IDs, and redundant visible-session IDs are not exposed. Human fields are bounded and escape terminal/control and bidirectional-format characters; JSON preserves each accepted string unchanged. Empty output means no UI report is currently retained; it does not inspect live target sockets. A recently closed page can remain in this five-minute TTL projection; the entry is discovery data, not proof of a live writable Sync socket.

roost api ui <command> [--tab <id>] accepts eight fire-and-forget commands: navigate <path>, place-split <sessionId> <anchorSessionId> <row|col> [--first], select-tab <sessionId>, focus-pane <sessionId>, move-tab <sessionId> <destSessionId>, arrange <even|rows|tiled|main-vertical|balance>, close-tab <sessionId>, and spotlight <sessionId> [--off]. --tab <id> may appear anywhere after the command name; duplicate/unknown options or wrong positional arity are errors. Each command prints exactly delivered=N. That number is the selected dashboard’s Sync-subscriber count at publication time, not an execution or per-tab acknowledgement count; targeting does not narrow it. One exact reported tab can instead apply a portable layout with acknowledgement:

roost api ui apply-layout <file> --tab <id>

The file must contain strict V1 layout JSON, and the nonempty --tab is required—there is no acknowledged broadcast. The CLI first reads the retained UI-state projection: no matching tab prints target_gone, while the same tab ID reported by multiple browser fingerprints prints rejected; neither case publishes an apply. One match pins that fingerprint/tab tuple into at most one apply RPC, so a later browser reusing the tab ID cannot take over a stale request.

Stdout is exactly one of applied, rejected, or target_gone; the latter two set a nonzero exit code. A stable sanitized reason, when present, is written to stderr. The CLI never retries the apply RPC. applied is emitted after the commit and a navigation attempt; it proves the commit, not successful navigation completion. target_gone means the exact fingerprint/tab/socket acknowledgement was unavailable after absence, close, replacement, or timeout; it does not prove that the browser did not execute before the acknowledgement was lost.

Task rows

tasks [--state X], task-enqueue <payload_json>, and task-cancel <id> read and write the coordinator’s task rows directly; task-enqueue parses the payload first so a malformed one fails at the CLI instead of becoming an opaque queue row. Roost ships no first-party runner that dequeues these rows, so treat them as a durable queue primitive rather than an automation feature.

Device revocation from the host

roost api device-revoke-local <fingerprint> --yes

This is the escape hatch for having lost every authorized browser. It is destructive, so --yes is mandatory, and it only talks to an http://127.0.0.1:<port> coordinator URL — no credentials in the URL, no path, no query, no fragment. The port comes from ROOST_COORDINATOR_BIND (default 127.0.0.1:4103) or an explicit ROOST_COORD_URL. Anything else is refused before a request is made.

How roost api enrolls its key

roost api always signs with its path-isolated ~/.roost/cli-key; it never borrows a worker credential. It confirms the key is authorized with one cheap protected call (WorkersList), and every unary and Sync request then reaches the whole install.

An unknown key can enroll automatically only while the CLI is running on the coordinator host: the host mints a scoped one-shot browser grant and the CLI redeems it through the normal browser-redemption RPC. A fresh remote CLI instead stops with explicit pairing-required guidance. A loopback or tailnet address is not a credential and never authorizes the key by itself.

Two verbs that were removed

cat and watch are retired and print an explicit message rather than failing oddly: use cells for scrollback and events for a live output stream.

Next

  • Fleetpush, deploy, and coordinator database backups
  • Networking — the loopback listener and the private paths
  • Security — keys, pairing, revocation, audit
  • Quickstartquickstart, add-machine, status, doctor