Skip to content

Teams

The Teams view in hive-app lets you spin up multi-agent teams. A team is one leader session plus zero or more worker sessions, all coordinated through the daemon. Each session can run a different AI provider - Claude, Codex, GitHub Copilot, a local Llama via Ollama, or a custom command - so a single team can mix providers that complement each other (e.g. Claude planner + Codex/Llama workers).

Teams differ from Tasks in three ways: they are interactive (PTY-driven, not one-shot), they support bi-directional messaging between leader and workers via the relay protocol, and they record per-worker results so the leader can aggregate work without parsing terminal output.

The Teams view

Route: /teams, sidebar icon: people.

Team cards

Each team is rendered as a card with:

ElementNotes
Name + UUID prefixClick anywhere on the card to open the detail view
Status badgeactive or inactive
Leader rowCrown icon + session name + provider badge (Claude, Codex, Copilot, Llama, Custom)
Worker rowsUser icon + session name + provider badge + result icon (check / cross) once reported
Final resultShown when the team is inactive and final_result is set
Last ActiveRelative time
ActionsAdd Worker (active teams) • Delete

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

Keyboard

  • Click or Enter on a team card → opens the detail view.

Team detail view

Route: /teams/:id.

The detail view splits horizontally:

  • Left column - team metadata (status, worker count, timestamps, final result), 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 session.
  • Right column - the Messages log and compose box. The target dropdown picks who the message goes to:
    • Leader (default) - sends the text to the leader as a SendInput prompt. A Claude leader runs headless (SDK stream-json) rather than as an interactive terminal, so it answers right in the Messages panel: when each turn completes, the daemon forwards the leader's final reply back as a TeamMessage (injected as a __STATUS__: control frame on the leader's output, which the team view is subscribed to). 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 without opening its terminal. (Non-Claude leaders have no SDK backend yet and keep the interactive PTY path - they receive input but reply only in their terminal.)
    • All workers (broadcast) / a specific worker - relayed by the daemon via RelayTeamMessage and injected into the target workers' output streams (and persisted to the outbox for hive team inbox replay). Broadcasts go to every worker except the sender, so a team with no workers has nothing to receive them.

Top-bar buttons: Complete (mark team inactive with an optional final result) and Delete (kill all sessions and remove 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 the actual binary on this host.

KindSpawn (default)Notes
claudeclaudeHonours --model.
codexcodexOpenAI Codex CLI.
copilotgh copilotGitHub Copilot CLI.
llamaollama run llama3Local llama runner; honours --model.
custom(user-supplied)Free-form binary path + args - used for any provider not yet first-class.

Per-session arguments from the CLI/UI are appended after the daemon's configured defaults, so the daemon-level --dangerously-skip-permissions (or similar) survives across all team sessions on that node.

Mixing providers

The leader and each worker hold their own TeamAgent. A common pattern is to use Claude as the planner-leader and cheaper / faster models as workers, e.g. claude leader plus three codex workers. The Add Worker dialog defaults to the leader's provider but you can change it for each new worker.

Creating a team

From the UI: click New Team, fill in name + working directory, optionally pick a project, choose the agent, and submit.

From the CLI:

bash
# Claude leader, default agent
hive team create --name review-team --working-dir /repo

# Codex leader
hive team create --name codex-team --working-dir /repo --agent codex

# Llama leader pinned to a specific model with extra args
hive team create --name llama-team --working-dir /repo \
    --agent llama --model llama3:70b --arg --temperature --arg 0.2

# Custom provider
hive team create --name local-agent --working-dir /repo \
    --agent custom --custom-cmd /opt/my-agent --arg --once

--arg may be repeated to pass multiple flags. Custom providers require --custom-cmd (the binary path).

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 complete.

Spawning workers

bash
# Default worker (Claude)
hive team spawn-worker <team-id> --working-dir /repo

# Mixed-provider team: Codex worker under a Claude leader
hive team spawn-worker <team-id> --working-dir /repo --agent codex --name explorer

Execution model

When a team is created:

  1. The daemon resolves the leader's TeamAgent to (binary, args) using the [agents] config. Per-request args and model are appended after the configured defaults.
  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. A Claude leader is started in SDK mode (headless stream-json, with --dangerously-skip-permissions so a leader with no human at a terminal can still use tools) and a forwarder task relays each completed turn's final text back to the team as a TeamMessage. Other providers start as an interactive PTY session. 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 same (binary, args) resolution is done for the worker's agent.
  2. A new PTY session is started; 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 a worker reports its result (ReportWorkerResult):

  1. The daemon stores the result in team.worker_results[worker_id].
  2. If reported_count == total_workers > 0, the team is auto-transitioned to inactive and a TeamCompleted event is emitted.

When the leader explicitly calls CompleteTeam:

  1. team.status becomes inactive, team.final_result records the leader's summary, and TeamCompleted 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 at the metadata level - sessions themselves are recreated when their PTY processes have exited, so an active team whose leader process died comes back as a team referencing a dead session ID. 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,
      "result": "string | null",
      "reported_at": "RFC3339"
    }
  },
  "final_result": "string | null",
  "leader_agent": { "kind": "claude", "args": null, "model": null },
  "worker_agents": {
    "<worker-session-id>": { "kind": "codex", "args": null }
  }
}

