Appearance
Teams
The Teams view in hive-app lets you spin up multi-agent teams. A team is one leader plus zero or more worker agents, all coordinated through the daemon. Every team member runs as a headless agent runtime - Claude Code, Codex, OpenCode, or Hive's native OpenRouter API loop - rather than an interactive terminal session, so the leader and its workers behave like one orchestrated agent swarm instead of a set of terminals you drive by hand.
A team is persistent. It has no "completed" state: finishing a round of work leaves the team, its roster, and its instructions in place for the next round. The only way a team ends is Delete.
Teams differ from Tasks in three ways: they are long-lived agents (not one-shot shell commands), they support push messaging between leader and workers (each message arrives as a real user turn in the recipient's conversation - no inbox polling), and they record per-worker results so the leader can aggregate work programmatically.
The worker roster shown by Hive is authoritative. A Hive worker is a real team session with its own UUID, transcript, status, and result. Provider-native Task subagents or .claude/agents definitions are not Hive workers and never appear in this roster. When you ask a Hive leader for "workers", "teammates", or "delegates", Hive instructs it to create managed workers through team_assign or team_handoff, verify their UUIDs with team_status, and report coordination failures instead of claiming unregistered workers are ready.
Managed members use a headless provider. A worker needs a headless backend;
claude,codex,opencode, andopenrouterhave one. Spawning a member withcopilot/llama/customis rejected withprovider '<x>' has no headless backend yet. Those providers remain selectable for a leader, which then falls back to the interactive PTY path (see AI providers below).
The Teams view
Route: /teams, sidebar icon: people.
At-a-glance roster and health dashboard
Each team is rendered as a dashboard card with:
| Element | Notes |
|---|---|
| Name, status, and last activity | Click anywhere on the card to open the detail view |
| Health counters | Total agents, worker count, and successfully completed workers |
| Coordinator | Leader name and provider badge, or External orchestrator |
| Roster | Up to four workers shown as compact status chips: amber for pending, green for success, red for failure, plus a count for additional workers |
| Latest summary | A short preview when the leader has published one |
| Actions | Add Worker and Delete |
The header offers a status filter (all / active / inactive) and the New Team button.
Keyboard
- Click or press
Enteron a team card to open the detail view.
Team detail view
Route: /teams/:id.
On desktop, the detail view splits into two columns:
- Left column - team metadata (status, worker count, timestamps, latest summary), the Leader card with provider badge, and the Workers card. Clicking a worker row expands its reported result inline; the external-link icon jumps to the worker's transcript. Because workers are headless agents, their output renders as a chat transcript (the same shared renderer used by Chats), not as a terminal. Add Worker is available in the top bar and in the empty worker state, using the same dialog as the Teams list.
- Right column - a sticky Team chat card with the message count, live status, conversation, and composer. The target dropdown picks who the message goes to:
- Leader (default) - pushes the text into the leader's conversation as a framed user turn (
[Hive team "<name>" - message from <sender>]). The leader runs headless (SDK stream-json), so it answers right in the Messages panel: when each turn completes the daemon forwards the leader's final reply back as aTeamMessage. Your own prompt shows as You; the leader's reply shows with the crown. This is how you kick off a freshly-created team and converse with the leader. A Copilot, llama, or custom leader keeps the interactive PTY path, so it receives input but replies only in its terminal. - All workers (broadcast) / a specific worker - relayed by the daemon via
RelayTeamMessageand pushed into each target agent's conversation as a framed user turn (and persisted to the outbox for crash-recovery replay). A busy worker sees the message queued after its current step; an idle or not-yet-started worker is booted by the first message. Broadcasts go to every worker except the sender, so a team with no workers has nothing to receive them.
- Leader (default) - pushes the text into the leader's conversation as a framed user turn (
You can open and message a team through any cluster node. Hive routes RelayTeamMessage through the node that owns the target team member before it persists the chat echo or reports delivery. A member appears in delivered only after its live session accepts the input. If the member is unavailable or its agent process rejects the input, Hive places it in undelivered and shows the delivery error in the app instead of silently treating the message as sent. History loads use the same ownership rule: the connected node routes GetTeamMessages to a member-owning daemon, so another machine connected to the cluster sees the same durable conversation.
On mobile, Team chat comes first, before team metadata and the worker roster. Operator messages are right-aligned primary-color bubbles; leader and worker messages are left-aligned bubbles labeled with their sender and time. Leader and worker replies use the same Markdown renderer as Chats, including headings, lists, links, tables, and fenced code blocks. Operator messages remain plain text. The target picker and message input stay pinned below the scrolling conversation, so sending a message does not require scrolling past the roster.
Live agent status in Team chat
Team messages only arrive when a turn finishes, so a working agent used to look identical to an idle one. The chat now reads the members' live agent output and shows what is happening between messages:
- Status strip above the conversation - one chip per member (Leader first) with its current state: Working, Needs approval, Error, Stopped, Idle, or Waiting (nothing received yet). A working chip names the current step, e.g.
Running Bash. - Typing indicator at the bottom of the conversation - a pulsing bubble per member that is mid-turn, labeled with the step (
Thinking,Writing reply,Running <Tool>) and the elapsed time, so a long turn is visibly running rather than a silent gap. The card header also counts how many members are working. Reopening the detail view does not reset a working member's timer: Hive resumes it from the newest durable incoming Team message timestamp, or falls back to that member's start time and then the team start. - Diagnostics above the composer - a collapsed row that expands into what the agent CLI reported and the message log cannot show: tools denied for want of permission, error results, agent stderr (last lines), and the exit code when a member's process ended.
The state comes from each member's stream-json output, which the team view already subscribes to; nothing extra is spawned or polled.
Existing team chats open at the latest message. New messages keep the view at the bottom while you are following the conversation. If you scroll up to read older messages, Hive preserves that position instead of pulling you back down; scroll to the bottom to resume following new messages. The same behavior applies when opening leader or worker agent transcripts: each newly selected agent chat starts at its latest content, then preserves your position after you scroll up.
The daemon also stores every operator-visible prompt, relay, and leader reply in the team's durable chat history. When a desktop or mobile client opens the team, it requests that history from the daemon, so a conversation started on mobile appears in the desktop app and survives client reconnects and daemon restarts. Live events and history rows carry the same stable message ID; clients merge them by that ID so a message received during history loading appears only once.
Relayed messages are still written to the target session's output ring as a __STATUS__: control frame for live attached client views. The agents never parse those frames - they act on the pushed user turn. The durable team chat history, rather than a single client's subscribed output ring, is the source of truth for the Messages panel.
Team and session changes are pushed to every connected client. A worker created from the leader's MCP tools, the CLI, another desktop app, or another cluster node therefore appears in every open roster without a manual refresh. An older list response cannot replace a newer roster update.
Top-bar buttons: Add Worker, Edit (name, per-team instructions, permission posture - see Team instructions), and Delete (kill all sessions and remove the team). There is no Complete button: a team is a persistent workspace, and a leader records progress with team_summary instead of ending the team.
AI providers (TeamAgent)
Every session in a team carries a TeamAgent value chosen at creation time. The kind discriminator selects which provider the daemon launches; the daemon's [agents] config supplies binaries for CLI providers. OpenRouter uses the daemon's native API backend instead.
| Kind | Spawn (default) | Leader | Worker | Notes |
|---|---|---|---|---|
claude | claude | yes (headless SDK) | yes (headless SDK) | Honours --model. Long-lived bidirectional process. |
codex | codex | yes (headless SDK) | yes (headless SDK) | codex exec --json; one process per turn, resumed via the thread id. Honours --model. |
opencode | opencode | yes (headless SDK) | yes (headless SDK) | opencode run --format json; one process per turn, resumed via the session id. Model ids use OpenCode's provider/model form. |
openrouter | native HTTP API | yes (headless SDK) | yes (headless SDK) | Hive streams OpenRouter directly, preserves API conversation history, and executes workspace tools. Uses native model ids. |
copilot | gh copilot | yes (PTY only) | no | GitHub Copilot CLI. Honours --model. |
llama | ollama run llama3 | yes (PTY only) | no | Local llama runner; honours --model. |
custom | (user-supplied) | yes (PTY only) | no | Free-form binary path + args - used for any provider not yet first-class. |
A claude, codex, opencode, or openrouter member is launched in headless mode. The daemon translates the team's permission posture into that provider's flags or native tool permissions and wires Hive coordination. OpenCode uses --auto or --agent plan --auto. Native OpenRouter exposes read-only tools for plan, file writes for accept-edits, and command execution for bypass. Its hive_team function calls the same Hive coordination CLI used by the other providers' MCP tools. A leader on any other provider has no headless backend, so it is launched as an interactive PTY session instead.
Per-session arguments from the CLI/UI are appended after the daemon's configured defaults, so daemon-level flags survive across all team sessions on that node.
Mixing providers and models
The leader and each worker hold their own TeamAgent, so one team can run a Claude leader with a mix of Claude, Codex, OpenCode, and OpenRouter workers, each on its own model. The Add Worker dialog exposes the agent, the model, an optional capability tier, and a free-form role label; the leader can do the same through team_assign / team_handoff.
Model catalog and tiers
The daemon builds a per-provider catalog and caches it (default 6h, [models] refresh_hours). Sources, lowest priority first:
- Curated - the list bundled with the daemon, so a picker is never empty.
- Config -
[models]entries an operator added inconfig.toml; these also override a bundled model's label and tier. - OpenRouter - the keyless public model index, which is how ids released after a Hive build still show up. Disable with
[models] openrouter = false. - Provider probe - Anthropic / OpenAI
/v1/modelswhen an API key is in the daemon's environment (ANTHROPIC_API_KEY,OPENAI_API_KEY, or an API-key~/.codex/auth.json),opencode modelsfor OpenCode, andollama's/api/tagsfor llama. OpenRouter uses the public OpenRouter catalog and includesopenrouter/autoas its automatic choice.
Every entry carries a tier: fast (mechanical work), balanced (ordinary implementation), or deep (planning, architecture, hard debugging). A member spawned with a tier and no explicit model gets that provider's recommended model for the tier, resolved on the node that runs the agent - so the choice keeps working as models are released. An explicit model always wins.
hive team models [--provider claude] [--refresh] prints the same catalog; the leader sees it through the team_models MCP tool.
Team instructions
Each team carries two operator-editable instruction fields, edited from Edit on the team detail view (or UpdateTeam on the wire):
- Leader instructions - layered on top of the built-in coordinator prompt.
- Worker instructions - applied to every worker, including ones added later.
They are prepended to the recipient's next message inside a [Team instructions] block, so an edit re-steers already-running agents from the next turn onward - no respawn, no daemon rebuild, no redeploy. Clearing a field restores the built-in prompt. Both fields are persisted with the team and restored on daemon restart.
Use them for standing orders: "reviewers get a deep model", "run the test suite before reporting", "never touch the migrations directory".
The leader's intake
A team with no leader instructions yet is treated as un-briefed: the leader is told to interview you before it spawns anything. On its first turn it sends one numbered message - goal and definition of done, autonomy (plan first / ask before merging / full auto), cost posture (max parallel workers, default tier, whether deep is allowed), provider mix, setup (target branch, build and test commands, off-limits paths, shared services), and reporting cadence. Every item carries a default, so "defaults" is a valid answer.
The leader then writes your answers back as team instructions, which is what retires the intake and what every worker - including ones spawned days later - inherits. Filling in Leader instructions yourself before the first message skips the interview entirely, as does answering it up front in your opening prompt.
Permission modes
A headless agent has no human at a terminal to answer permission prompts, so each team fixes a permission posture at creation time that is applied to every managed agent it spawns. It is stored on the team (permission_mode) and set with hive team create --permission-mode <mode> or the permission_mode field on the CreateTeam protocol message.
| Mode | Claude | Codex | OpenCode | Native OpenRouter | Behaviour |
|---|---|---|---|---|---|
bypass (default) | --dangerously-skip-permissions | --dangerously-bypass-approvals-and-sandbox | --auto | Read, write, and command tools | Full autonomy, no prompts. |
accept_edits | --permission-mode acceptEdits | --sandbox workspace-write | --auto | Read and write tools | File edits are allowed without arbitrary command execution in the native loop. |
plan | --permission-mode plan | --sandbox read-only | --agent plan --auto | Read-only tools | Planning posture with mutations restricted by the provider. |
bypass is the default because anything short of full autonomy needs a human or a routing mechanism to approve blocked tools, which a headless team does not have.
Which OS user a team runs as
Every team member is a runtime on the node. CLI providers read the credentials, settings, and trust store of the OS account they run as. Native OpenRouter reads its API key from daemon configuration, but its workspace tools still run as the resolved account. The daemon resolves that account the same way an interactive session does:
- the project's Run as user (Projects > edit project), node-specific entry first, then the
*wildcard; - the node's
default_session_userconfig value; - otherwise the account hived itself runs as, which under systemd is
root.
Falling through to root is usually wrong. The agent then needs a valid login in /root, and the Claude CLI refuses --dangerously-skip-permissions under root or sudo, so a bypass team would die at startup. Hive rejects that combination up front with an error naming the fix rather than spawning an agent that cannot work, so set a Run as user on the project (or default_session_user on the node) before creating teams on a root daemon.
The resolved account must exist on the node that spawns the team. Run-as names travel with the session (a project's * mapping, another node's default_session_user), so creating a team against a directory that belongs to a different machine can hand a node a username it has no account for - ubuntu on a node whose only user is vazy, for example. Hive now refuses that up front with run-as user '<name>' does not exist on <host> instead of spawning an agent that dies inside runuser. Create the team on the node that owns the directory, or point that node's Run as / default_session_user at a local account.
When a provider needs credentials
When a CLI provider rejects an expired or revoked login, the daemon raises AgentAuthRequired on that session. The team detail view shows a banner naming the member, the reason, and the OS account whose login expired. Sign in starts a supported provider's guided login as that account. OpenCode can connect to different upstream providers, so run opencode auth login as the team account and choose the upstream provider before retrying.
OpenRouter does not use any CLI login. The native backend requires a node-level OPENROUTER_API_KEY or [openrouter].api_key. Open Settings > Appearance > OpenRouter API, or use F1 > Configure OpenRouter API key, to see the redacted configured/source status and save or replace the private config key. You can clear a config-provided key there; an environment-provided key must be changed in the daemon's service environment. Changes apply to the next turn without restarting the daemon. The team member's Run As account still governs its filesystem and command tools, but does not own the OpenRouter credential.
In the native desktop and mobile apps, provider authorization links for CLI logins open in the OS default browser instead of Hive's embedded webview. If a provider asks for a verification code, the code-entry dialog remains open in Hive while you authorize in the browser and return. Paste the code there to finish signing in; the team then works again without recreating it.
Coordination tools
Claude, Codex, and OpenCode leaders and workers coordinate through the hive-team MCP server, wired automatically at spawn. It runs as hive team mcp, a stdio JSON-RPC server, and exposes tools as mcp__hive-team__<name>. Native OpenRouter receives one daemon-owned hive_team function and passes the matching command and arguments through it. Both paths provide the operations below:
| Tool | Role | Purpose |
|---|---|---|
team_status | any | Roster, roles, per-worker results, per-member token usage, your own identity. Call first. |
team_send | any | DM one teammate (pushed as a user turn). |
team_broadcast | any | Message every other member. |
team_assign | leader | Spawn a worker + hand it a prompt, return immediately. Takes agent, model or tier, and role. |
team_handoff | leader | Spawn a worker + block until it reports (default 60s, max 300s). Same agent/model/tier/role selection. |
team_wait | leader | Block on pending workers; re-callable, completions never lost. |
team_report_result | worker | Record this worker's result (keep under 4 KB). |
team_summary | leader | Publish a progress summary. The team stays active - it is not an end state. |
team_models | leader | List the models this node can give a worker, with tier and the recommended pick per tier. |
team_refresh_context | leader | Restart the leader's own conversation from empty, seeded by a carryover note. See Context and token usage. |
team_inbox | any | Replay persisted messages - crash catch-up only. |
The hive team CLI (status, send, assign, handoff, wait, report, summary, models, inbox, refresh-context, ...) mirrors these tools for shell scripts and humans. Full reference and orchestration playbook: Teams Autopilot.
Context and token usage
Every member's runtime reports token counts in the output the daemon already reads, so GetTeamRoster (and therefore team_status and hive team roster) carries a usage block per member:
| Field | Meaning |
|---|---|
context_tokens | Tokens in that conversation as of its last request - prompt, cache reads, cache writes and the reply. This is re-sent on every turn the member takes. |
context_window | The window it is measured against. |
context_window_source | reported when the provider stated the window itself, or assumed when the daemon derived it from the model id. |
context_pct | Share of the window used, rounded down. Absent when the window is unknown. |
total_input_tokens, total_output_tokens | What the conversation has cost so far. |
turns | Assistant turns since the conversation started. |
A member that has not taken a turn yet has no usage at all - a measured zero and "never ran" are different claims.
Why the leader cares. Workers are cheap: each one starts with an empty context at team_assign and ends with its task. A leader is not - it accumulates every roster dump, worker report and merge preview for as long as the team lives, and pays for all of it on every turn. team_status puts the leader's own numbers in you.usage along with context_advice, and the skill tells it to stop enlarging its context past 60% and to refresh past 75%.
Refreshing the leader. team_refresh_context (provider tool) or hive team refresh-context <team-id> --carryover "...":
- The daemon acknowledges immediately and waits five seconds, so the leader's in-flight turn can finish - a leader calling this is mid-turn by definition.
- It stops the leader's runtime, drops provider resume state, and zeroes the usage counters. Native OpenRouter discards the stored API message history.
- The carryover note is delivered as the first prompt of a conversation respawned without
--resume, with the team's standing orders prepended as usual.
The session, worktree, branch, environment, workers, queued assignments and merge state are untouched: this replaces the leader's memory, not the leader. Whatever is not in the carryover note (or in the team instructions, plan.md, or the roster) is gone, which is why the skill has the leader publish team_summary and write state to disk first.
Coordination readiness
Hive validates the coordination path before starting a new managed leader or worker. The colocated hive CLI must:
- be present and report the same version as
hived; - support the Hive team coordination commands used by the provider;
- receive the team identity, daemon address, authentication token, and TLS mode from the daemon.
If validation fails, creation or spawn is rejected with a team coordination is unavailable error instead of starting an agent that can talk about workers but cannot register them. When an update channel is configured, hived also refreshes a missing or stale colocated CLI in the background. Retry after that refresh completes. See Troubleshooting for checks.
Creating a team
From the UI (Teams view, route /teams): click New Team, fill in name + working directory, optionally pick a project, choose the leader agent and the permission mode (bypass / accept_edits / plan), and submit. The team detail view (route /teams/:id) then hosts per-member messaging with the leader and each worker.
Which node runs the team
A team is a set of real processes on a real filesystem, so it runs on the node the project belongs to - not on whichever node the app happens to be connected to. Picking a project in New Team routes creation to that project's node exactly as New Session does, and the dialog names the target node under the working directory. Without a project, the team runs on the connected node.
This matters because a directory path and an OS account only mean something on one machine: a team created on the wrong node lands in a directory that does not exist there, under a user it does not have.
From the CLI (--node <NODE_ID>, node ids from hive cluster-status):
bash
# Claude leader, default agent, default (bypass) permission mode
hive team create --name review-team --working-dir /repo
# Claude leader in the more cautious accept-edits posture
hive team create --name review-team --working-dir /repo \
--permission-mode accept_edits
# Read-only planning team
hive team create --name plan-team --working-dir /repo --permission-mode plan
# Codex leader
hive team create --name codex-team --working-dir /repo --agent codex
# OpenRouter leader using its automatic model router
hive team create --name router-team --working-dir /repo --agent openrouter \
--model openrouter/auto
# Custom provider leader
hive team create --name local-agent --working-dir /repo \
--agent custom --custom-cmd /opt/my-agent --arg --once
# Run the team on another node in the cluster
hive team create --name pi-team --working-dir /home/vazy/Repos/Spajzka \
--node b78790d5-d913-4351-b183-d207e8083195--arg may be repeated to pass multiple flags. Custom providers require --custom-cmd (the binary path). --permission-mode is one of bypass (default), accept_edits, or plan - see Permission modes.
Headless teams (no leader session)
hive team create --headless creates a team with no leader session - an external orchestrator acts as the leader and drives workers directly. The team's leader_session_id is the nil UUID; --working-dir becomes the root for worker worktrees (instead of a leader session's cwd). Read the team's final_result back with hive team get after the leader publishes a summary. The app labels the leader as External orchestrator and never tries to open or message the nil leader session. Add a worker before using the detail view's message composer.
Spawning workers
bash
# Worker (headless; claude, codex, opencode, or openrouter)
hive team spawn-worker <team-id> --working-dir /repo --name explorerIn practice a leader agent rarely calls spawn-worker directly - it uses the team_assign / team_handoff MCP tools (or their hive team assign / hive team handoff CLI mirrors), which spawn the worker and hand it a task prompt in one step, each in an isolated git worktree. See Teams Autopilot.
Execution model
When a team is created:
- The daemon resolves the leader's
TeamAgent. CLI providers use(binary, args)from[agents]; OpenRouter uses the native API backend with the selected model and node API key. - A new session is started in the team's working directory; its session ID becomes
team.leader_session_idandteam.leader_agentrecords the agent value. Claude, Codex, OpenCode, and OpenRouter leaders start in headless SDK mode. The daemon appends the provider-specific permission permissions and coordination tools, and relays each completed turn's final text back to the team as aTeamMessage. Other leaders start as an interactive PTY session instead. Either way the leader inherits the team'sHIVE_*env wiring and the materialisedhive-teamskill. - The daemon emits
TeamCreatedto the client and persists the team to theteamsSQL table.
When a worker is spawned:
- The worker's agent must be
claude,codex,opencode, oropenrouter; providers without a headless backend are rejected. The daemon resolves its headless runtime, applies the team's permission posture, and wires the provider's coordination tool path. - A new headless SDK session is started (in an isolated git worktree when spawned via the autopilot
assign/handoffpaths); its session ID is pushed ontoteam.worker_session_idsand the agent stored inteam.worker_agents(keyed by the worker's session ID string). team.last_activeis updated andWorkerSpawnedis emitted. When the worker is spawned with a task prompt (assign/handoff), that prompt is pushed into its conversation as the first user turn.
When a worker reports its result (ReportWorkerResult):
- The daemon stores the result in
team.worker_results[worker_id], together with itsreported_viaprovenance. A result carryingstop_hook_fallbackcan never recordsuccess: true- see Result provenance. WorkerResultReceivedreports progress andTeamUpdatedcarries the new team state. The team stays active even when every worker has reported - it is a persistent workspace, reusable for the next round.- The freed worker slot triggers the assignment queue: if the team has queued assignments, the next one spawns and receives its prompt.
Result provenance
WorkerResult.success is true | false | null, paired with reported_via:
reported_via | Source | success |
|---|---|---|
explicit | The worker called team_report_result / hive team report, or an operator reported for it in the app. | Its own claim |
stop_hook_fallback | The worker's process ended without reporting; the Stop hook salvaged its last assistant message. | null, or false when the process failed - never true |
The fallback exists so a dead worker cannot block a leader's team_wait forever, not to certify work: the salvaged text is frequently a mid-thought. The daemon strips a success claim off any fallback report, team_status lists affected workers under unverified_results, and the app shows them as Unverified (amber) rather than Done. Rows written before this field existed deserialize as explicit; the daemon cannot tell them apart retroactively.
Per-worker isolation
Beyond the git worktree, each autopilot worker is spawned with its own service allocation, deterministic per (team, slot):
| Variable | Meaning |
|---|---|
HIVE_WORKER_INDEX | Slot in the team, 0..4; recycled when a worker reports |
HIVE_WORKER_DB_NAME | hive_<team-prefix>_w<index> |
HIVE_WORKER_PORT_BASE / HIVE_WORKER_PORT_COUNT | Private port range (20 ports; the cap of 5 fills a 100-port team block) |
HIVE_WORKER_BRANCH | Branch its worktree is checked out on |
The same values appear in .hive/team.json under workspace. A per-worker permission_mode may be passed to Assign / Handoff / SpawnWorker; it can only narrow the team's posture (plan < accept_edits < bypass).
When a team is edited (UpdateTeam):
- Name, instruction overrides, permission posture, and the published summary are applied to the stored team and persisted.
- The instruction overlay is handed to the session manager, so running members pick it up on their next message.
TeamUpdatedis emitted.
When a team is deleted (DeleteTeam):
- All sessions in the team (leader + workers) are killed.
- The team row is removed from in-memory state and the SQL table.
Teams survive daemon restarts, members included. The team row is persisted; the member sessions are not (every session row is cleared at startup and the agent runtimes stopped with the daemon), so on startup the node rebuilds the session records for the headless members it owned before the restart:
- The SDK leader of a led team, plus every worker that had not yet reported a result. Workers with a result are finished and are left alone; a PTY leader is a human's terminal, not an agent, and is never recreated.
- Each member keeps its original session ID, so the roster, the outbox, and any pending
team_waitstill resolve. - The provider runtime is not started. A headless member's turn only runs while it has work, so the revived member sits
idleuntil it is messaged, exactly like one that just finished a turn. - CLI provider conversation IDs are persisted at the end of every turn so the first message after a restart can resume. Native OpenRouter reconstructs its API message history from Hive's stored transcript.
- Members whose account, worktree, or provider runtime no longer resolve are skipped with a warning in the daemon log rather than half-restored.
Relatedly, an idle headless member (between turns, no runtime active) counts as live and owned by its node for cluster purposes. Treating "no process" as "dead" previously let an incoming peer snapshot tombstone every idle worker of a team, which surfaced in the app as "The agent session is offline or its node is unavailable."
Cluster mode replicates teams between peers via the same StateMutation::TeamUpserted / TeamRemoved channel as the other replicated state.
Wire protocol
Teams ride the same WebSocket framing as everything else (see protocol.md). The team-specific messages are:
TeamAgent
jsonc
{
"kind": "claude", "args": ["--temperature", "0.2"], "model": "claude-opus-4-7"
}
// or
{ "kind": "codex", "args": null }
// or
{ "kind": "copilot", "args": null }
// or
{ "kind": "llama", "args": null, "model": "llama3:70b" }
// or
{ "kind": "custom", "command": "/opt/my-agent", "args": ["--once"] }kind is the serde discriminator.
Team
jsonc
{
"id": "uuid",
"name": "string",
"leader_session_id": "uuid",
"worker_session_ids": ["uuid", "..."],
"status": "active | inactive",
"created_at": "RFC3339",
"last_active": "RFC3339",
"project_id": "uuid | null",
"worker_results": {
"<worker-session-id>": {
"success": "true | false | null",
"result": "string | null",
"reported_at": "RFC3339",
"reported_via": "explicit | stop_hook_fallback"
}
},
"final_result": "string | null",
"leader_agent": { "kind": "claude", "args": null, "model": null },
"worker_agents": {
"<worker-session-id>": { "kind": "claude", "args": null }
},
"updated_at": "RFC3339",
"headless": false,
"root_dir": "string | null",
"permission_mode": "bypass | accept_edits | plan",
"worker_meta": {
"<worker-session-id>": {
"name": "string | null",
"branch": "string | null",
"working_dir": "string | null",
"started_at": "RFC3339",
"index": 0,
"permission_mode": "bypass | accept_edits | plan"
}
},
"queued_assignments": [
{
"id": "uuid",
"prompt": "string",
"name": "string | null",
"role": "string | null",
"agent": "TeamAgent | null",
"tier": "fast | balanced | deep | null",
"permission_mode": "bypass | accept_edits | plan | null",
"queued_at": "RFC3339"
}
]
}headless marks a leaderless team (external orchestrator; leader_session_id is the nil UUID) and root_dir is its worker-worktree root. permission_mode is the posture applied to every headless agent (see Permission modes). updated_at is the last-writer-wins clock used when two leaderless cluster nodes merge team state.
Client → Daemon
| Message | Payload |
|---|---|
CreateTeam | { name, working_dir, arguments?, project_id?, agent?, headless?, permission_mode? } - agent defaults to {kind:"claude"} when omitted; headless skips the leader session; permission_mode defaults to bypass |
SpawnWorker | { team_id, working_dir, arguments?, name?, agent?, role?, tier?, permission_mode? } - same default; tier (fast/balanced/deep) resolves to a model when agent.model is unset; permission_mode can only narrow the team's |
UpdateTeam | { team_id, name?, leader_instructions?, worker_instructions?, permission_mode?, summary? } - omitted fields are untouched; an empty instruction string clears that override |
ListAgentModels | { provider?, refresh? } - model catalog for one provider or all |
ListTeams | - |
GetTeam | { team_id } |
GetTeamMessages | { team_id } - load the daemon-persisted operator conversation |
RelayTeamMessage | { team_id, from_session_id, to_session_id?, payload } - to_session_id=null broadcasts to all members except the sender |
ReportWorkerResult | { team_id, worker_session_id, success?, result?, reported_via? } - success is true/false/null; reported_via defaults to explicit |
DeleteTeam | { team_id } |
Assign | { team_id, worker_name?, working_dir?, prompt, agent?, role?, tier?, permission_mode? } - spawn a worker in a worktree and push it prompt, return immediately (backs team_assign); past the worker cap the assignment is queued |
Handoff | { team_id, worker_name?, working_dir?, prompt, agent?, role?, tier?, permission_mode?, timeout_ms? } - like Assign but block until the worker reports (backs team_handoff) |
GetTeamRoster | { team_id } - composed roster: durable per-member facts joined with live session state (backs team_status) |
TeamMerge | { team_id, worker_session_ids?, target_branch?, dry_run } - preview or perform merges of worker branches (backs team_merge); dry_run defaults to true |
TeamWait | { team_id, for_session_ids?, timeout_ms?, wait_id? } - block until pending workers report (backs team_wait) |
GetTeamInbox | { team_id, session_id, since_seq? } - replay persisted messages (backs team_inbox) |
RefreshTeamLeader | { team_id, carryover? } - discard the leader's conversation and restart it from empty with carryover as its first prompt (backs team_refresh_context) |
Daemon → Client
| Message | Payload |
|---|---|
TeamList | { teams: Team[] } - response to ListTeams and GetTeam |
TeamCreated | { team, leader_session } |
WorkerSpawned | { team, worker_session } |
WorkerResultReceived | { team_id, worker_session_id, success?, result?, reported_via, reported_count, total_workers } |
TeamMessageDelivery | { team_id, message_id, delivered, undelivered } - per-recipient outcome of a relay; delivered means the owning node's live member session accepted the input, while unavailable sessions and rejected input are undelivered and accompanied by an app-visible error |
TeamRoster | { team_id, members, queued_assignments } - response to GetTeamRoster; each member carries state (working/idle/exited/unknown), branch, worktree, last activity, token usage and its result |
TeamLeaderRefreshScheduled | { team_id, leader_session_id, delay_ms } - response to RefreshTeamLeader; the restart lands after delay_ms, not when this arrives |
TeamMergeResult | { team_id, target_branch, dry_run, entries } - per branch: commits ahead, changed files, conflicting paths, whether it merged |
TeamMessage | { team_id, message }, where message is { id, from_session_id, to_session_id?, payload, sent_at } - a newly persisted prompt, relay, or headless leader reply |
TeamMessageHistory | { team_id, messages } - response to GetTeamMessages; clients merge it with live TeamMessage events by stable message ID |
TeamUpdated | { team } - the team's own fields changed (rename, instruction edit, permission mode, published summary, worker result recorded) |
AgentModelList | { provider?, models, fetched_at } - response to ListAgentModels |
TeamDeleted | { team_id } |
WorkerAssigned | { team_id, worker_session_id?, queued, queue_position?, assignment_id? } - response to Assign; worker_session_id is null when the assignment was queued behind the worker cap |
HandoffResult | { team_id, worker_session_id, completed, success?, result?, reported_via?, wait_id } - response to Handoff; completed=false means still running, keep waiting with TeamWait |
TeamWaitResult | { team_id, completed_workers, pending_workers, timed_out, wait_id } - response to TeamWait |
TeamInbox | { messages, next_seq } - response to GetTeamInbox |
Daemon config
The daemon resolves each TeamAgent to a binary using the [agents] table:
toml
[agents.claude]
bin = "claude"
args = ["--dangerously-skip-permissions"]
[agents.codex]
bin = "codex"
[agents.opencode]
bin = "opencode"
[agents.copilot]
bin = "gh"
args = ["copilot"]
[agents.llama]
bin = "ollama"
args = ["run", "llama3"]All sub-tables are optional; missing CLI providers fall back to PATH lookups (claude, codex, opencode, gh copilot, ollama run llama3). OpenRouter does not resolve a binary. It uses the separate [openrouter] api_key = "..." native API setting or the daemon's OPENROUTER_API_KEY. Per-request CLI args and model values from the TeamAgent payload are appended after provider defaults; native OpenRouter reads the selected model directly.
Persistence
Teams live in the SQLite teams table (columns added incrementally by migration; permission_mode arrived in schema v29):
sql
CREATE TABLE teams (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
leader_session_id TEXT NOT NULL, -- nil UUID for a headless team
worker_session_ids TEXT NOT NULL DEFAULT '[]', -- JSON array
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
last_active TEXT NOT NULL,
project_id TEXT,
worker_results TEXT NOT NULL DEFAULT '{}', -- JSON map
final_result TEXT,
leader_agent TEXT NOT NULL DEFAULT '{"kind":"claude"}', -- JSON
worker_agents TEXT NOT NULL DEFAULT '{}', -- JSON map
headless INTEGER NOT NULL DEFAULT 0, -- leaderless team flag
root_dir TEXT, -- worker-worktree root when headless
updated_at TEXT, -- last-writer-wins merge clock
permission_mode TEXT NOT NULL DEFAULT 'bypass', -- bypass | accept_edits | plan
worker_roles TEXT NOT NULL DEFAULT '{}', -- JSON map
leader_instructions TEXT,
worker_instructions TEXT,
worker_meta TEXT NOT NULL DEFAULT '{}', -- JSON map (schema v34)
queued_assignments TEXT NOT NULL DEFAULT '[]' -- JSON array (schema v34)
);worker_session_ids, worker_results, leader_agent, and worker_agents are all JSON columns.
In cluster mode teams are replicated like sessions and projects: each mutation goes through StateMutation::TeamUpserted / TeamRemoved on the leader, and followers persist the resulting team rows on apply.
Autopilot
A headless leader orchestrates its workers from inside its own agent loop, using Hive's team coordination tools (see Coordination above). See teams-autopilot.md for the full design, the skill contract, the delivery protocol, and the CLI mirror.
In short: every team agent gets the hive-team skill and coordinator guidance materialised into its working directory, plus HIVE_TEAM_ID, HIVE_SESSION_ID, HIVE_TEAM_ROLE, HIVE_DAEMON_SOCKET, HIVE_TOKEN, and HIVE_TLS injected into its env. CLI providers receive the hive-team MCP server through their supported configuration path. Native OpenRouter receives a daemon-owned hive_team function that calls the same coordination CLI. The generated permissions explicitly allow the mcp__hive-team__* tools where the provider uses MCP. A Stop hook reports a worker's last message as a fallback result if it exits without calling team_report_result; leader turns do not run that worker-only fallback. Relayed messages persist to the SQLite team_outbox so an agent that was offline can replay them via team_inbox (hive team inbox) - normally unnecessary, since messages are pushed live as user turns.
Caveats
- Provider binaries must already be installed. The daemon doesn't fetch
claude/codex/opencode/gh/ollamafor you. If the configured binary is missing the spawn fails with a session-startup error. Usewhich <bin>on the daemon host to verify before creating a team. OpenRouter is the exception: its native backend needs an API key, not a provider binary. gh copilotrequires GitHub authentication on that host. The daemon inherits the user'sghauth state - typicallygh auth loginonce per user account.- Managed members need a headless backend. Claude, Codex, OpenCode, and OpenRouter are supported. Spawning a worker with another provider is rejected; a leader may still run one as a PTY.
- Per-session agents. A team has no global
agentfield; you'll seeleader_agent, the per-workerworker_agentsmap, and the per-workerworker_roleslabels instead. - Teams never complete. There is no completion state or
CompleteTeammessage;team_summaryrecords progress and the team stays active until deleted. AStophook reports a worker's last message as a fallback if it exits without callingteam_report_result. - Message delivery is push, persisted at-least-once. A relayed message is written to the recipient's outbox before it is pushed into that agent's conversation, so an offline or not-yet-started worker still gets it (booted or replayed via
team_inbox). Agents never poll. - Active worker cap of 5 per team. Only workers without a recorded result consume a slot. Reported workers remain in the history and roster but free capacity for another worker, so larger fan-outs can run in batches.
- No retry on runtime crashes. A worker whose runtime exits without reporting is gone (the
Stophook aside) - spawn a replacement if needed.