Appearance
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:
| Element | Notes |
|---|---|
| Name + UUID prefix | Click anywhere on the card to open the detail view |
| Status badge | active or inactive |
| Leader row | Crown icon + session name + provider badge (Claude, Codex, Copilot, Llama, Custom) |
| Worker rows | User icon + session name + provider badge + result icon (check / cross) once reported |
| Final result | Shown when the team is inactive and final_result is set |
| Last Active | Relative time |
| Actions | Add Worker (active teams) • Delete |
The header offers a status filter (all / active / inactive) and the New Team button.
Keyboard
- Click or
Enteron 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
SendInputprompt. 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 aTeamMessage(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
RelayTeamMessageand injected into the target workers' output streams (and persisted to the outbox forhive team inboxreplay). Broadcasts go to every worker except the sender, so a team with no workers has nothing to receive them.
- Leader (default) - sends the text to the leader as a
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.
| Kind | Spawn (default) | Notes |
|---|---|---|
claude | claude | Honours --model. |
codex | codex | OpenAI Codex CLI. |
copilot | gh copilot | GitHub Copilot CLI. |
llama | ollama run llama3 | Local 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 explorerExecution model
When a team is created:
- The daemon resolves the leader's
TeamAgentto(binary, args)using the[agents]config. Per-requestargsandmodelare appended after the configured defaults. - A new session is started in the team's working directory; its session ID becomes
team.leader_session_idandteam.leader_agentrecords the agent value. A Claude leader is started in SDK mode (headless stream-json, with--dangerously-skip-permissionsso 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 aTeamMessage. Other providers start as an interactive PTY session. 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 same
(binary, args)resolution is done for the worker's agent. - A new PTY session is started; its session ID is pushed onto
team.worker_session_idsand the agent stored inteam.worker_agents(keyed by the worker's session ID string). team.last_activeis updated andWorkerSpawnedis emitted.
When a worker reports its result (ReportWorkerResult):
- The daemon stores the result in
team.worker_results[worker_id]. - If
reported_count == total_workers > 0, the team is auto-transitioned toinactiveand aTeamCompletedevent is emitted.
When the leader explicitly calls CompleteTeam:
team.statusbecomesinactive,team.final_resultrecords the leader's summary, andTeamCompletedis 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 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
| Message | Payload |
|---|---|
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
| 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_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/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. gh copilotrequires GitHub authentication on that host. The daemon inherits the user'sghauth state - typicallygh auth loginonce per user account.- Per-session, not per-team. A team has no global
agentfield; you'll seeleader_agentand the per-workerworker_agentsmap 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 withCompleteTeam. - Message relay is at-most-once.
RelayTeamMessageinjects 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.