Rove shell control

SkillCommunication

Use rove api to manage local coding tasks. Managed Tasks created by API automation own a git Worktree and branch plus Hosted PTY engine tabs; project main and directory Tasks reuse existing directories. API automation works without an open TUI; prompted send and add ensure a target engine tab.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Rove shell control skill

About this capability

Use when controlling Rove tasks, parallel coding attempts, hosted agent sessions, task lifecycle, or the daemon-owned issue tracker from a shell. Also the ONLY channel for messaging another agent session on this machine — `rove api send`, never a peer/MCP side channel.

What this skill tells your AI

The instructions your AI receives, as published by sma1lboy/rove in claude-plugin/skills/rove/SKILL.md and read by ahel’s review.

Use rove api to manage local coding tasks. Managed Tasks created by API automation own a git Worktree and branch plus Hosted PTY engine tabs; project main and directory Tasks reuse existing directories. API automation works without an open TUI; prompted send and add ensure a target engine tab.

Inside a Rove session, Rove verbs come first

Check where you are before choosing how to delegate or parallelize:

test -n "${ROVE_TASK_ID:-}"

When that passes, you are an engine session Rove manages — $ROVE_TASK_ID is your task, $ROVE_TAB_ID your tab. Coordination should then go through Rove, not around it, because work routed through rove api gets what ad-hoc subprocesses never do: its own Worktree and branch (no file collisions with you), a sidebar row with live state the user can watch, lifecycle tracking, and an explicit outcome contract.

  • Parallel attempts of one prompt → add --count N, not N hand-rolled subagents.
  • Delegating a scoped piece of work → add --prompt, not a raw claude -p child the user cannot see or manage.
  • Following up on a task you started → send; comparing → collect; finished your own task and were spawned by another → a bare send (no --task-id) replies to the DISPATCHER — the exact task + tab that created you, recorded at creation (.task.dispatcher on get-task). If that tab died it falls back to the dispatcher task's live canonical engine tab; nothing alive fails loud (DISPATCHER_UNREACHABLE) — it never spawns a new engine, so a failed reply is visible, not fake-ok.
  • Messaging another agent session on this machine → send, and ONLY send — never relay through the user, a side file, or a generic peer channel (an MCP server offering to message "other instances" is exactly that channel: it reaches a process, not a task, so nothing it delivers is attributable, watchable, or replyable). Sent from inside a Rove task, the prompt arrives prefixed [ROVE PEER] from "<title>" (task <id> — Rove agent skill /rove, read it once per session …), so the receiver knows who is talking, that this skill is required reading, and how to answer — the baked-in reply command is tab-precise (--task-id <sender> --tab <sender's tab>), so peer conversations need no coordinator and no human relay. That prefix is the contract: do not strip it with --plain for coordination messages (--plain is only for a verbatim paste the receiver should treat as content, not conversation). Received a [ROVE PEER] message yourself? Read this skill once per session. The baked-in command identifies where a necessary reply goes; it is not an instruction to acknowledge every message. Act on FYIs without replying.
  • send carries text, but that text can carry FILES: peers share a filesystem, so put the absolute path of a screenshot, log, diff, or any artifact in the prompt and the receiver opens it with its own Read tool — images included. An annotated screenshot beats a paragraph describing one; prefer "see /path/to/shot.png — the arrow marks the broken card" over re-narrating pixels. Paths under the sender's Worktree work too (worktrees are world-readable locally); just never expect the receiver to WRITE there.
  • dispatch stays the dispatcher's verb (deliver-only into an already-hosted session; never impersonate the user in someone else's terminal).

Your own engine's in-context subagents remain fine for read-only research/exploration inside your task — the boundary is WORK: anything that edits files, runs long, or the user should be able to see and steer belongs in a Rove task. Do not recursively fan out from a spawned task.

When the check fails, none of this applies — use rove api only if the user asks for Rove by name.

Vocabulary — what the user's words map to