Client → Daemon

MessagePayload
CreateTeam{ name, working_dir, arguments?, project_id?, agent?, headless? } - agent defaults to {kind:"claude"} when omitted; headless skips the leader session
SpawnWorker{ team_id, working_dir, arguments?, name?, agent? } - same default
ListTeams-
GetTeam{ team_id }
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? }
CompleteTeam{ team_id, result? }
DeleteTeam{ team_id }

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_count, total_workers }
TeamMessage{ team_id, from_session_id, payload } - injected into each target session's output stream prefixed by __STATUS__:
TeamCompleted{ team, result? }
TeamDeleted{ team_id }

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.copilot]
bin = "gh"
args = ["copilot"]

[agents.llama]
bin = "ollama"
args = ["run", "llama3"]

All sub-tables are optional; missing providers fall back to PATH lookups (claude, codex, gh copilot, ollama run llama3). Per-request args and model from the TeamAgent payload are appended after these defaults.

Persistence

Teams live in the SQLite teams table (schema v12):

sql
CREATE TABLE teams (
    id                  TEXT PRIMARY KEY,
    name                TEXT NOT NULL,
    leader_session_id   TEXT NOT NULL,
    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
);

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

Teams can run in autopilot mode - the leader agent orchestrates workers via the hive team CLI from inside its own loop. See teams-autopilot.md for the full design, the skill contract, and the CLI surface (handoff, assign, send, inbox, workers, report, complete, wait, status).

In short: every team session gets .claude/skills/hive-team/SKILL.md for Claude, or .codex/skills/hive-team/SKILL.md for Codex, materialised into its working directory plus HIVE_TEAM_ID, HIVE_SESSION_ID, HIVE_TEAM_ROLE, and HIVE_DAEMON_SOCKET injected into its env. Claude also gets .claude/settings.json to wire the Stop hook used for automatic worker completion. Relayed messages persist to the SQLite team_outbox so workers that weren't attached can replay them via hive team inbox.

Caveats

  • Provider binaries must already be installed. The daemon doesn't fetch claude / codex / 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.
  • 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.
  • Per-session, not per-team. A team has no global agent field; you'll see leader_agent and the per-worker worker_agents map instead. UI and CLI default new workers to the leader's provider, but each can be overridden.
  • Worker results are advisory. Auto-completion fires on reported_count == total_workers, but workers are free not to report - the leader can always force completion with CompleteTeam.
  • Message relay is at-most-once. RelayTeamMessage injects into the receiver's PTY output channel; if the receiver isn't attached or its session has exited, the message is dropped silently.
  • No retry on agent crashes. A worker whose underlying PTY process exits is gone - spawn a replacement worker if needed.

Hive - remote AI coding agents over WebSocket.