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_URL → ROOST_COORDINATOR_PUBLIC_URL → ROOST_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
- Fleet —
push,deploy, and coordinator database backups - Networking — the loopback listener and the private paths
- Security — keys, pairing, revocation, audit
- Quickstart —
quickstart,add-machine,status,doctor