TermWhat it isIsolation it givesUsers also say
Taskone tracked workspace record — managed worktree, saved-project main, or existing directorymanaged Tasks own files + branch; main/directory Tasks reuse files"a task", "a new one", "a separate attempt"
Worktreethe isolated git working tree a managed Task owns (.task.worktreePath) — a filesystem address, never shown in the UI"workspace", "this checkout", "this branch", "here"
Terminal Tabone engine, shell, command, or content surface inside a Taskan engine tab has its own conversation, but every tab uses the SAME Task files"tab", "chattab", "another chat", "a second agent on this"
Splitthe tree that divides ONE Terminal Tab into several regions (the pane-open verb's unit; a leaf is not called a pane)none — same session's screen, same files"split it", "side by side", "put the logs next to it"

Two of those colloquialisms are traps, so read them as INTENT, not as product terms: in Rove's own vocabulary Workspace is the center Terminal Tab region of the UI (CONTEXT.md), not a checkout, and ChatTab is retired vocabulary for Terminal Tab. A user saying "in this workspace" means the worktree they are looking at — answer the intent, keep writing the real term.

They nest — Task ⊃ Terminal Tab ⊃ Split — and isolation drops at every level down:

new Task   → own worktree + own branch     (parallel work can't collide)
new Tab    → own engine session, SAME files (a helper in the same checkout)
new Split  → same session's screen, one tab divided (a monitor beside the work)

The distinction that decides every routing call: a new tab shares the worktree and branch; only a new task gets its own. Two tabs in one task edit the same files, so they can collide — that is a feature when the user wants a helper in the same checkout, and a bug when they wanted parallel attempts. A split isolates nothing at all: it is a layout, for watching something (logs, btop, a test loop) next to the work — never the answer to "do this work".

One boundary the isolation model does NOT cover: never git stash in a managed worktree. The stash stack lives in the repo's common dir (.git/refs/stash) and is shared by every linked worktree — two tasks that stash in parallel can pop or drop each other's work. Commit instead; a commit is per-branch and isolates exactly the way the model promises.

Where does this work land?

Inside a Rove session ($ROVE_TASK_ID non-empty — check first: it is what makes the tab/split rows addressable at all), match top to bottom and take the first row that fits:

The user saysLands inCommand
"you do it", "just fix it", "change X to Y"you, right hereno Rove verb — edit the files yourself
"try it N ways", "compare approaches"N new tasksrove api add --repo "$PWD" --count N --prompt "…"
"split", "side by side", "keep an eye on X while…"a new region in the CURRENT tabrove api pane-open --command "…"
names a tab: "tell the agent in tab 3"that exact tabrove api send --task-id <id> --tab tab-3 --prompt "…"
a LOCATION word: "in this workspace/worktree/checkout", "on this branch", "here", "same task"THIS task, a NEW Terminal Tabrove api send --task-id "$ROVE_TASK_ID" --tab new --prompt "…"
names an ENGINE for the same files: "let codex take over here", "try this one with claude instead"THIS task, a new tab pinned to that enginerove api send --task-id "$ROVE_TASK_ID" --tab new --command codex --prompt "…"
anything else — no row above matcheda NEW task — new worktree + branchrove api add --repo "$PWD" --prompt "…"

Order is the tiebreak: a count ("3 ways in this workspace") beats a location word, and delegation language loses to "you do it" — the user asking YOU is not asking for a fleet. Two more rules the table can't show:

  • No location word at all ⇒ new task. It is the only routing whose isolation cannot corrupt work-in-progress, so it is the safe default.
  • "this repo" / "this project" are NOT location words — they name the repo, not the checkout. Only worktree-scoped words route to a tab.

Outside a Rove session none of this applies: there is no "this task" to add a tab to, so add (single or --count) is the only routing available.

Know where you are before you route

echo "$ROVE_TASK_ID / $ROVE_TAB_ID"          # who you are (empty = not a Rove session)
rove api get-task --task-id "$ROVE_TASK_ID"  # .task.title, .task.branch, .task.id, .running, .tabs[]
                                             # .task.worktreePath too — only if you are about to read/write its files

get-task is the per-task read that answers "what is my worktree, my branch, and which sibling tabs exist" — .tabs[] carries each tab's id, kind, vendor, liveVendor, lastTitle and alive, which is exactly the target list for send --tab. A tab flagged unregistered: true is a live session the tab snapshot lost; it is addressable like any other.

Refer to a task the way the user sees it

The sidebar row is the task's title; the line under it is its branch. Nothing in Rove's UI ever renders worktreePath, so the directory name in it (~/.rove/worktrees/<repo>/marlin) is a filesystem address the daemon picked from an animal pool — the user has never read that word and cannot find it on screen. Never name a task by its directory to a user or in a report.

Use the title when a human reads it, the task id (or its last six characters) when you need it to be unique, and both when you need both:

  • marlin opened a PR · landed in zorilla · see mammoth's branch
  • "Skill version guard…" (task …CWWA) opened PR #972
  • succeeded: guard now fails the build (branch fix/skill-version-bump)

worktreePath earns a mention only when the sentence is about files on disk — cd, a path in a command, a file you edited.

Found a defect in ANOTHER project? File a request, don't work around

The repos on this machine are deliberately interdependent — one project using (and stumbling over) another is the normal case, not the edge case. Each saved project can keep a main task: a long-lived resident agent on that repo's own checkout. It is more than "task kind = existing directory" — it is that project's standing inbox, addressable from any other repo's session, and it dispatches what it receives.

So when work in repo A surfaces a defect in product B, the default is to file a request with B's main task — not to quietly patch around it in A. The test: would this bite someone else? A local workaround fixes only you; a fix in B fixes everyone. Work around locally to unblock yourself if you must, but file the report either way.

rove api list                                   # find B's main task id
rove api get-task --task-id <their-main>        # .tabs[] → the engine tab's id
rove api send --task-id <their-main> --tab tab-N --prompt "<report>"

Address the engine tab explicitly (--tab tab-N from get-task's .tabs[], kind == "engine"): a long-lived main task's engine tab is often not tab-1, and an un-addressed send may refuse with NO_ENGINE_TAB rather than guess.

A report worth sending carries: symptoms (what happened, concretely, how many times), root cause if you found it, why it is worth fixing (who else it bites), and suggestions without prescribing the implementation — the receiving project decides how to fix its own product.

Send the report — never add a task into someone else's repo. You hold the problem-side context (symptoms, root cause, impact); only that project's main agent holds the solution-side context (code structure, existing issues, what's mid-flight, where this slots in the schedule). A task you compose would bake YOUR guess at the fix into the brief, and that guess is usually wrong — one field report here looked like "add a line to the docs" and actually split into a docs fix plus a runtime-detection issue anchored in files the reporter didn't know existed. You raise the problem; their main agent decomposes it into tasks. That translation is the main task's whole job, and it is why every project should keep one.

Fresh worktrees start empty — install before you judge

A managed task's worktree is a brand-new checkout: no node_modules, no build artifacts, nothing a lockfile promises. Two consequences:

  • A test failure in a fresh worktree may be fake. Missing dependencies masquerade as product bugs ("Could not resolve: react-dom/client" reads like a regression, not a missing install). Before reporting any failure as real, confirm the repo's install step ran — and when a failure looks unrelated to your change, compare against the same command on the base branch before believing it.
  • Repos with an install step should ship .rove/init.sh — it runs once per worktree, before the engine, in the worktree (per-user override: rove repo set --init-script; inspect with rove repo show, which lists .rove/init.sh: absent when unset). Working in a repo that lacks one and you just paid the install tax? Suggest adding it.

Discover before calling

Do not guess flags — but do not pay a round-trip for the ones you use every turn either. These five carry almost all traffic:

add      --repo(REQ) --prompt --title --command --count --agents --activate
send     --prompt|--prompt-file(REQ) --task-id --tab --command --plain
get-task --task-id(REQ)          list  (no flags)
collect  --group <groupId> | --task-ids <csv> | --repo

Four names that have actually been guessed wrong here: add --vendor is --command; read-output --task is --task-id; dispatch --text is --prompt (--text belongs to note); issue-list has no --state at all — filter its JSON yourself.

references/api-flags.md is every verb and flag. Find your section by what you were ASKED, not by Rove's noun for it:

You are asked to …Read section
schedule / recur / cron / "every morning" / "keep messaging one window" / 定时 / 每天自动routines
a GitHub issue, "file it upstream" / 提 issueworkitems
track work in Rove's own backlog, move a kanban cardissues
remember this for the repo, leave a field note / 记一笔drive
what did the agent do, per-turn cost, read its screenread
make / rename / retarget / land / close a taskcreate / edit / lifecycle
materialize or adopt a checkoutworktree
which engines exist, send Rove product feedbackdiscover / feedback
an error code came back, nextCommandArgsError codes

Reach for schema when the binary and that file disagree.

rove api schema --verb add    # or --group create, --all
rove api <verb> --help
rove api engine-list          # what you can launch, and with what command

Commands emit one JSON object; errors use {"error":{"message","code",...}} on stderr. Common rejections also carry hint (what to do) and nextCommandArgs (argv for the same Rove executable — run rove <args...> verbatim to recover, e.g. ["api","list"] after TASK_NOT_FOUND). Add --pretty for readable output.

Common operations

# Create one task and start its first engine turn.
rove api add --repo "$PWD" --title "focused title" --command claude \
  --prompt "<complete scoped instruction>"

# Parallel attempts of the same prompt (hard cap 10; prefer 3-4).
rove api add --repo "$PWD" --count 3 --prompt "<prompt>"
rove api add --repo "$PWD" --agents claude:2,codex:1 --prompt "<prompt>"

# Follow up. Use an explicit id for unattended work; the active task can drift.
# From inside a Rove task this auto-prefixes [ROVE PEER] provenance
# (sender + reply command) — `add --prompt` wears it too, so a dispatched
# task's opening brief carries its reply address; --plain sends verbatim.
#
# A prompt with backticks, $vars, or quotes goes through --prompt-file, NEVER
# a double-quoted --prompt: in double quotes `rove api send …` is command
# substitution — the shell RUNS it and ships its output, and the words vanish.
# Single quotes block $ROVE_TASK_ID too, so there is no quoting that fits both.
rove api send --task-id <id> --prompt "<complete next turn>"      # plain text only
rove api send --task-id <id> --prompt-file - <<'EOF'              # anything else
<turn — backticks, $vars, quotes, multi-line, all verbatim>
EOF

# Reply home: no --task-id inside a dispatched task = the dispatcher's tab.
rove api send --prompt "succeeded: <one line> (branch <final branch>)"

# A task can hold several Terminal Tabs. `get-task` lists ONE task's tabs (the
# usual read before addressing one); `inspect` is the wider diagnostic — every
# task's snapshot plus daemon activity and live pty sessions.
rove api inspect --task-id <id>
rove api send --task-id <id> --tab tab-3 --prompt "<turn>"  # exact alive tab
rove api send --task-id <id> --tab new --prompt "<turn>"    # fresh engine tab
# Same worktree, DIFFERENT agent — the API twin of the TUI's ctrl+e pick. The
# engine is pinned to that tab (survives restarts, unaffected by a later
# set-command) and the task's own engine is left alone. --tab new only.
rove api send --task-id <id> --tab new --command codex --prompt "<turn>"

rove api get-task --task-id <id>
# One read for the whole round — the groupId `add --count` returned. Never
# hand-copy N task ids across turns: collect by group. Lost the groupId? Any
# sibling task's `.groupId` in `list` output recovers it.
rove api collect --group <groupId> --pretty
rove api list --pretty

.running is true / false / null. It means an ENGINE PROCESS is alive in one of the task's engine tabs — a live shell, command, or content tab alone does not count, and neither does an engine tab whose engine exited (the PTY survives it as a login shell; each tab reports that as engineAlive). null means the pty host could not be asked: "couldn't look", NOT "nothing is running". Never act on null as if it were false — deleting a task on it destroys a worktree that may hold live work. It is process truth, not progress: a task whose work is merged and whose worker has signed off still reads true until somebody deletes it (see "A task is finished when it is GONE"). Omitting BOTH --task-id and --tab inside a task that has a dispatcher targets that dispatcher's tab (see the reply rule above); otherwise the target is the active task. Omitting only --tab targets a live engine tab (tab-1 first, then any surviving engine tab). Trap: omitting only --task-id while giving --tab tab-N inside a dispatched task delivers to tab-N of the DISPATCHER's task — the dispatcher's id fills in for --task-id, but an explicit --tab is kept, so your send --tab tab-3 lands in the middle of another session's tab-3, not yours. Target your own task's tab with --task-id "$ROVE_TASK_ID" --tab tab-N. Only when the task has NO live session at all does send auto-start the canonical engine in the task's worktree (started: true in the result marks that fresh session). If live tabs exist but none resolves as an engine, it refuses with NO_ENGINE_TAB — address one with --tab tab-N or spawn one with --tab new; it never silently spawns a duplicate engine. Its hint names pty-list — the live-PTY read (key, alive, pid, command); use it when .tabs[] and reality disagree.

Terminal panes

Split the workspace terminal the user is watching (tmux-style) or open a separate command tab — the attached TUI performs it, so this is a no-op headless:

# Split the focused tab; the pane runs the command through your login
# shell's `-ilc` (so it sees the same rc-exported PATH the engine tab does)
# and closes when it exits. Omit --command for an interactive shell.
rove api pane-open --command "btop"
rove api pane-open --direction down --command "watch -n1 git status -sb"
rove api pane-open --placement tab --title logs --command "tail -f app.log"

# Close panes you opened, by their --title (engine panes are never closed).
rove api pane-close --title logs

# Close one whole Terminal Tab by the id from `get-task .tabs[]`.
# This works with or without an attached TUI.
rove api tab-close --task-id <id> --tab tab-3

# Toast a one-liner in every attached Rove UI — surface "done / needs input /
# error" moments without touching any session (kinds get severity styling).
rove api notify --title "build green, artifacts in dist/" --kind done

Defaults: the caller's own task ($ROVE_TASK_ID, then the active task), --placement split, --direction right. Alternate right/down to build a grid; screen size bounds splitting — a split that would shrink any pane below the minimum usable size (20×6 cells) falls back to a tab. Panes land in the USER'S live workspace — open them when asked (monitors, logs, dashboards), don't scatter panes for work add should own.

Lifecycle

VerbPurpose
rename --task-id ID --title TRename a task
set-branch --task-id ID --branch BRename its branch
set-command --task-id ID --command CMDChange the engine launch command for the next launch
set-status --task-id ID --status SSet the lifecycle LABEL (backlog/in_progress/in_review/done/canceled/error). Cosmetic: the row, its Worktree, its branch and its engine all stay. canceled does NOT close or clean up anything
pin --task-id ID [--pinned=false]Pin/unpin
set-active --task-id ID / --noneChange shared active task
ensure-worktree --task-id IDMaterialize without starting an engine
land --task-id ID [--strategy merge|squash] [--delete-branch] [--remove-worktree=false]Merge the task's branch into the base repo's current branch; the Worktree is removed by default (--remove-worktree=false keeps it). The branch always stays; dirty/self/base removals are refused, outcome in the result's worktree field
delete --task-id ID [--force] [--delete-branch]Remove task + Worktree; the git branch stays unless --delete-branch (and --force never implies it)
discover-adoptable --repo PATHFind untracked Worktrees
adopt --repo PATH --worktree PATHImport a Worktree

"Close this task" means delete

delete is the ONLY verb that ends a task: it removes the row and its Worktree, and the git branch survives as the durable record. set-status canceled is a label — the row, Worktree, branch and engine all stay, so a "close" done that way changes nothing the user can see. Reach for delete whether or not the work merged; an unmerged branch is still on disk afterwards, which is what makes this recoverable.

There is no "hide the row without deleting" verb (archive was removed).

Deleting still needs the user to ask for it in that turn — say what you would remove and wait. --delete-branch (or --force on a dirty Worktree) destroys the recoverable half and needs its own explicit authorization; the two flags are never implied by one another.

Issue tracker

Issues are daemon-owned, not repo files:

rove api issue-list --repo "$PWD" --pretty
rove api issue-create --repo "$PWD" --title "title" --body "context"
rove api issue-set-status --repo "$PWD" --id <n> --status done
rove api issue-update --repo "$PWD" --id <n> --title "new" --body "body"
rove api issue-update --repo "$PWD" --id <n> --task <taskId>   # link; `--task none` unlinks

Kanban semantics

The TUI and web render issues as a Backlog / In progress / Done board whose columns derive from the issue's own lifecycle — do NOT move cards with issue-set-status doing:

  • In progress = the issue has a linked task; issue-update --task <taskId> IS the move (typical flow: issue-createadd a task → link them).
  • Done = status done; the daemon mirrors it automatically when the linked task finishes.
  • Backlog = everything else (open/doing/hold, unlinked).

Choosing the engine (--command)

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
122
Forks
8
Last commit
Sep 2026

ahel recommends instead

Advanced
Catalog kind
skill
Gateway key
rove-sma1lboy
Source
github.com/sma1lboy/rove