Skip to content

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, and openrouter have one. Spawning a member with copilot / llama / custom is rejected with provider '<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:

ElementNotes
Name, status, and last activityClick anywhere on the card to open the detail view
Health countersTotal agents, worker count, and successfully completed workers
CoordinatorLeader name and provider badge, or External orchestrator
RosterUp to four workers shown as compact status chips: amber for pending, green for success, red for failure, plus a count for additional workers
Latest summaryA short preview when the leader has published one
ActionsAdd Worker and Delete

The header offers a status filter (all / active / inactive) and the New Team button.

Keyboard

  • Click or press Enter on 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 a TeamMessage. 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 RelayTeamMessage and 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.

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.

KindSpawn (default)LeaderWorkerNotes
claudeclaudeyes (headless SDK)yes (headless SDK)Honours --model. Long-lived bidirectional process.
codexcodexyes (headless SDK)yes (headless SDK)codex exec --json; one process per turn, resumed via the thread id. Honours --model.
opencodeopencodeyes (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.
openrouternative HTTP APIyes (headless SDK)yes (headless SDK)Hive streams OpenRouter directly, preserves API conversation history, and executes workspace tools. Uses native model ids.
copilotgh copilotyes (PTY only)noGitHub Copilot CLI. Honours --model.
llamaollama run llama3yes (PTY only)noLocal llama runner; honours --model.
custom(user-supplied)yes (PTY only)noFree-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:

  1. Curated - the list bundled with the daemon, so a picker is never empty.
  2. Config - [models] entries an operator added in config.toml; these also override a bundled model's label and tier.
  3. OpenRouter - the keyless public model index, which is how ids released after a Hive build still show up. Disable with [models] openrouter = false.
  4. Provider probe - Anthropic / OpenAI /v1/models when an API key is in the daemon's environment (ANTHROPIC_API_KEY, OPENAI_API_KEY, or an API-key ~/.codex/auth.json), opencode models for OpenCode, and ollama's /api/tags for llama. OpenRouter uses the public OpenRouter catalog and includes openrouter/auto as 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.

ModeClaudeCodexOpenCodeNative OpenRouterBehaviour
bypass (default)--dangerously-skip-permissions--dangerously-bypass-approvals-and-sandbox--autoRead, write, and command toolsFull autonomy, no prompts.
accept_edits--permission-mode acceptEdits--sandbox workspace-write--autoRead and write toolsFile edits are allowed without arbitrary command execution in the native loop.
plan--permission-mode plan--sandbox read-only--agent plan --autoRead-only toolsPlanning 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:

  1. the project's Run as user (Projects > edit project), node-specific entry first, then the * wildcard;
  2. the node's default_session_user config value;
  3. 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:

ToolRolePurpose
team_statusanyRoster, roles, per-worker results, per-member token usage, your own identity. Call first.
team_sendanyDM one teammate (pushed as a user turn).
team_broadcastanyMessage every other member.
team_assignleaderSpawn a worker + hand it a prompt, return immediately. Takes agent, model or tier, and role.
team_handoffleaderSpawn a worker + block until it reports (default 60s, max 300s). Same agent/model/tier/role selection.
team_waitleaderBlock on pending workers; re-callable, completions never lost.
team_report_resultworkerRecord this worker's result (keep under 4 KB).
team_summaryleaderPublish a progress summary. The team stays active - it is not an end state.
team_modelsleaderList the models this node can give a worker, with tier and the recommended pick per tier.
team_refresh_contextleaderRestart the leader's own conversation from empty, seeded by a carryover note. See Context and token usage.
team_inboxanyReplay 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:

FieldMeaning
context_tokensTokens 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_windowThe window it is measured against.
context_window_sourcereported when the provider stated the window itself, or assumed when the daemon derived it from the model id.
context_pctShare of the window used, rounded down. Absent when the window is unknown.
total_input_tokens, total_output_tokensWhat the conversation has cost so far.
turnsAssistant 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 "...":

  1. 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.
  2. It stops the leader's runtime, drops provider resume state, and zeroes the usage counters. Native OpenRouter discards the stored API message history.
  3. 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 explorer

In 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:

  1. 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.
  2. A new session is started in the team's working directory; its session ID becomes team.leader_session_id and team.leader_agent records 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 a TeamMessage. Other leaders start as an interactive PTY session instead. Either way the leader inherits the team's HIVE_* env wiring and the materialised hive-team skill.
  3. The daemon emits TeamCreated to the client and persists the team to the teams SQL table.

When a worker is spawned:

  1. The worker's agent must be claude, codex, opencode, or openrouter; 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.
  2. A new headless SDK session is started (in an isolated git worktree when spawned via the autopilot assign / handoff paths); its session ID is pushed onto team.worker_session_ids and the agent stored in team.worker_agents (keyed by the worker's session ID string).
  3. team.last_active is updated and WorkerSpawned is 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):

  1. The daemon stores the result in team.worker_results[worker_id], together with its reported_via provenance. A result carrying stop_hook_fallback can never record success: true - see Result provenance.
  2. WorkerResultReceived reports progress and TeamUpdated carries the new team state. The team stays active even when every worker has reported - it is a persistent workspace, reusable for the next round.
  3. 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_viaSourcesuccess
explicitThe worker called team_report_result / hive team report, or an operator reported for it in the app.Its own claim
stop_hook_fallbackThe 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):

VariableMeaning
HIVE_WORKER_INDEXSlot in the team, 0..4; recycled when a worker reports
HIVE_WORKER_DB_NAMEhive_<team-prefix>_w<index>
HIVE_WORKER_PORT_BASE / HIVE_WORKER_PORT_COUNTPrivate port range (20 ports; the cap of 5 fills a 100-port team block)
HIVE_WORKER_BRANCHBranch 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):

  1. Name, instruction overrides, permission posture, and the published summary are applied to the stored team and persisted.
  2. The instruction overlay is handed to the session manager, so running members pick it up on their next message. TeamUpdated is emitted.

When a team is deleted (DeleteTeam):

  1. All sessions in the team (leader + workers) are killed.
  2. 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_wait still resolve.
  • The provider runtime is not started. A headless member's turn only runs while it has work, so the revived member sits idle until 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

MessagePayload
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

MessagePayload
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 / ollama for you. If the configured binary is missing the spawn fails with a session-startup error. Use which <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 copilot requires GitHub authentication on that host. The daemon inherits the user's gh auth state - typically gh auth login once 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 agent field; you'll see leader_agent, the per-worker worker_agents map, and the per-worker worker_roles labels instead.
  • Teams never complete. There is no completion state or CompleteTeam message; team_summary records progress and the team stays active until deleted. A Stop hook reports a worker's last message as a fallback if it exits without calling team_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 Stop hook aside) - spawn a replacement if needed.

Hive - remote AI coding agents over WebSocket.