Introduction

Swarm is a multi-agent orchestration framework for AI coding agents. It coordinates multiple AI agents working in parallel on a shared codebase, where each agent runs in an isolated git worktree, communicates through a SQLite-backed mailbox, and is managed by a supervisor that handles merging results back to the main branch.

Who Is This For?

Swarm is built for developers who want to:

  • Parallelize large coding tasks across multiple AI agents, each with a distinct role (backend, frontend, reviewer, etc.)
  • Maintain isolation between agents so they don't interfere with each other's work
  • Coordinate agent work through messaging, shared issue tracking, and supervised merging
  • Monitor progress in real time through a terminal UI

Key Capabilities

CapabilityDescription
Parallel agent sessionsRun multiple AI agents simultaneously, each in its own git worktree branch
SQLite messagingAgents communicate via a durable, threaded mailbox with urgency levels
State machine lifecycleEach agent follows a well-defined state machine (Initializing → Running → Stopped) with automatic error recovery and backoff
TUI dashboardReal-time terminal UI showing agent status, logs, and events
Git worktree isolationEach agent works on an isolated branch; changes are merged back on stop
Configurable permissionsFine-grained allow/ask/deny rules per tool, per agent
MCP integrationConnect external tool servers via the Model Context Protocol
WASM sandboxed toolsRun untrusted tool code in a WebAssembly sandbox with resource limits
Hooks systemExecute custom scripts on lifecycle events (session start, tool calls, etc.)
Beads issue trackingBuilt-in integration with the bd CLI for task management across agents
Workflow pipelinesDefine multi-stage workflows with gates and approvals
Iteration engineRun repeated task-solving loops with configurable progress detection

Architecture at a Glance

┌─────────────────────────────────────────────────────┐
│                   Orchestrator                       │
│  (session management, periodic tasks, shutdown)      │
├──────────┬──────────┬──────────┬────────────────────┤
│ Agent 1  │ Agent 2  │ Agent N  │    Supervisor       │
│ worktree │ worktree │ worktree │    worktree         │
│ branch   │ branch   │ branch   │    branch           │
├──────────┴──────────┴──────────┴────────────────────┤
│              SQLite Mailbox + Router                 │
│         (message delivery, urgent interrupts)        │
├─────────────────────────────────────────────────────┤
│   AgentBackend (Anthropic API / pluggable providers) │
└─────────────────────────────────────────────────────┘

The orchestrator creates a session that tracks the base commit, agent list, and process ID. Each agent runs through a state machine loop: build prompt → spawn backend session → run → handle results → repeat. Agents communicate by sending messages through the SQLite mailbox, and a router polls for urgent messages to trigger interrupts.

When you stop a session, the supervisor merges (or squashes/discards) each agent's branch back into the base branch.

When to Use Swarm

Swarm is a good fit when:

  • Your task can be decomposed into independent or loosely-coupled subtasks (e.g., "backend builds API endpoints while frontend builds the UI")
  • You want automated coordination between agents rather than manual copy-paste between chat windows
  • You need durability — agent work persists in git branches even if the process crashes
  • You want observability into what each agent is doing via the TUI

Swarm may not be the right tool if:

  • Your task is small enough for a single agent session
  • You need agents to share the same working directory in real time (swarm uses branch isolation)

What's Next

Quick Start

Install, configure, and run your first swarm session in minutes.

Prerequisites

Before you begin, ensure you have:

RequirementMinimum VersionCheck Command
Rust toolchainLatest stablerustc --version
Git2.20+git --version
Anthropic API keyecho $ANTHROPIC_API_KEY

Swarm uses git worktrees, which require git 2.20 or newer. If your version is older, swarm will exit with a VersionTooOld error at startup.

Install

Clone the repository and build:

git clone <repo-url> swarm
cd swarm
cargo build --release

The binary is at target/release/swarm. Add it to your PATH or use cargo install --path swarm.

To enable WASM sandboxed tools (optional):

cargo build --release --features wasm-sandbox

Initialize a Project

Navigate to a git repository and run:

cd /path/to/your-project
swarm init

This creates ~/.swarm/settings.json with a starter configuration for your project. The config is keyed by the absolute, canonicalized path to your project directory.

Tip: You can also pass --path /path/to/project to initialize a different directory.

Configure Agents

Open ~/.swarm/settings.json and define your agents. Here is a minimal two-agent configuration:

{
  "version": 2,
  "/home/user/my-project": {
    "providers": {
      "default": {
        "type": "anthropic",
        "api_key_env": "ANTHROPIC_API_KEY"
      }
    },
    "defaults": {
      "model": "sonnet"
    },
    "agents": [
      {
        "name": "backend",
        "prompt": "You are a backend engineer. Work on server-side code, APIs, and database logic."
      },
      {
        "name": "frontend",
        "prompt": "You are a frontend engineer. Work on UI components, styling, and client-side logic."
      }
    ]
  }
}

Each agent needs:

  • name — Unique identifier matching [a-z][a-z0-9-]*
  • prompt — System prompt text, or @path/to/file to load from a file

See Writing Agents for the full configuration guide.

Set Your API Key

Export your Anthropic API key:

export ANTHROPIC_API_KEY="sk-ant-..."

The environment variable name can be customized per provider via the api_key_env field.

Start a Session

swarm start

This launches the orchestrator, which:

  1. Loads and validates your configuration
  2. Checks git prerequisites (clean tree, version)
  3. Creates a session with ID format YYYYMMDD-XXXX
  4. Creates a git worktree per agent at .swarm/worktrees/<name>
  5. Opens the SQLite mailbox at .swarm/messages.db
  6. Spawns all agents in parallel
  7. Opens the TUI dashboard

If your working tree has uncommitted changes, use --stash to auto-stash them:

swarm start --stash

To run without the TUI (log output to terminal instead):

swarm start --no-tui

The TUI Dashboard

The TUI displays a panel for each agent showing:

  • Agent name and current state (e.g., Running, CoolingDown)
  • Session sequence number
  • Live log output

Key Bindings

KeyAction
Tab / Shift+TabCycle focus between agent panels
19Jump to agent panel by index
lToggle log viewer overlay
eToggle event viewer overlay
qQuit TUI (session keeps running)
:Open input bar for commands

The TUI refreshes at approximately 30 FPS (33ms frame interval).

Send Messages to Agents

From a separate terminal, send a message to a specific agent:

swarm send backend "Add a health check endpoint at GET /health"

Or broadcast to all agents:

swarm broadcast "Please commit your current work"

For messages that should interrupt an agent immediately:

swarm send backend "Stop what you're doing and fix the failing tests" --urgent

Urgent messages trigger the router interrupt — the agent's current session is cancelled and it restarts with the urgent message in its prompt.

Check Status

swarm status

This shows each agent's current state, session sequence, and error counts. Add --json for machine-readable output.

Stop the Session

When you're done, stop the session and merge all agent work:

swarm stop --merge

Stop modes:

FlagBehavior
--mergeMerge each agent's worktree branch into the base branch
--squashSquash-merge each agent's work into a single commit
--discardDiscard all agent work and clean up worktrees

If no flag is provided, swarm prompts for your choice.

The stop sequence:

  1. Signals all agents to stop
  2. Waits for graceful shutdown
  3. Applies the chosen merge strategy
  4. Removes worktrees and session artifacts
  5. Cleans up the lockfile

View Logs

To view an agent's logs:

swarm logs backend

Follow logs in real time:

swarm logs backend --follow

View logs from a specific session:

swarm logs backend --session 2

Clean Up

If a session crashed or left stale artifacts:

swarm clean

Use --force to remove artifacts without confirmation.

Next Steps

Architecture

This page describes swarm's high-level architecture: the crate structure, module map, data flow between components, and key design decisions.

Crate Structure

Swarm is a single-crate Rust project organized as a Cargo workspace with one member:

swarm/                  # Workspace root
├── Cargo.toml          # Workspace manifest (members = ["swarm"])
└── swarm/              # Main crate
    ├── Cargo.toml      # Crate manifest
    └── src/
        ├── lib.rs      # Module declarations and crate-level docs
        ├── main.rs     # Binary entry point (CLI parsing → orchestrator)
        └── ...         # All modules below

The crate exposes a library (swarm_lib) and a binary (swarm). The binary is a thin wrapper that parses CLI arguments and delegates to the orchestrator.

Module Map

ModulePurpose
cliCLI argument parsing via clap (commands, flags, subcommands)
configSettings file loading, validation, and resolution (raw → resolved types)
orchestratorTop-level session lifecycle: start (13-step flow), stop, status
sessionSession ID generation, session.json management, PID-based liveness
agent::stateAgent state machine (AgentState, AgentEvent, SideEffect)
agent::runnerAgent lifecycle loop driver (prompt → spawn → run → repeat)
agent::registryCentral registry of all running agents and their handles
backendAgentBackend trait abstraction for LLM providers (Anthropic, mock)
prompt14-section prompt assembly pipeline (build_prompt())
mailboxSQLite-backed per-agent message broker with threading and urgency
routerAsync message router that polls for urgent messages and sends interrupts
toolsTool trait, ToolRegistry, and all built-in tools
tools::wasmWASM sandboxed tool execution (feature-gated: wasm-sandbox)
permissionsPermission rules, sets, modes, and evaluation logic
skillsSkill discovery, frontmatter parsing, argument substitution, resolution
mcpModel Context Protocol client, transport (HTTP/SSE/Stdio), and manager
hooksHook configuration, event types, and script execution
worktreeGit worktree creation, cleanup, merging, and recovery
tuiTerminal UI application (agent panels, log viewer, event viewer, input)
livenessAgent liveness monitoring (idle nudges, stall detection, warnings)
iterationIteration engine for repeated task-solving loops
workflowWorkflow pipeline definitions and execution
conversationConversation history management
context_windowContext window size tracking and management
supervisorSupervisor agent logic and merge-focused prompt
tasksTask system integration
modesAgent execution modes (code, delegate, etc.)
loggingStructured logging setup
errorsError types for all subsystems
historySession history and archiving

Data Flow

Session Start (13-Step Flow)

CLI (swarm start)
  │
  ├── 1. Load config (~/.swarm/settings.json)
  ├── 2. Validate git prerequisites (version, repo, not detached)
  ├── 3. Handle --init flag
  ├── 4. Handle --stash or require clean working tree
  ├── 5. Check for stale session + recovery
  ├── 6. Create session (session.json + lockfile)
  ├── 7. Create worktrees (one per agent + supervisor)
  ├── 8. Initialize SQLite mailbox database
  ├── 9. Create agent runners + registry
  ├── 10. Start message router (100ms poll loop)
  ├── 11. Start periodic tasks (WAL checkpoint, message prune)
  ├── 12. Launch TUI or headless mode
  └── 13. Await shutdown signal → graceful shutdown

Agent Lifecycle Loop

Each agent runs independently through its state machine:

Initializing → BuildingPrompt → Spawning → Running → SessionComplete
                    ↑                          │            │
                    │                          │            │
                    │    ┌─────── CoolingDown ←┘ (on error) │
                    │    │  (exponential backoff)            │
                    │    ↓                                   │
                    └────┴───────────────────────────────────┘
                                                    (next session)

The runner loop for each agent:

  1. Build prompt — Assembles a 14-section system prompt with environment info, role, tools, pending messages, beads tasks, etc.
  2. Spawn backend session — Sends the prompt to the configured LLM provider (Anthropic API)
  3. Run — The backend session executes, making tool calls that the runner handles
  4. Handle exit — On success, transition to SessionComplete; on error, enter CoolingDown with exponential backoff
  5. Repeat — After cooldown or session complete, rebuild prompt and spawn again

Message Flow

Agent A                    SQLite DB                    Agent B
   │                          │                            │
   ├── send(to=B, body) ─────►│                            │
   │                          ├── INSERT INTO messages ────►│
   │                          │                            │
   │                     Router (100ms poll)                │
   │                          ├── poll_urgent() ───────────►│
   │                          │   (if urgent)     InterruptSignal
   │                          │                            │
   │                          │◄── consume() ──────────────┤
   │                          │   (next prompt build)      │

Shutdown Flow

SIGTERM received (or operator stop)
  │
  ├── Signal all agents: OperatorStop event
  ├── Wait for all agents to reach Stopped state
  ├── Auto-commit any dirty worktrees
  ├── Merge/squash/discard agent branches (based on StopMode)
  ├── Remove worktrees and prune
  ├── Delete session branches
  ├── Remove session.json + lockfile
  └── Exit

Key Dependencies

DependencyUsed For
tokioAsync runtime (ADR-001)
clapCLI argument parsing
serde / serde_jsonConfiguration and message serialization
rusqliteSQLite mailbox (ADR-002)
ratatuiTerminal UI rendering (ADR-007)
tracingStructured logging
reqwestHTTP client for Anthropic API and MCP transports
chronoTimestamp handling
anyhow / thiserrorError handling (ADR-009)
wasmtimeWASM sandbox runtime (optional, feature-gated)
libcProcess liveness checks (kill signal 0)

Design Decisions

The architecture is shaped by several key decisions documented in ADRs:

Component Interactions

                    ┌──────────────┐
                    │     CLI      │
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐
                    │ Orchestrator │──────────────────┐
                    └──────┬───────┘                  │
                           │                          │
              ┌────────────┼────────────┐      ┌──────▼──────┐
              │            │            │      │   Session    │
        ┌─────▼────┐ ┌────▼─────┐ ┌────▼────┐ │  Management │
        │ Agent 1  │ │ Agent 2  │ │ Agent N │ └─────────────┘
        │ Runner   │ │ Runner   │ │ Runner  │
        └────┬─────┘ └────┬─────┘ └────┬────┘
             │            │            │
        ┌────▼────────────▼────────────▼────┐
        │           Agent Registry          │
        └────┬─────────────────────────┬────┘
             │                         │
      ┌──────▼───────┐         ┌──────▼───────┐
      │   Backend    │         │   Mailbox    │
      │  (Anthropic) │         │   (SQLite)   │
      └──────────────┘         └──────┬───────┘
                                      │
                               ┌──────▼───────┐
                               │    Router    │
                               │ (100ms poll) │
                               └──────────────┘

Agent Lifecycle

Each swarm agent follows a deterministic state machine that drives its lifecycle from initialization through multiple backend sessions to eventual shutdown. The state machine is defined in agent::state and executed by the runner loop in agent::runner.

Agent States

The AgentState enum defines 8 observable states:

StateDescription
InitializingAgent registered; waiting for its git worktree to be ready
BuildingPromptAssembling the system prompt (environment, role, tools, messages, tasks)
SpawningPrompt stored; launching a backend session with the LLM provider
Running { session_seq }Backend session is active; session_seq tracks which session iteration
Interrupting { session_seq }Graceful cancellation requested (urgent message received); waiting for session exit
SessionCompleteBackend session exited successfully; ready for next iteration
CoolingDown { until }Session failed; waiting for exponential backoff to elapse
StoppedTerminal state — agent will not run again

Check if an agent has reached its terminal state with AgentState::is_terminal(), which returns true only for Stopped.

Agent Events

The AgentEvent enum defines the events that drive state transitions:

EventTrigger
WorktreeReadyGit worktree created and ready for use
PromptReady(String)System prompt assembled successfully
SessionStarted(u32)Backend session launched (carries the session sequence number)
SessionExited(ExitOutcome)Backend session ended — Success, Error(String), or Timeout
UrgentMessageRouter detected an urgent message for this agent
GraceExceededInterruption grace period expired without session exit
BackoffElapsedCoolingDown timer expired
OperatorStopOperator requested shutdown (global — valid from any state)
FatalError(String)Unrecoverable error (global — valid from any state)

Side Effects

Each transition returns a SideEffect telling the runner what action to take:

SideEffectRunner Action
NoneNo action needed
StorePrompt(String)Save the assembled prompt for the next spawn
CancelSessionRequest graceful cancellation of the current backend session
ForceStopSessionForce-stop the session immediately (grace period exceeded)
IncrementSessionBump session sequence counter and loop back to BuildingPrompt
LogFatal(String)Log the fatal error message; agent is now Stopped

State Diagram

                    ┌──────────────┐
                    │ Initializing │
                    └──────┬───────┘
                           │ WorktreeReady
                    ┌──────▼────────┐
              ┌────►│ BuildingPrompt │◄─────────────────────┐
              │     └──────┬────────┘                       │
              │            │ PromptReady                    │
              │     ┌──────▼───────┐                        │
              │     │   Spawning   │──── SessionExited ─────┤
              │     └──────┬───────┘    (Error/Timeout)     │
              │            │ SessionStarted                 │
              │     ┌──────▼───────┐                 ┌──────┴──────┐
              │     │   Running    │── Error/Timeout─►│ CoolingDown │
              │     └──┬───┬───────┘                 └──────┬──────┘
              │        │   │ UrgentMessage                  │ BackoffElapsed
              │        │   │                                │
              │        │ ┌─▼────────────┐                   │
              │        │ │ Interrupting  │──────────────────►│
              │        │ └──────────────┘                   │
              │        │ SessionExited(Success)              │
              │ ┌──────▼────────┐                           │
              │ │SessionComplete│                           │
              │ └──────┬────────┘                           │
              │        │ WorktreeReady                      │
              └────────┘◄───────────────────────────────────┘

        ── OperatorStop or FatalError from ANY state ──► Stopped

Error Thresholds and Backoff

The state machine tracks two error counters:

CounterDefault LimitBehavior
consecutive_errors5 (max_consecutive_errors)Reset to 0 on SessionStarted or SessionExited(Success)
total_errors20 (max_total_errors)Never reset; accumulates across all sessions

When either counter reaches its limit, the agent transitions to Stopped with a LogFatal side effect.

Backoff Formula

When an error occurs, the agent enters CoolingDown with exponential backoff:

duration_ms = min(2000 * 2^(n-1), 60000)

Where n is consecutive_errors (after increment). Examples:

Consecutive ErrorsBackoff Duration
12,000 ms
24,000 ms
38,000 ms
416,000 ms
532,000 ms
6+60,000 ms (cap)

Agent Registry

The AgentRegistry (agent::registry) is the central data structure that tracks all running agents:

  • AgentHandle — Bundles an agent's resolved config, state watch channel, interrupt sender, and task join handle
  • Registrationregister() adds a new agent handle; each agent gets a unique name
  • State queriesstates() returns a snapshot of all agent states; state_of(name) queries a single agent
  • Interrupt deliveryinterrupt_senders() returns a map of interrupt channels for the router
  • Shutdownshutdown() sends OperatorStop to all agents and awaits their task handles

Runner Loop

The run_agent() function in agent::runner is the top-level entry point for each agent's lifecycle:

  1. Setup — Create worktree, initialize environment variables, fire SessionStart hook
  2. State machine loop — Process events, execute side effects, manage the backend session
  3. Session iteration — On SessionComplete + WorktreeReady, increment sequence and rebuild prompt
  4. Interrupt handling — On UrgentMessage, cancel the session with a grace period; force-stop on GraceExceeded
  5. Cleanup — On Stopped, archive session logs, fire SessionEnd hook, prune old logs

The runner manages environment variables injected into each backend session:

  • SWARM_AGENT_ID — The agent's name
  • SWARM_SESSION_ID — The current session ID
  • SWARM_DB_PATH — Path to the SQLite mailbox database
  • SWARM_AGENTS — Comma-separated list of all agent names

Messaging

Swarm agents communicate through a SQLite-backed mailbox system. Messages are stored durably in a shared database, delivered to recipients on their next prompt build, and can trigger real-time interrupts for urgent communications.

Design

The messaging system uses SQLite in WAL (Write-Ahead Logging) mode as the message store. This choice (ADR-002) provides:

  • Durability — Messages survive process crashes
  • Concurrent access — WAL mode allows multiple readers with a single writer
  • No external dependencies — No message broker or network service required
  • Simplicity — A single file at .swarm/messages.db

Message Structure

The Message struct represents a single message:

FieldTypeDescription
idi64Auto-incrementing primary key
thread_idOption<i64>ID of the root message in the thread (for grouping)
reply_toOption<i64>ID of the message this is a direct reply to
senderStringName of the sending agent (or "operator" for CLI messages)
recipientStringName of the receiving agent
msg_typeMessageTypeDiscriminator: Message, Task, Status, or Nudge
urgencyUrgencyNormal or Urgent
bodyStringThe message content
created_ati64Epoch nanoseconds when the message was created
delivered_atOption<i64>Epoch nanoseconds when consumed; NULL while pending

MessageType

VariantUsage
MessageGeneral inter-agent communication
TaskTask assignment or delegation
StatusStatus updates between agents
NudgeLiveness nudge from the monitoring system

Urgency

VariantBehavior
NormalDelivered on the recipient's next prompt build
UrgentTriggers an interrupt via the router, causing the recipient to restart its session

Mailbox Operations

The Mailbox struct provides per-agent messaging operations:

OperationDescription
send(recipient, body, msg_type, urgency)Send a message to another agent (self-send rejected)
reply(original_id, body, msg_type, urgency)Reply to an existing message, inheriting thread context
broadcast(recipients, body, msg_type, urgency)Send to multiple agents in a single transaction
consume()Atomically read and mark all pending messages as delivered
thread(thread_id)Retrieve all messages in a conversation thread
outbox(limit)Get recently sent messages

Free functions are also available for use outside the Mailbox context:

  • send_message() — Send a message using a raw connection
  • broadcast_message() — Broadcast to multiple recipients
  • consume_messages() — Consume pending messages for an agent

Message Router

The router module runs an async polling loop that watches for urgent messages:

Router Loop (every 100ms):
  1. poll_urgent(conn) → Vec<UrgentMessage>
  2. For each urgent message:
     a. Skip if already signalled (deduplication via HashSet)
     b. Send InterruptSignal to recipient's mpsc channel
     c. Add to signalled set
  3. Sleep 100ms
  4. Exit on shutdown signal

When the router sends an InterruptSignal, the agent's runner receives an UrgentMessage event, which triggers the interrupt flow:

  1. RunningInterrupting (with CancelSession side effect)
  2. The backend session is gracefully cancelled
  3. On session exit → BuildingPrompt (the new prompt will include the urgent message)

Message Threading

Messages can be organized into threads using thread_id and reply_to:

  • When you send a new message, thread_id and reply_to are NULL
  • When you reply to a message, the reply inherits the original's thread_id (or uses the original's id as the thread root)
  • The thread() method retrieves all messages sharing the same thread_id

Database Configuration

The SQLite database is configured with these PRAGMAs:

PRAGMAValuePurpose
journal_modeWALConcurrent reads, single writer
busy_timeout5000 msWait up to 5 seconds on lock contention

Periodic maintenance tasks run in the background:

TaskIntervalAction
WAL checkpoint60 secondsPRAGMA wal_checkpoint(TRUNCATE) — reclaims WAL file space
Message prune300 secondsDelete old delivered messages, keeping the most recent 1000

Message Flow in Practice

  1. Agent A calls the mailbox tool to send a message to Agent B
  2. The message is INSERTed into the messages table with delivered_at = NULL
  3. If the message is urgent, the router detects it within 100ms and sends an InterruptSignal to Agent B
  4. Agent B's runner cancels its current session and rebuilds the prompt
  5. On the next prompt build, consume() marks all pending messages as delivered and includes them in the system prompt
  6. Agent B reads the messages in its prompt context and responds accordingly

Orchestration

The orchestrator is the top-level component that manages the entire swarm lifecycle. It implements the 13-step start flow, handles shutdown, and coordinates all subsystems.

Session Management

Each swarm run creates a session represented by a SessionInfo struct:

FieldTypeDescription
idStringFormat YYYYMMDD-XXXX (date + 4 random hex chars, e.g. 20250115-a3f2)
base_commitStringThe HEAD commit hash at session start
agentsVec<String>List of agent names from the config
started_atDateTime<Utc>UTC timestamp of session creation
pidu32Process ID of the orchestrator (used for liveness checks)

Session state is persisted in .swarm/session.json alongside a lockfile containing the PID. Both files are written atomically (temp file then rename).

Stale Session Detection

A session is considered stale if its owning process no longer exists. This is checked using libc::kill(pid, 0):

  • Returns 0 — process alive, session is active
  • Returns -1 with ESRCH — process gone, session is stale

Stale sessions are automatically recovered before creating a new one.

The 13-Step Start Flow

When you run swarm start, the orchestrator executes these steps in order:

Step 1: Load Configuration

Read ~/.swarm/settings.json, validate the version, look up the project by its canonicalized path, and resolve all defaults into a ResolvedConfig.

Step 2: Validate Git Prerequisites

  • Check git version >= 2.20
  • Verify the project is a git repository
  • Confirm HEAD is not detached

Step 3: Handle --init Flag

If --init is set and the repo needs initialization, run init_git_repo(). If the repo already exists, this is a no-op.

Step 4: Handle Working Tree State

  • If --stash is set: auto-stash uncommitted changes (git stash push --include-untracked -m "swarm auto-stash")
  • Otherwise: require a clean working tree (git status --porcelain must be empty)

Step 5: Check for Stale Session

If .swarm/session.json exists:

  • If the process is alive: bail with "session already active"
  • If the process is dead: recover the stale session (auto-commit, remove worktrees, delete branches)

Step 6: Create Session

Generate a session ID, write session.json and lockfile atomically.

Step 7: Create Worktrees

For each agent and the supervisor, create a git worktree:

git worktree add .swarm/worktrees/<name> -b swarm/<session_id>/<name> <base_commit>

Lock each worktree to prevent accidental pruning.

Step 8: Initialize SQLite

Open (or create) the mailbox database at .swarm/messages.db with WAL mode enabled.

Step 9: Create Agent Runners and Registry

For each resolved agent config:

  1. Create an AgentHandle with state channels and interrupt sender
  2. Spawn the run_agent() task on Tokio
  3. Register in the AgentRegistry

Step 10: Start Message Router

Launch the async router loop that polls for urgent messages every 100ms and delivers InterruptSignals to the appropriate agent channels.

Step 11: Start Periodic Tasks

  • WAL checkpoint: Every 60 seconds, run PRAGMA wal_checkpoint(TRUNCATE)
  • Message prune: Every 300 seconds, delete old delivered messages (keep recent 1000)

Step 12: Launch TUI or Headless Mode

  • Default: Launch the TUI with agent panels, log viewer, and command input
  • --no-tui: Run in headless mode, logging to stdout

Step 13: Await Shutdown

Block until a shutdown signal is received (SIGTERM, TUI quit, or all agents stopped), then execute graceful shutdown.

Stop Modes

When a session is stopped (swarm stop), agent branches are handled according to the stop mode:

ModeFlagBehavior
Merge--merge (default)git merge --no-ff each agent branch into the base branch, in config order
Squash--squashgit merge --squash each agent branch, creating a single commit per agent
Discard--discardDelete agent branches without merging any changes

The merge order is: agent branches first (in the order defined in settings.json), then the supervisor branch.

Shutdown Sequence

The graceful shutdown sequence runs inside the orchestrator process:

  1. Signal all agents — Send OperatorStop to each agent via the registry
  2. Wait for agents — Wait for all agents to reach the Stopped state
  3. Stop router — Signal the router's shutdown channel
  4. Auto-commit — For each worktree (agents + supervisor), commit any dirty changes
  5. Merge branches — Apply the selected stop mode (merge/squash/discard)
  6. Remove worktrees — Unlock and remove each worktree
  7. Prune worktrees — Run git worktree prune to clean stale references
  8. Delete branches — Remove all swarm/<session_id>/* branches
  9. Remove session — Delete session.json and lockfile
  10. Exit

When swarm stop is run from a separate terminal:

  1. Load the session from .swarm/session.json
  2. Send SIGTERM to the orchestrator PID
  3. Wait up to 60 seconds for the process to exit
  4. If session files remain after exit, perform cleanup from the stop side

Status Command

swarm status provides a snapshot of the current session:

Session: 20250115-a3f2 (active)
Started: 2025-01-15T10:30:00Z (2h 15m ago)
Base commit: abc123def456
PID: 12345

Agents:
  ● backend          Running (1h 23m)
  ● frontend         Running (45m 12s)
  ● reviewer         SessionComplete (idle 5m)

Beads: 3 ready, 2 claimed, 8 closed

With --json, the output is a structured JSON object including agent states, liveness data, and beads summary.

Prompt Pipeline

The prompt pipeline assembles a comprehensive system prompt for each agent session. It gathers environment information, role instructions, tool descriptions, pending messages, beads tasks, and session context into a structured multi-section prompt that guides the agent's behavior.

Overview

The build_prompt() function in prompt.rs orchestrates prompt assembly. Each time an agent transitions to BuildingPrompt, a fresh prompt is assembled from current state — there is no cached or incremental prompt. This ensures the agent always sees the latest messages, tasks, and environment.

Prompt Sections

The prompt is assembled from up to 14 numbered sections, each conditionally included based on available context:

#SectionContentConditional
1IdentityAgent name, swarm contextAlways
2Agent RoleThe agent's configured prompt textAlways
3Mode InstructionsBehavior rules for the agent's execution mode (code, delegate, etc.)When mode is set
4Workflow ContextCurrent workflow stage, inputs, and constraintsWhen in a workflow
5Project InstructionsContents of AGENTS.md from the project rootWhen file exists
6Core MandatesUniversal rules: commit discipline, branch hygiene, communication protocolAlways
7Doing TasksHow to approach coding tasks, use tools, handle errorsAlways
8Tool Usage PolicyRules for tool selection, permission handlingAlways
9Swarm WorkflowInter-agent communication protocol, when to message teammatesAlways
10Tone & StyleOutput formatting guidelinesAlways
11EnvironmentPlatform, OS, git status, recent commits, working directoryAlways
12MessagesPending messages from other agents (consumed from mailbox)When messages exist
13Beads TasksAvailable tasks from bd ready --jsonWhen beads is available
14Session ContextSession ID, session sequence, interrupt contextAlways

PromptContext

The PromptContext struct carries all the data needed for prompt assembly:

FieldDescription
agent_nameName of the agent being prompted
agent_promptThe agent's configured prompt text
modeAgent execution mode
session_idCurrent session ID
session_seqSession iteration number
db_pathPath to the SQLite mailbox database
worktree_pathAgent's worktree directory
agent_namesList of all agents in the session
workflow_contextOptional workflow stage context
interrupt_contextOptional interrupt reason

Environment Information

The EnvironmentInfo struct gathers runtime context:

FieldSource
platformstd::env::consts::OS
os_versionuname -r output
shell$SHELL environment variable
cwdCurrent working directory
git_statusgit status --short output
recent_commitsgit log --oneline -5 output
dateCurrent date

Message Formatting

Pending messages are consumed from the mailbox and formatted with urgency labels:

## Messages from teammates

[URGENT] From backend (2m ago):
The API endpoint /users is returning 500 errors, please check the database migration.

From frontend (5m ago):
I've finished the login page UI, ready for API integration.

Urgent messages are prefixed with [URGENT] to draw the agent's attention.

Beads Task Integration

When the bd CLI is available, the prompt pipeline runs bd ready --json with a timeout to discover available tasks:

## Available Tasks (from beads)

- SWARM-42: [open] Implement user authentication endpoint
- SWARM-43: [open] Add input validation to signup form
- SWARM-44: [in_progress] Write integration tests for login flow

This allows agents to autonomously pick up and work on tracked issues.

Interrupt Context

When an agent is interrupted (due to an urgent message), the interrupt context is included in the rebuilt prompt:

## Interrupt Context

You were interrupted by an urgent message. Your previous session was cancelled
so you could process this message. Review the messages section above and respond
to the urgent request.
  • Agent Lifecycle — When prompts are built in the state machine
  • Messaging — How messages are consumed into the prompt
  • Configuration — Agent prompt configuration
  • Skills — How skills inject into the prompt context

Skills

Skills are markdown-based prompt templates that extend agent capabilities. Each skill is a markdown file with YAML frontmatter defining metadata and a body containing the instructions injected into the agent's prompt when invoked.

Skill File Format

A skill file has two parts:

---
name: review-pr
description: Review a pull request
user-invocable: true
argument-hint: "<PR number>"
---

Review pull request $ARGUMENTS and provide feedback on:
1. Code quality
2. Test coverage
3. Security concerns

Frontmatter Fields

The YAML frontmatter (between --- delimiters) supports these fields:

FieldTypeRequiredDefaultDescription
nameStringNoDerived from filenameSkill identifier
descriptionStringNo""Human-readable description
user-invocableboolNofalseWhether users can invoke this skill directly
allowed-toolsVec<String>No[]Tools the skill is allowed to use
modelStringNonullModel override for this skill
contextStringNonullAdditional context instructions
agentStringNonullTarget agent for the skill
hooksVec<String>No[]Hook events this skill responds to
argument-hintStringNonullHint text shown when skill expects arguments
unsafeboolNofalseWhether the skill performs potentially dangerous operations

Frontmatter uses kebab-case field names (e.g., user-invocable) which are deserialized to snake_case internally.

If frontmatter parsing fails, the skill still loads with default values — this is non-fatal.

Argument Substitution

Skill bodies support argument placeholders that are replaced at invocation time:

PlaceholderReplacement
$ARGUMENTSThe full argument string passed to the skill
$ARGUMENTS0 through $ARGUMENTS9Positional arguments (0-indexed)
$0 through $9Shorthand for positional arguments

Example:

---
name: compare
description: Compare two files
---

Compare the files $0 and $1, highlighting the differences.

Invoked as /compare src/old.rs src/new.rs, this becomes:

Compare the files src/old.rs and src/new.rs, highlighting the differences.

Resolution Order

When a skill is invoked by name, swarm searches three directories in priority order:

PriorityPathStyle
1.claude/skills/<name>/SKILL.mdProject-local, directory-style
2.skills/<name>.mdProject-local, flat files (backward-compatible)
3~/.claude/skills/<name>/SKILL.mdGlobal user skills

The first match wins. This means project-local skills override global ones.

Skill Names

Valid skill names contain only: [a-zA-Z0-9_:-]. Names with other characters are rejected.

Skill Discovery

The discover_skills() function scans all three directories and returns a BTreeMap<String, SkillSummary> of available skills:

FieldDescription
nameSkill name
descriptionFrom frontmatter
user_invocableWhether the skill can be invoked by users

Discovery is sorted alphabetically for consistent ordering.

Skill Resolution

The resolve() function finds a skill by name and returns a fully-resolved SkillDefinition:

FieldDescription
pathFile path where the skill was found
frontmatterParsed SkillFrontmatter
bodySkill body text (with frontmatter stripped)

Tools

Tools are the primary mechanism through which agents interact with the environment. Each tool implements a common trait and is registered in a central registry that the backend session uses for tool calls.

Tool Trait

The Tool trait defines the interface every tool must implement:

#![allow(unused)]
fn main() {
pub trait Tool: Send + Sync {
    fn name(&self) -> &str;
    fn description(&self) -> &str;
    fn input_schema(&self) -> serde_json::Value;
    fn execution_mode(&self) -> ExecutionMode { ExecutionMode::Native }
    fn execute(&self, input: Value, ctx: ToolContext)
        -> Pin<Box<dyn Future<Output = ToolResult> + Send + '_>>;
}
}
MethodDescription
name()Unique identifier for the tool (e.g., "bash", "read")
description()Human-readable description shown to the LLM
input_schema()JSON Schema defining the expected input parameters
execution_mode()Native (in-process) or Sandboxed (WASM) — defaults to Native
execute()Async execution function taking input JSON and a ToolContext

ToolResult

Every tool execution returns a ToolResult:

FieldTypeDescription
contentVec<ToolResultContent>One or more content blocks
is_errorboolWhether this result represents an error (signals retry to the LLM)

Content blocks can be:

  • Text — String content
  • Image — Base64-encoded image with media type

Helper constructors:

  • ToolResult::text(s) — Creates a successful text result
  • ToolResult::error(s) — Creates an error text result with is_error = true

ExecutionMode

ModeDescription
NativeRuns in-process with full system access
SandboxedRuns in a WASM sandbox with resource limits and capability restrictions

ToolContext

The ToolContext struct provides execution context to tools:

FieldDescription
working_dirThe agent's worktree directory
agent_nameName of the executing agent
session_idCurrent session ID
env_varsEnvironment variables to inject
cancellation_tokenToken to check if the session has been cancelled
permissionsOptional PermissionEvaluator for permission checks

The context supports fluent building with with_env() and with_permissions().

ToolRegistry

The ToolRegistry manages tool registration and lookup:

MethodDescription
register(tool)Add a tool to the registry (insertion-order preserved)
get(name)Look up a tool by name
names()List all registered tool names
definitions()Return tool definitions (name, description, schema) for the LLM
execute(name, input, ctx)Execute a tool by name
retain(predicate)Remove tools that don't match a predicate
register_mcp_tools(tools)Register tools from MCP servers
register_wasm_tools(tools)Register WASM sandboxed tools (feature-gated)

Default Registry

default_registry() creates a registry pre-populated with all built-in tools.

Built-in Tools

Swarm ships with these native tools:

ToolDescription
bashExecute shell commands
readRead file contents
writeWrite/create files
editEdit files with search-and-replace
globFind files by pattern
grepSearch file contents with regex
notebookEdit Jupyter notebook cells
web_fetchFetch and process web content
web_searchSearch the web
ask_userAsk the operator a question
mailboxSend messages to other agents
sub_agentDelegate tasks to sub-agents
taskInteract with the task system
skillExecute a skill by name
mcp_proxyProxy tool calls to MCP servers
workflow_outputReport outputs from workflow stages

WASM Tools

When the wasm-sandbox feature is enabled, additional tools can be loaded from compiled WebAssembly components. These run in a sandboxed environment with configurable resource limits and capabilities. See WASM Tools for details.

MCP Integration

Swarm integrates with external tool servers via the Model Context Protocol (MCP). This allows agents to use tools provided by external processes, expanding capabilities beyond the built-in tool set.

Architecture

The MCP integration consists of three layers:

ComponentModulePurpose
McpClientmcp::clientSingle-server JSON-RPC client for tool discovery and invocation
McpManagermcp::managerMulti-server lifecycle manager (start, route, shutdown)
Transportsmcp::transportConnection layer (Stdio, HTTP, SSE)

Transports

Swarm supports three MCP transport types:

Stdio

Launches the MCP server as a subprocess. Communication happens over stdin/stdout using JSON-RPC.

{
  "transport": {
    "type": "stdio",
    "command": "npx",
    "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"]
  }
}

HTTP

Connects to an MCP server over HTTP. Each JSON-RPC request is a POST.

{
  "transport": {
    "type": "http",
    "url": "http://localhost:3000/mcp",
    "headers": {
      "Authorization": "Bearer token123"
    }
  }
}

SSE (Server-Sent Events)

Connects via SSE for server-push capabilities, with HTTP POST for client requests.

{
  "transport": {
    "type": "sse",
    "url": "http://localhost:3000/sse",
    "headers": {}
  }
}

McpClient

The McpClient wraps a transport and provides the MCP protocol methods:

MethodDescription
initialize()Send the MCP initialization handshake
list_tools()Discover available tools from the server
call_tool(name, args)Invoke a tool with JSON arguments
shutdown()Gracefully close the connection

Each client uses an IdGenerator for unique JSON-RPC request IDs.

McpToolDefinition

Tools discovered from MCP servers are represented as:

FieldTypeDescription
nameStringTool name as reported by the server
descriptionOption<String>Human-readable tool description
input_schemaValueJSON Schema for tool inputs

McpManager

The McpManager manages the lifecycle of multiple MCP server connections:

MethodDescription
start_all(configs)Connect to all configured servers; skip failures with warnings
connect_server(name, config)Connect to a single server
all_tool_definitions()Collect and prefix tools from all servers
call_tool(prefixed_name, args)Route a tool call to the correct server
shutdown_all()Gracefully shutdown all server connections

Tool Namespacing

To avoid name collisions between different MCP servers and built-in tools, MCP tools are prefixed with the server name:

mcp__<server_name>__<tool_name>

For example, a tool read_file from a server named filesystem becomes:

mcp__filesystem__read_file

The parse_prefixed_name() function extracts the server and tool names from this format. The PrefixedToolDefinition struct carries the original tool definition along with the prefixed name.

Configuration

MCP servers are configured in the mcpServers section of settings.json:

{
  "mcpServers": {
    "filesystem": {
      "transport": {
        "type": "stdio",
        "command": "npx",
        "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
      },
      "env": {
        "NODE_PATH": "/usr/local/lib/node_modules"
      }
    },
    "api": {
      "transport": {
        "type": "http",
        "url": "http://localhost:8080/mcp"
      }
    }
  }
}

Each server entry has:

FieldTypeDescription
transportMcpTransportConnection configuration (stdio/http/sse)
envHashMap<String, String>Optional environment variables for the server process

Server Lifecycle

  1. During orchestrator start (Step 9), the McpManager connects to all configured servers
  2. Tool definitions are fetched and registered in the ToolRegistry with prefixed names
  3. During agent sessions, MCP tool calls are routed through the manager to the correct server
  4. On shutdown, all servers are gracefully disconnected

Failed server connections are logged as warnings but don't prevent the swarm from starting.

Configuration

Swarm uses a centralized configuration file at ~/.swarm/settings.json that defines projects, agents, providers, permissions, and more. The config system distinguishes between raw (as-written) and resolved (fully-defaulted) types.

Settings File Location

~/.swarm/settings.json

Created automatically by swarm init, or manually. The file uses JSON format with a required version field and project entries keyed by absolute path.

File Structure

{
  "version": 2,
  "/absolute/path/to/project": {
    "providers": { ... },
    "agents": [ ... ],
    "supervisor": { ... },
    "defaults": { ... },
    "permissions": { ... },
    "hooks": { ... },
    "mcpServers": { ... },
    "wasm_tools": [ ... ],
    "sub_agent_defaults": { ... }
  }
}

Version

The version field must be 1 or 2. Version 2 is the current schema. Versions above the supported maximum are rejected.

Providers

Named provider blocks describe how to reach an LLM API:

"providers": {
  "default": {
    "type": "anthropic",
    "api_key_env": "ANTHROPIC_API_KEY",
    "base_url": null,
    "max_retries": null,
    "timeout": null
  }
}
FieldTypeRequiredDefaultDescription
typeStringYesProvider type (currently only "anthropic")
api_key_envStringNo"ANTHROPIC_API_KEY"Environment variable holding the API key
base_urlStringNonullCustom API base URL
max_retriesu32NonullMax retries for transient failures
timeoutu64NonullRequest timeout in seconds

If no providers block is defined, an implicit "default" Anthropic provider is created.

Agents

An array of agent definitions (at least one required):

"agents": [
  {
    "name": "backend",
    "prompt": "You are a backend engineer. Focus on API and data layer.",
    "model": "sonnet",
    "provider": "default",
    "permissions": { ... },
    "delegate_mode": false,
    "mode": "code"
  }
]
FieldTypeRequiredDefaultDescription
nameStringYesUnique name matching [a-z][a-z0-9-]*
promptStringYesSystem prompt text, or @path/to/file to load from file
modelStringNodefaults.model or "sonnet"Model identifier
providerStringNodefaults.provider or "default"Provider name
permissionsPermissionsConfigNonullAgent-level permission overrides
delegate_modeboolNofalseLegacy flag for delegate mode
modeStringNoSee resolutionAgent execution mode

Prompt resolution: If the prompt starts with @, the remainder is treated as a file path relative to the project root, and the file contents are loaded as the prompt.

Mode resolution priority: agent.mode > defaults.mode > delegate_mode compat ("delegate" if true) > "code"

Supervisor

Optional supervisor configuration:

"supervisor": {
  "prompt": "Custom supervisor prompt",
  "model": "sonnet"
}
FieldTypeRequiredDefault
promptStringNoBuilt-in merge-focused supervisor prompt
modelStringNodefaults.model

Defaults

Project-wide defaults applied when agent-level values are not specified:

"defaults": {
  "model": "sonnet",
  "provider": "default",
  "session_timeout": null,
  "commit_interval": 300,
  "max_consecutive_errors": 5,
  "max_total_errors": 20,
  "mode": "code",
  "liveness": { ... }
}
FieldTypeDefaultDescription
modelString"sonnet"Default model for agents and supervisor
providerString"default"Default provider name
session_timeoutu64nullSession timeout in seconds (none = no timeout)
commit_intervalu64300Auto-commit interval in seconds
max_consecutive_errorsu325Consecutive errors before agent stops
max_total_errorsu3220Total errors before agent stops
modeStringnullDefault agent mode
livenessLivenessConfigSee belowLiveness monitoring settings

Liveness Configuration

Controls idle detection, nudging, and stall monitoring:

"liveness": {
  "enabled": true,
  "idle_nudge_after_secs": 120,
  "idle_nudge_interval_secs": 300,
  "max_nudges": 3,
  "idle_warn_after_secs": 600,
  "stall_timeout_secs": 900,
  "auto_interrupt_stalled": false
}
FieldTypeDefaultDescription
enabledbooltrueEnable/disable liveness monitoring
idle_nudge_after_secsu64?120Seconds idle before first nudge
idle_nudge_interval_secsu64?300Seconds between subsequent nudges
max_nudgesu323Maximum nudge messages per idle episode
idle_warn_after_secsu64?600Seconds idle before warning hook fires
stall_timeout_secsu64?900Seconds without heartbeat before stall detection
auto_interrupt_stalledboolfalseAuto-interrupt stalled agents

Permissions

Project-level permission rules (also available per-agent):

"permissions": {
  "allow": ["Bash(npm run *)"],
  "ask": ["Bash(rm *)"],
  "deny": ["Bash(curl *)"],
  "default_mode": "ask"
}

See Permissions for full details.

Resolution Cascade

When the configuration is loaded, raw values are resolved into fully-defaulted types:

Agent model    = agent.model    ?? defaults.model    ?? "sonnet"
Agent provider = agent.provider ?? defaults.provider ?? "default"
Agent mode     = agent.mode     ?? defaults.mode     ?? (delegate_mode ? "delegate" : "code")

The ResolvedConfig struct has no Option fields — every value is filled in.

Validation Rules

The config loader validates:

  1. Version — Must be 1 or 2 (no higher)
  2. At least one agent — The agents array cannot be empty
  3. Agent names — Must match [a-z][a-z0-9-]* and be unique
  4. Provider references — All agent.provider and defaults.provider values must reference a defined provider
  5. Provider types — Cannot be empty strings
  6. WASM tool names — Must match [a-z][a-z0-9_-]*, be unique, have non-empty paths
  7. WASM capabilities — Must be one of: Logging, WorkspaceRead, HttpRequest, ToolInvoke, SecretCheck

Worktrees

Swarm uses git worktrees to provide each agent with an isolated working directory and branch. This prevents file conflicts between agents working in parallel and enables clean merging of results back to the main branch.

Prerequisites

  • Git 2.20+ — Required for git worktree lock/unlock support. Checked automatically at startup.
  • Not detached HEAD — Swarm requires a branch checkout for merge-back to work.
  • Clean working tree — Either commit/stash changes first, or use --stash to auto-stash.

Path and Branch Conventions

ItemPatternExample
Swarm directory<repo>/.swarm//home/user/project/.swarm/
Worktree path<repo>/.swarm/worktrees/<name>.swarm/worktrees/backend
Agent branchswarm/<session_id>/<name>swarm/20250115-a3f2/backend
Supervisor branchswarm/<session_id>/supervisorswarm/20250115-a3f2/supervisor
Beads branchswarm/<session_id>/beadsswarm/20250115-a3f2/beads

The .swarm/ directory is automatically added to .git/info/exclude so it doesn't appear in git status.

Worktree Creation

During session start (Step 7), the orchestrator creates worktrees for each agent and the supervisor:

# For each agent:
git worktree add .swarm/worktrees/<name> -b swarm/<session_id>/<name> <base_commit>
git worktree lock .swarm/worktrees/<name>

# For the supervisor:
git worktree add .swarm/worktrees/supervisor -b swarm/<session_id>/supervisor <base_commit>
git worktree lock .swarm/worktrees/supervisor

# Shared beads branch:
git branch swarm/<session_id>/beads <base_commit>

Each worktree starts from the same base commit (HEAD at session start), ensuring all agents begin with identical codebases.

Worktree Locking

Worktrees are locked immediately after creation using git worktree lock. This prevents git worktree prune from accidentally removing them during the session. Worktrees are unlocked during cleanup.

Worktree Cleanup

When a session stops, worktrees are cleaned up in order:

  1. Auto-commit dirty — If the worktree has uncommitted changes, stage and commit them:

    git -C .swarm/worktrees/<name> add -A
    git -C .swarm/worktrees/<name> commit -m "swarm: auto-commit on stop"
    
  2. Unlockgit worktree unlock .swarm/worktrees/<name>

  3. Removegit worktree remove .swarm/worktrees/<name>

  4. Prunegit worktree prune to clean stale references

  5. Delete branches — Remove all swarm/<session_id>/* branches

Merge Operations

After cleanup, agent branches are merged based on the stop mode:

Merge (--merge, default)

Non-fast-forward merge in config order (agents first, then supervisor):

git merge --no-ff swarm/<session_id>/backend -m "Merge agent: backend"
git merge --no-ff swarm/<session_id>/frontend -m "Merge agent: frontend"
git merge --no-ff swarm/<session_id>/supervisor -m "Merge supervisor"

Squash (--squash)

Squash-merge each agent branch into a single commit:

git merge --squash swarm/<session_id>/backend
git commit -m "Squash agent: backend"

Discard (--discard)

Delete branches without merging — all agent work is discarded.

Recovery

Swarm handles crash recovery for stale sessions:

Stale Session Detection

A session is stale when its pid (from session.json) no longer corresponds to a running process. This is detected via libc::kill(pid, 0).

Recovery Flow

When a stale session is detected (during swarm start or swarm stop):

  1. Auto-commit any dirty worktrees
  2. Remove all session worktrees (unlock + remove)
  3. Prune stale worktree references
  4. Delete all swarm/<session_id>/* branches
  5. Remove session.json and lockfile

Clean Command

swarm clean provides manual cleanup:

swarm clean          # Interactive — asks for confirmation
swarm clean --force  # Removes artifacts without confirmation

This handles cases where automatic recovery isn't sufficient (e.g., corrupted worktree state).

Permissions

The permission system controls which tools agents can use and under what conditions. It uses a layered rule evaluation model with project-level and agent-level overrides.

Permission Rules

A permission rule is a string in the format "ToolName(specifier)":

Bash(npm run *)     — Allow any npm run command
Read(./.env)        — Match reading .env file
WebFetch(domain:*.example.com) — Match fetching from example.com subdomains
Bash(rm -rf *)      — Match rm -rf commands

Rule Format

ComponentDescription
Tool nameCase-insensitive tool identifier (e.g., Bash, Read, Write)
SpecifierOptional pattern inside parentheses — glob/prefix matching for commands and paths

Special specifier prefixes:

  • domain: — For WebFetch, matches against the URL's host using glob patterns
  • No prefix — Matches against the tool's primary input (command for Bash, path for Read/Write)

If no specifier is provided (e.g., just "Bash"), the rule matches all invocations of that tool.

PermissionSet

A PermissionSet contains three ordered lists of rules:

{
  "allow": ["Bash(npm run *)", "Read(*)"],
  "ask": ["Bash(rm *)"],
  "deny": ["Bash(curl *)"]
}
ListEffect
allowTool call is permitted without asking the user
askTool call requires user approval
denyTool call is blocked outright

Permission Modes

The PermissionMode enum defines 5 evaluation modes that change the default behavior:

ModeDescriptionDefault Decision
DefaultStandard mode — explicit rules apply, unmatched calls require askingAsk
AcceptEditsAutomatically allow file edits (Write, Edit, NotebookEdit)Ask (except edits)
PlanRead-only mode — denies all write/execute tools (Bash, Write, Edit, NotebookEdit)Deny for writes
DontAskNever prompt the user — unmatched calls are allowedAllow
BypassPermissionsAll tool calls are allowed regardless of rulesAllow

Evaluation Order

When a tool call is evaluated, the PermissionEvaluator follows this 6-step process:

Step 1: Mode Short-Circuit

  • BypassPermissionsAllow immediately
  • Plan mode and tool is bash, write, edit, or notebookeditDeny immediately

Step 2: Agent-Specific Overrides

If the agent has its own permissions block in the config, evaluate those rules first:

  • Check deny rules → Deny if matched
  • Check ask rules → Ask if matched
  • Check allow rules → Allow if matched

Step 3: Global Deny Rules

Check project-level deny rules → Deny if matched

Step 4: Global Ask Rules

Check project-level ask rules → Ask if matched

Step 5: Global Allow Rules

Check project-level allow rules → Allow if matched

Step 6: Mode Default

If no rule matched, apply the mode's default decision:

  • DefaultAsk
  • AcceptEditsAllow for edit tools, Ask for others
  • DontAskAllow
  • BypassPermissionsAllow

Permission Decision

The evaluation returns one of three decisions:

DecisionBehavior
AllowTool call proceeds
AskTool call requires user approval (via TUI or hook)
DenyTool call is blocked; error returned to the agent

Configuration

Permissions are configured at two levels:

Project Level

{
  "permissions": {
    "allow": ["Bash(npm run *)", "Bash(cargo test *)"],
    "ask": ["Bash(git push *)"],
    "deny": ["Bash(rm -rf /)"],
    "default_mode": "default"
  }
}

Agent Level

{
  "agents": [
    {
      "name": "reviewer",
      "prompt": "...",
      "permissions": {
        "allow": ["Read(*)"],
        "deny": ["Write(*)", "Edit(*)", "Bash(*)"]
      }
    }
  ]
}

Agent-level rules take precedence over project-level rules (evaluated first in the evaluation order).

  • Configuration — How permissions are configured
  • Tools — The tools that permissions control
  • Hooks — Hook-based permission decisions

TUI

Swarm's terminal user interface (TUI) is the primary way to monitor and interact with a running swarm session. It provides real-time visibility into agent states, logs, events, and session metadata.

Design Philosophy

The TUI is a first-class component, not an afterthought (ADR-007). It's built with ratatui (a Rust TUI framework) on top of crossterm for terminal handling, rendering at approximately 30 FPS (33ms frame interval).

For environments where a TUI isn't suitable (CI, remote servers, testing), the --no-tui flag runs swarm in headless mode, logging to stdout instead.

Components

TuiApp

The TuiApp struct holds all mutable TUI state:

FieldTypeDescription
agentsVec<AgentEntry>Ordered list of agents (sorted by name)
selectedusizeIndex of the currently selected agent
log_viewerLogViewerLog file viewer for the selected agent
event_viewerEventViewerReal-time streaming event viewer
inputStringCurrent contents of the command input bar
session_idStringDisplayed in the status bar
quit_requestedboolSet to true when user requests quit
context_infoHashMap<String, ContextInfo>Per-agent context window usage
show_task_listboolTask list overlay visibility (Ctrl+T)
tasksVec<Task>Snapshot of tasks from the task store
show_workflow_panelboolWorkflow panel visibility (Ctrl+W)
show_iteration_panelboolIteration panel visibility (Ctrl+I)

AgentEntry

Each agent in the TUI is represented as an AgentEntry:

FieldTypeDescription
nameStringAgent name
stateAgentStateCurrent state from the state machine
log_pathPathBufPath to the agent's log file
livenessOption<AgentLiveness>Liveness monitoring data (idle time, stall, nudges)

Agent Panel

The left panel shows a list of all agents with their current state:

┌─ Agents ──────────────────┐
│ ● backend      Running    │
│ ● frontend     Running    │
│ ○ reviewer     CoolingDown│
│ ■ supervisor   Stopped    │
└───────────────────────────┘

Each agent shows:

  • A state indicator icon
  • The agent name
  • The current state label
  • Liveness suffix when available (running duration, idle time, stall warnings)

Log/Event Viewer

The right panel displays output for the selected agent:

  • Event viewer — Real-time streaming events from the agent's backend session
  • Log viewer — Fallback log file viewer when event streaming isn't available

The viewer auto-scrolls to follow new output.

Input Bar

The bottom of the screen shows a command input bar where you can type messages to send to agents. The input bar is always visible.

Overlays

Toggle-able overlay panels:

OverlayToggleContent
Task listCtrl+TTasks from the beads task store
Workflow progressCtrl+WActive/recent workflow runs and stages
Iteration progressCtrl+IIteration loop runs and status

Context Window

Per-agent context window usage is tracked and displayed:

FieldDescription
used_tokensTokens consumed in the current session
max_tokensMaximum context window size
usage_percentPercentage of context used
KeyAction
Up / DownSelect previous/next agent
Ctrl+TToggle task list overlay
Ctrl+WToggle workflow panel
Ctrl+IToggle iteration panel
q / Ctrl+CQuit (triggers graceful shutdown)

State Refresh

The TUI refreshes agent states each frame by calling AgentRegistry::states(). Liveness data is refreshed from a watch channel provided by the liveness monitor. This keeps the display current without expensive polling.

Headless Mode

When --no-tui is specified:

  • No terminal UI is rendered
  • Agent state changes are logged to stdout via tracing
  • The orchestrator still runs all the same subsystems (router, periodic tasks, etc.)
  • Shutdown is triggered by SIGTERM only (no interactive quit)

Writing Agents

How to define, configure, and tune agents in swarm.

Overview

Agents are the core unit of work in swarm. Each agent runs in its own git worktree with its own backend session, system prompt, and permissions. You define agents in ~/.swarm/settings.json under the agents array of your project configuration.

Step 1: Create the Settings File

If you haven't already, initialize the configuration:

swarm init --path /path/to/your-project

This creates ~/.swarm/settings.json with a skeleton project config. Open it in your editor.

Step 2: Define a Provider

Providers specify how swarm connects to a model API. At minimum, you need one provider:

{
  "version": 2,
  "/home/user/my-project": {
    "providers": {
      "default": {
        "type": "anthropic",
        "api_key_env": "ANTHROPIC_API_KEY"
      }
    }
  }
}

Provider fields:

FieldTypeRequiredDefaultDescription
typeStringYesProvider type ("anthropic")
api_key_envStringNo"ANTHROPIC_API_KEY"Environment variable holding the API key
base_urlStringNonullCustom API base URL
max_retriesu32NonullMax retries for transient errors
timeoutu64NonullRequest timeout in seconds

You can define multiple providers and reference them by name in agent or defaults config.

Step 3: Set Project Defaults

The defaults section provides fallback values for all agents:

{
  "defaults": {
    "model": "sonnet",
    "provider": "default",
    "commit_interval": 300,
    "max_consecutive_errors": 5,
    "max_total_errors": 20,
    "liveness": {
      "enabled": true,
      "idle_nudge_after_secs": 120,
      "idle_nudge_interval_secs": 300,
      "max_nudges": 3
    }
  }
}

Key defaults:

FieldDefaultDescription
model"sonnet"Default model identifier
provider"default"Default provider name
session_timeoutnullPer-session timeout (seconds)
commit_interval300Auto-commit interval (seconds)
max_consecutive_errors5Errors before an agent stops
max_total_errors20Lifetime errors before an agent stops

Step 4: Define Agents

Each agent needs a name and a prompt. All other fields are optional:

{
  "agents": [
    {
      "name": "backend",
      "prompt": "You are a senior backend engineer. Focus on API design, database schemas, and server-side logic. Write tests for all new endpoints.",
      "model": "sonnet"
    },
    {
      "name": "frontend",
      "prompt": "@prompts/frontend.md",
      "model": "sonnet"
    },
    {
      "name": "reviewer",
      "prompt": "You are a code reviewer. Read changes from other agents and provide feedback via messages.",
      "model": "sonnet"
    }
  ]
}

Agent fields:

FieldTypeRequiredDefaultDescription
nameStringYesUnique identifier ([a-z][a-z0-9-]*)
promptStringYesSystem prompt text or @path/to/file
modelStringNodefaults.modelModel identifier
providerStringNodefaults.providerProvider name reference
permissionsPermissionsConfigNonullAgent-level permission overrides
modeStringNoSee cascadeAgent execution mode

Prompt Loading

Prompts can be specified in two ways:

  • Inline: Write the prompt directly as a string value
  • File reference: Use @path/to/file to load from a file relative to the project root

File references are useful for long prompts and allow version-controlling prompts alongside your code:

prompts/
  backend.md
  frontend.md
  reviewer.md

Name Rules

Agent names must:

  • Start with a lowercase letter
  • Contain only lowercase letters, digits, and hyphens
  • Be unique within the project

Invalid names cause a ValidationError at startup.

Step 5: Configure Permissions

Permissions control what tools each agent can use. They're defined at the project level and optionally overridden per agent.

Project-Level Permissions

{
  "permissions": {
    "allow": ["Read(*)", "Glob(*)", "Grep(*)"],
    "deny": ["Bash(rm -rf *)"],
    "default_mode": "default"
  }
}

Agent-Level Overrides

{
  "name": "frontend",
  "prompt": "...",
  "permissions": {
    "allow": ["Bash(npm *)"],
    "deny": ["Bash(rm *)"]
  }
}

Agent permissions are evaluated after project permissions. See Permissions for the full evaluation order.

Rule Format

Rules follow the pattern Tool(specifier):

ExampleMeaning
Read(*)Allow reading any file
Bash(npm *)Allow any npm command
Bash(rm *)Match any rm command
Edit(src/*.rs)Match editing Rust files in src/

Step 6: Choose Agent Modes

The mode field controls how an agent interacts with tools and the operator. Available modes:

ModeDescription
defaultStandard mode — tools require permission checks
accept-editsAuto-accept file edits without confirmation
planPlanning mode — agent proposes changes but doesn't execute
dont-askSkip all permission prompts (auto-allow)
bypass-permissionsBypass the permission system entirely

Set a default mode for all agents:

{
  "defaults": {
    "mode": "dont-ask"
  }
}

Or override per agent:

{
  "name": "reviewer",
  "prompt": "...",
  "mode": "plan"
}

Step 7: Add a Supervisor (Optional)

The supervisor generates the final merge commit message when stopping with --merge or --squash:

{
  "supervisor": {
    "prompt": "Summarize all agent changes into a concise merge commit message.",
    "model": "sonnet"
  }
}

If omitted, swarm uses a built-in merge prompt.

Complete Example

A full three-agent configuration:

{
  "version": 2,
  "/home/user/my-project": {
    "providers": {
      "default": {
        "type": "anthropic",
        "api_key_env": "ANTHROPIC_API_KEY"
      }
    },
    "defaults": {
      "model": "sonnet",
      "commit_interval": 300,
      "max_consecutive_errors": 5,
      "max_total_errors": 20,
      "mode": "dont-ask",
      "liveness": {
        "enabled": true,
        "idle_nudge_after_secs": 120,
        "idle_nudge_interval_secs": 300,
        "max_nudges": 3,
        "stall_timeout_secs": 900
      }
    },
    "agents": [
      {
        "name": "backend",
        "prompt": "@prompts/backend.md",
        "model": "sonnet"
      },
      {
        "name": "frontend",
        "prompt": "@prompts/frontend.md",
        "model": "sonnet",
        "permissions": {
          "allow": ["Bash(npm *)", "Bash(npx *)"],
          "deny": ["Bash(rm -rf *)"]
        }
      },
      {
        "name": "reviewer",
        "prompt": "@prompts/reviewer.md",
        "model": "sonnet",
        "mode": "plan"
      }
    ],
    "supervisor": {
      "prompt": "@prompts/supervisor.md"
    },
    "permissions": {
      "allow": ["Read(*)", "Glob(*)", "Grep(*)"],
      "default_mode": "default"
    }
  }
}

Troubleshooting

"config validation failed: agent names must be unique"

Two agents have the same name. Each agent must have a distinct name.

"config validation failed: agents list cannot be empty"

You must define at least one agent in the agents array.

"config file not found"

Run swarm init to create the settings file, or verify the path in ~/.swarm/settings.json matches your project's canonicalized absolute path.

Agent keeps entering CoolingDown state

Check the agent's logs with swarm logs <name>. Common causes:

  • Invalid API key (check the api_key_env environment variable)
  • Model name not recognized by the provider
  • Prompt too large for the model context window

The backoff formula is min(2000 * 2^(n-1), 60000) ms, where n is consecutive errors. After max_consecutive_errors (default 5), the agent stops.

Custom Skills

Creating custom skills to extend agent capabilities.

Overview

Skills are reusable prompt fragments that agents can invoke during their sessions. Each skill is a Markdown file with optional YAML frontmatter that controls how the skill behaves — who can invoke it, what tools it can use, and how arguments are substituted.

Skill File Format

A skill file has two parts: frontmatter (optional) and body (required).

---
name: my-skill
description: A brief description of what this skill does
user-invocable: true
argument-hint: "<required-arg> [optional-arg]"
---

You are now executing the "my-skill" skill.

The user asked: $ARGUMENTS

Perform the requested action and report results.

Step 1: Choose a Location

Skills are discovered from three paths, in priority order:

PriorityPathStyle
1 (highest)<project>/.claude/skills/<name>/SKILL.mdDirectory
2<project>/.skills/<name>.mdFlat file
3 (lowest)~/.claude/skills/<name>/SKILL.mdGlobal directory

Directory style is preferred — each skill gets its own directory, which can contain supporting files:

.claude/skills/
  commit/
    SKILL.md
  review/
    SKILL.md
    checklist.md

Flat file style is available for backward compatibility:

.skills/
  commit.md
  review.md

If a skill with the same name exists at multiple paths, the highest-priority path wins. First match is used; duplicates at lower priorities are ignored.

Step 2: Write the Frontmatter

The YAML frontmatter block is delimited by --- lines at the top of the file. All fields are optional:

FieldTypeDefaultDescription
nameStringFilename/directory nameSkill identifier (overrides the inferred name)
descriptionStringNoneOne-line description shown in skill discovery
user-invocableboolfalseIf true, appears in the / slash-command menu
disable-model-invocationboolfalseIf true, only the user can invoke this skill (not the model)
allowed-toolsString[]NoneRestrict which tools the skill can access
modelStringNoneOverride the model for this skill's execution
contextStringNoneSet to "fork" for subagent execution
agentStringNoneSubagent type when context: fork
argument-hintStringNoneUsage hint displayed to the user
hooksHooksConfigNoneLifecycle hooks specific to this skill
unsafeboolNoneReserved for future WASM sandbox flag

Invocation Control

SettingEffect
user-invocable: trueAppears in / menu; user and model can invoke
user-invocable: falseHidden from menu; model-only unless model invocation also disabled
disable-model-invocation: trueOnly the user can invoke via /skill-name

Example Frontmatter

---
name: commit
description: Create a conventional commit
user-invocable: true
disable-model-invocation: false
allowed-tools:
  - Bash
  - Read
  - Grep
argument-hint: "[commit message]"
model: claude-sonnet-4-5-20250929
---

Step 3: Write the Body

The body is everything after the closing ---. It's injected into the agent's prompt when the skill is invoked. Use argument placeholders to make the skill dynamic:

Argument Substitution

PlaceholderReplaced With
$ARGUMENTSThe full argument string passed by the invoker
$ARGUMENTS[0] through $ARGUMENTS[9]Positional argument (space-split)
$0 through $9Shorthand for $ARGUMENTS[N]

Substitution is single-pass (left-to-right) — output from one replacement is never re-processed. Missing positional arguments are replaced with an empty string. Only indices 0–9 are supported.

Example Body

---
name: test-file
description: Run tests for a specific file
user-invocable: true
argument-hint: "<file-path>"
---

Run the tests for the file at `$0`.

Steps:
1. Read the file to understand what it does
2. Find the corresponding test file
3. Run the tests with `cargo test`
4. If tests fail, analyze the failures and suggest fixes
5. Report results

Full context from user: $ARGUMENTS

When invoked as /test-file src/main.rs, the placeholders expand to:

  • $0src/main.rs
  • $ARGUMENTSsrc/main.rs

When invoked as /test-file src/main.rs --verbose, the placeholders expand to:

  • $0src/main.rs
  • $1--verbose
  • $ARGUMENTSsrc/main.rs --verbose

Step 4: Add Skill-Level Hooks (Optional)

Skills can define their own hooks that fire during skill execution:

---
name: deploy
description: Deploy to staging
user-invocable: true
hooks:
  pre_tool_use:
    - matcher: Bash
      hooks:
        - type: command
          command: ./scripts/validate-deploy.sh
          timeout: 10
---

Hook configuration follows the same format as project-level hooks. See Hooks for details.

Step 5: Test Your Skill

Verify Discovery

Skills are discovered automatically when agents build their prompts. To verify your skill is found, check that:

  1. The file exists at one of the three resolution paths
  2. The filename or name field matches what you expect
  3. If user-invocable: true, it should appear in the agent's available slash commands

Test Resolution

The resolution order is deterministic:

  1. Swarm checks <project>/.claude/skills/<name>/SKILL.md
  2. If not found, checks <project>/.skills/<name>.md
  3. If not found, checks ~/.claude/skills/<name>/SKILL.md
  4. If not found at any path, the skill is not available

Validate the Name

Skill names must contain only [a-zA-Z0-9_:-]. Names with other characters (including /, .., spaces) are rejected to prevent path traversal.

Examples

Simple Commit Skill

---
name: commit
description: Create a git commit with conventional format
user-invocable: true
allowed-tools:
  - Bash
  - Read
  - Grep
argument-hint: "[commit message]"
---

Create a git commit following the conventional commits format.

If no message was provided, analyze the staged changes and generate an appropriate message.

User input: $ARGUMENTS

Steps:
1. Run `git diff --cached` to see staged changes
2. Generate or use the provided commit message
3. Ensure the message follows conventional commits (feat:, fix:, chore:, etc.)
4. Create the commit

Code Review Skill

---
name: review
description: Review code changes in the current branch
user-invocable: true
disable-model-invocation: true
argument-hint: "[focus area]"
---

Review the code changes in the current branch compared to main.

Focus area: $ARGUMENTS

Steps:
1. Run `git diff main...HEAD` to see all changes
2. For each changed file, analyze:
   - Correctness: Are there bugs or logic errors?
   - Security: Any vulnerabilities introduced?
   - Performance: Any obvious performance issues?
   - Style: Does the code follow project conventions?
3. Provide a summary of findings

Subagent Skill

---
name: research
description: Research a topic using a subagent
user-invocable: true
context: fork
agent: researcher
argument-hint: "<topic>"
---

Research the following topic thoroughly: $ARGUMENTS

Provide a detailed summary with references.
  • Skills Concept — How the skill system works internally
  • Hooks — Lifecycle hooks for skills and agents
  • Permissions — How allowed-tools interacts with permissions

MCP Servers

Connecting and configuring Model Context Protocol (MCP) servers to extend agent capabilities.

Overview

MCP integration allows swarm to connect to external tool servers using the Model Context Protocol. Tools from MCP servers are registered in the tool registry with prefixed names and appear as regular tools to all agents.

How It Works

  1. Startup — The orchestrator reads mcpServers from your configuration and connects to each server
  2. Discovery — Each server's tools are listed and registered with prefixed names: mcp__<server>__<tool>
  3. Execution — When an agent calls an MCP tool, the McpProxyTool routes the call to the correct server
  4. Shutdown — All MCP servers are gracefully shut down when the session ends

MCP servers are started once per orchestrator session and shared across all agents.

Step 1: Add MCP Servers to Configuration

Add the mcpServers section to your project config in ~/.swarm/settings.json:

{
  "version": 2,
  "/home/user/my-project": {
    "agents": [ ... ],
    "mcpServers": {
      "server-name": {
        "transport": { ... },
        "env": { ... }
      }
    }
  }
}

Each server has a unique name (the key) and a configuration object with transport and optional env fields.

Step 2: Choose a Transport

Swarm supports three MCP transport types:

Stdio Transport

Spawns a child process and communicates via JSON-RPC over stdin/stdout. Best for locally installed tools.

{
  "mcpServers": {
    "filesystem": {
      "transport": {
        "type": "stdio",
        "command": "npx",
        "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
      }
    }
  }
}
FieldTypeRequiredDescription
type"stdio"YesTransport discriminator
commandStringYesCommand to execute
argsString[]NoCommand arguments

HTTP Transport

Connects over Streamable HTTP. Best for remote servers with request-response semantics.

{
  "mcpServers": {
    "github": {
      "transport": {
        "type": "http",
        "url": "https://api.example.com/mcp/",
        "headers": {
          "Authorization": "Bearer ${GITHUB_TOKEN}"
        }
      }
    }
  }
}
FieldTypeRequiredDescription
type"http"YesTransport discriminator
urlStringYesServer URL
headersMap<String, String>NoHTTP headers

SSE Transport

Connects over Server-Sent Events (legacy EventSource). Best for servers that push updates.

{
  "mcpServers": {
    "events": {
      "transport": {
        "type": "sse",
        "url": "https://sse.example.com/events",
        "headers": {
          "X-Token": "abc123"
        }
      }
    }
  }
}
FieldTypeRequiredDescription
type"sse"YesTransport discriminator
urlStringYesSSE endpoint URL
headersMap<String, String>NoHTTP headers

Step 3: Configure Environment Variables (Optional)

MCP servers launched via stdio transport can receive environment variables:

{
  "mcpServers": {
    "database": {
      "transport": {
        "type": "stdio",
        "command": "npx",
        "args": ["-y", "@bytebase/dbhub"]
      },
      "env": {
        "DB_URL": "sqlite:///path/to/db",
        "DB_READ_ONLY": "true"
      }
    }
  }
}

The env map is injected into the child process environment when the server is spawned.

Step 4: Use MCP Tools

Once configured, MCP tools appear in the agent's tool registry with prefixed names:

mcp__<server_name>__<tool_name>

For example, a server named github that exposes a search_repos tool creates:

mcp__github__search_repos

Agents can call these tools like any other tool. The McpProxyTool handles routing the call to the correct server, serializing the input as JSON-RPC, and returning the result.

Tool Discovery

Each MCP server advertises its tools via the tools/list JSON-RPC method. Swarm calls this during startup and registers each tool with:

  • Name: mcp__<server>__<tool> (prefixed to avoid conflicts with built-in tools)
  • Description: As provided by the server
  • Input schema: As provided by the server

Permission Integration

MCP tools respect the permission system. You can allow or deny specific MCP tools:

{
  "permissions": {
    "allow": ["mcp__filesystem__read_file(*)"],
    "deny": ["mcp__filesystem__write_file(*)"]
  }
}

JSON-RPC Protocol

Swarm communicates with MCP servers using JSON-RPC 2.0. The key message exchanges:

Initialize

// Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-03-26",
    "capabilities": {},
    "clientInfo": { "name": "swarm", "version": "0.1.0" }
  }
}

// Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-03-26",
    "capabilities": { "tools": {} },
    "serverInfo": { "name": "server-name", "version": "1.0" }
  }
}

List Tools

// Request
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }

// Response
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "search_repos",
        "description": "Search GitHub repositories",
        "inputSchema": { "type": "object", "properties": { ... } }
      }
    ]
  }
}

Call Tool

// Request
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "search_repos",
    "arguments": { "query": "swarm" }
  }
}

// Response
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      { "type": "text", "text": "Found 3 repositories..." }
    ]
  }
}

Complete Example

A configuration with two MCP servers:

{
  "version": 2,
  "/home/user/my-project": {
    "agents": [
      {
        "name": "backend",
        "prompt": "You are a backend engineer with access to the database and filesystem tools."
      }
    ],
    "mcpServers": {
      "filesystem": {
        "transport": {
          "type": "stdio",
          "command": "npx",
          "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
        }
      },
      "database": {
        "transport": {
          "type": "stdio",
          "command": "npx",
          "args": ["-y", "@bytebase/dbhub"]
        },
        "env": {
          "DB_URL": "sqlite:///home/user/my-project/data.db"
        }
      }
    }
  }
}

This gives the backend agent access to:

  • mcp__filesystem__read_file
  • mcp__filesystem__write_file
  • mcp__filesystem__list_directory
  • mcp__database__query
  • mcp__database__list_tables
  • (and any other tools the servers expose)

Error Handling

  • Failed server: If an MCP server fails to start, swarm logs a warning and continues. Other servers and agents are not affected.
  • Failed tool listing: If tools/list fails for a server, its tools are not registered but the server remains connected.
  • Tool call error: Transport and JSON-RPC errors are returned as tool execution errors to the agent.
  • Transport retry: Transport errors are retried once before being returned as failures.

Troubleshooting

"Failed to connect to MCP server"

  • Verify the command is installed and in your PATH (for stdio transport)
  • Check that the URL is reachable (for http/sse transport)
  • Review the server's stderr output in swarm logs

Tools not appearing

  • Ensure the server's tools/list response is valid
  • Check that the server name doesn't contain characters that would break the prefix format
  • Verify the server completes initialization within the timeout

Permission denied for MCP tools

MCP tools use the prefixed name format for permission rules. Use mcp__<server>__<tool>(*) in your permission rules.

WASM Tools

Building and using WebAssembly sandboxed tools in swarm.

Overview

Swarm supports running tools as WebAssembly components in a sandboxed environment. WASM tools execute with strict resource limits and explicit capability grants, providing isolation from the host system. This feature is gated behind the wasm-sandbox Cargo feature flag.

Prerequisites

  • Rust toolchain with wasm32-wasip2 target installed
  • Swarm built with the wasm-sandbox feature flag
  • A WASM component file (.wasm) compiled from a WIT interface

Build Swarm with WASM Support

cargo build --release --features wasm-sandbox

Install the WASM Target

rustup target add wasm32-wasip2

Architecture

WASM tools run inside a Wasmtime runtime with the Component Model enabled. Each tool invocation gets a fresh Store with its own HostState, ensuring complete isolation between calls.

Agent → ToolRegistry → WasmTool → Wasmtime Store → WASM Component
                                       ↑
                                   HostState (capabilities, limits, secrets)

Key components:

ComponentRole
WasmRuntimeEngine lifecycle, component compilation, caching
WasmToolTool trait implementation, bridges registry to WASM
HostStatePer-invocation state with capability gating
CredentialInjectorSecret injection into headers, response redaction
ResourceLimitsMemory, CPU, I/O quotas

Step 1: Define the WIT Interface

WASM tools implement the tool world defined in wit/tool.wit. The interface exposes these host functions:

Host FunctionRequired CapabilityDescription
log(level, message)LoggingWrite a log entry
read-workspace-file(path)WorkspaceReadRead a file from the workspace
make-http-request(request)HttpRequestMake an HTTP request
invoke-tool(name, params)ToolInvokeCall another tool in the registry
secret-exists(name)SecretCheckCheck if a secret exists

The tool must export:

ExportSignatureDescription
name()() -> StringTool name
description()() -> StringTool description
schema()() -> StringJSON Schema for input
execute(input)(String) -> Result<String, String>Execute with JSON input, return JSON result

Step 2: Implement the Tool

Write your tool in Rust (or any language targeting WASM Component Model):

#![allow(unused)]
fn main() {
// In your WASM component crate
wit_bindgen::generate!({
    world: "tool",
    path: "../wit/tool.wit",
});

struct MyTool;

impl Guest for MyTool {
    fn name() -> String {
        "my-tool".to_string()
    }

    fn description() -> String {
        "A custom sandboxed tool".to_string()
    }

    fn schema() -> String {
        serde_json::json!({
            "type": "object",
            "properties": {
                "query": { "type": "string", "description": "Search query" }
            },
            "required": ["query"]
        }).to_string()
    }

    fn execute(input: String) -> Result<String, String> {
        let params: serde_json::Value = serde_json::from_str(&input)
            .map_err(|e| e.to_string())?;

        let query = params["query"].as_str().ok_or("missing query")?;

        // Use host functions (if capabilities are granted)
        host::log(LogLevel::Info, &format!("Searching for: {query}"));

        Ok(serde_json::json!({
            "result": format!("Results for: {query}")
        }).to_string())
    }
}

export!(MyTool);
}

Step 3: Compile to WASM

Build the component:

cargo build --target wasm32-wasip2 --release

The output .wasm file will be at target/wasm32-wasip2/release/my_tool.wasm.

Step 4: Configure the Tool

Add the tool to wasm_tools in your project configuration:

{
  "wasm_tools": [
    {
      "name": "my-tool",
      "path": "./tools/my_tool.wasm",
      "capabilities": ["Logging", "HttpRequest"],
      "limits": {
        "max_memory_bytes": 67108864,
        "fuel_limit": 1000000000,
        "execution_timeout_secs": 30
      },
      "secrets": ["API_TOKEN"],
      "workspace_prefixes": ["src/", "data/"],
      "endpoint_allowlist": ["api.example.com"]
    }
  ]
}

Configuration Fields

FieldTypeRequiredDefaultDescription
nameStringYesTool name ([a-z][a-z0-9_-]*)
pathStringYesPath to .wasm component file
capabilitiesString[]No[]Granted capabilities
limitsWasmLimitsConfigNoDefaultsResource limit overrides
secretsString[]No[]Secret names the tool may query
workspace_prefixesString[]No[]Readable workspace path prefixes
endpoint_allowlistString[]No[]Allowed HTTP endpoint patterns
tool_aliasesMap<String, String>No{}Aliases for invoke-tool calls

Capabilities

Each capability unlocks a specific host function. Tools without a capability cannot call the corresponding function — attempts result in a CapabilityDenied error.

CapabilityHost FunctionDescription
Logginglog()Write log entries (up to max_log_entries)
WorkspaceReadread-workspace-file()Read files within allowed prefixes
HttpRequestmake-http-request()Make HTTP requests to allowed endpoints
ToolInvokeinvoke-tool()Call other registered tools
SecretChecksecret-exists()Check if a named secret exists

Grant only the capabilities your tool actually needs.

Resource Limits

Every WASM tool runs with resource limits to prevent runaway computation or excessive I/O:

LimitDefaultMaximumDescription
max_memory_bytes64 MiB512 MiBMaximum memory allocation
fuel_limit1,000,000,000Computation fuel (instruction budget)
execution_timeout_secs30300Wall-clock timeout
max_log_entries1,000Max log entries per invocation
max_http_requests50Max HTTP requests per invocation
max_tool_invocations20Max tool calls per invocation
max_file_read_bytes10 MiBCumulative file read limit

Exceeding a limit produces the corresponding error:

  • Memory: wasmtime memory allocation trap
  • Fuel: FuelExhausted error
  • Timeout: TimeoutExceeded error (enforced via epoch interruption at 100ms intervals)
  • Counters: RateLimitExceeded error

Security Model

File Access

The read-workspace-file function enforces multiple security layers:

  1. Path traversal prevention — Rejects paths containing .. components
  2. Canonicalization — Resolves symlinks and normalizes paths
  3. Boundary check — Canonical path must be within the workspace root
  4. Prefix matching — If workspace_prefixes is configured, the file must match at least one prefix
  5. Symlink escape detection — Detects symlinks pointing outside the workspace
  6. Size validation — Checks cumulative bytes read against max_file_read_bytes

HTTP Requests

The make-http-request function enforces:

  1. Endpoint allowlist — URL host must match a glob pattern in endpoint_allowlist. Empty allowlist denies all requests
  2. HTTPS enforcement — Non-HTTPS URLs are rejected (except localhost, 127.*, ::1)
  3. Credential injection$SECRET_NAME patterns in headers are replaced with actual values from the environment
  4. Response scanning — Leaked secret values in response bodies are redacted as [REDACTED:<name>]

Secrets

Secrets are sourced from environment variables, restricted to the names listed in secrets:

{
  "secrets": ["API_TOKEN", "DB_PASSWORD"]
}
  • secret-exists("API_TOKEN") → checks if API_TOKEN is set in the environment
  • HTTP header "Authorization": "Bearer $API_TOKEN" → injected with the actual value
  • Secret values appearing in HTTP response bodies are automatically redacted

Error Types

WASM tools can produce these errors:

ErrorCause
CompilationFailed.wasm file failed to compile
InstantiationFailedMissing imports or initialization failure
ExecutionTrappedGuest code triggered a trap
FuelExhaustedInstruction budget exceeded
TimeoutExceededEpoch deadline exceeded
HostFunctionErrorHost function returned an error
CapabilityDeniedTool called a function without the required capability
RateLimitExceededResource quota (HTTP requests, tool invocations, etc.) exceeded

Complete Example

A weather lookup tool with HTTP access and logging:

Configuration:

{
  "wasm_tools": [
    {
      "name": "weather",
      "path": "./tools/weather.wasm",
      "capabilities": ["Logging", "HttpRequest"],
      "limits": {
        "max_http_requests": 5,
        "execution_timeout_secs": 15
      },
      "secrets": ["WEATHER_API_KEY"],
      "endpoint_allowlist": ["api.openweathermap.org"]
    }
  ]
}

Tool implementation (pseudocode):

#![allow(unused)]
fn main() {
fn execute(input: String) -> Result<String, String> {
    let params: Value = serde_json::from_str(&input)?;
    let city = params["city"].as_str().ok_or("missing city")?;

    host::log(LogLevel::Info, &format!("Looking up weather for {city}"));

    let response = host::make_http_request(HttpRequest {
        method: HttpMethod::Get,
        url: format!(
            "https://api.openweathermap.org/data/2.5/weather?q={city}&appid=$WEATHER_API_KEY"
        ),
        headers: vec![],
        body: None,
    })?;

    Ok(response.body)
}
}

The $WEATHER_API_KEY in the URL header is automatically injected, and any leaked key values in the response are redacted.

Troubleshooting

"CompilationFailed"

  • Verify the .wasm file was built with wasm32-wasip2 target
  • Ensure the Component Model is enabled in your build
  • Check that all WIT imports are satisfied

"CapabilityDenied"

  • Add the required capability to the capabilities array in your config
  • Check spelling: capabilities are case-sensitive (Logging, not logging)

"RateLimitExceeded"

  • Increase the relevant limit in the limits config
  • Optimize your tool to make fewer requests/invocations

Tool not appearing in registry

  • Ensure swarm was built with --features wasm-sandbox
  • Verify the path is correct and the file exists
  • Check that name follows the [a-z][a-z0-9_-]* pattern

Hooks

Lifecycle hooks and event handling in swarm.

Overview

Hooks are shell commands that execute in response to lifecycle events during a swarm session. They can observe events (logging, notifications), modify behavior (blocking tool calls), or perform side effects (running scripts). Hooks are configured in ~/.swarm/settings.json under the hooks section.

How Hooks Work

  1. An event fires (e.g., PreToolUse before a tool executes)
  2. Swarm finds matching hooks by checking each MatcherGroup for the event
  3. Matching hooks receive a HookContext JSON object on stdin
  4. Sync hooks block until they return a HookDecision; async hooks fire and forget
  5. If any sync hook returns Deny or Block, the action is prevented

Step 1: Add Hooks to Configuration

Hooks are organized by event type in the hooks section:

{
  "hooks": {
    "pre_tool_use": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/validate-bash.sh",
            "timeout": 10
          }
        ]
      }
    ],
    "session_start": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/on-session-start.sh",
            "async": true
          }
        ]
      }
    ]
  }
}

Event Types

Swarm supports 13 hook event types:

Event TypeConfig KeyWhen It Fires
SessionStartsession_startA new agent session begins
PreToolUsepre_tool_useBefore a tool executes
PostToolUsepost_tool_useAfter a tool executes successfully
PostToolUseFailurepost_tool_use_failureAfter a tool execution fails
StopstopSession stop is initiated
SessionEndsession_endAn agent session ends
SubagentStartsubagent_startA subagent is spawned
SubagentStopsubagent_stopA subagent stops
TeammateIdleteammate_idleAn agent has been idle past the nudge threshold
TeammateIdleWarningteammate_idle_warningAn agent has been idle past the warning threshold
TaskCompletedtask_completedAn agent completes a task
NotificationnotificationA notification is generated
StallDetectedstall_detectedAgent stall detected (no heartbeat past threshold)

Step 2: Define Matcher Groups

Each event type has an array of MatcherGroup entries. A matcher group specifies:

  • matcher (optional) — A regex pattern matched against the tool name or event source. If omitted, matches all events of that type.
  • hooks — An array of hook handlers to execute when matched.
{
  "pre_tool_use": [
    {
      "matcher": "Bash",
      "hooks": [ ... ]
    },
    {
      "matcher": "Edit|Write",
      "hooks": [ ... ]
    },
    {
      "hooks": [ ... ]
    }
  ]
}

The first group matches only Bash tool calls, the second matches Edit or Write, and the third matches all tool calls (no matcher = match all).

Step 3: Write Hook Handlers

Each hook handler is a command type with these fields:

FieldTypeRequiredDefaultDescription
type"command"YesHandler type (only command is supported)
commandStringYesShell command to execute (run via sh -c)
timeoutu64No30Timeout in seconds
asyncboolNofalseIf true, fire-and-forget (no decision collected)
status_messageStringNonullDisplay message while hook runs

Example:

{
  "type": "command",
  "command": "./scripts/lint-check.sh",
  "timeout": 15,
  "status_message": "Running lint check..."
}

Step 4: Write Hook Scripts

Hook scripts receive a HookContext JSON object on stdin and optionally output a HookDecision JSON object on stdout.

HookContext (stdin)

The context object sent to every hook:

{
  "session_id": "20250115-a3f2",
  "cwd": "/home/user/my-project/.swarm/worktrees/backend",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  },
  "tool_response": null
}
FieldTypePresentDescription
session_idStringAlwaysActive session ID
cwdStringAlwaysAgent working directory
hook_event_nameStringAlwaysEvent name (e.g., "PreToolUse")
tool_nameStringTool events onlyName of the tool
tool_inputJSONTool events onlyTool input payload
tool_responseJSONPostToolUse onlyTool response payload

Fields that don't apply to the event type are omitted from the JSON (not set to null).

Specialized Event Inputs

Some events provide additional context beyond the standard HookContext:

TeammateIdleWarning:

{
  "event": "TeammateIdleWarning",
  "agent_name": "backend",
  "session_id": "20250115-a3f2",
  "idle_duration_secs": 300,
  "nudge_count": 2,
  "last_state": "Running"
}

StallDetected:

{
  "event": "StallDetected",
  "agent_name": "backend",
  "session_id": "20250115-a3f2",
  "stall_duration_secs": 900,
  "last_heartbeat_event": "PostToolUse"
}

HookDecision (stdout)

Sync hooks can output a decision JSON to control the action:

{ "decision": "allow" }
{ "decision": "deny", "reason": "Command not allowed by policy" }
{ "decision": "block", "reason": "Security violation detected" }
DecisionEffect
allowPermits the action to proceed
denyPrevents the action (with reason)
blockPrevents the action (with reason)

Both deny and block prevent the action. The reason field is optional and logged for diagnostics.

Exit Code Behavior

Exit CodeBehavior
0Parse stdout for decision JSON. No output = implicit allow
2Blocking error — treated as deny with stderr as reason
OtherNon-blocking error — logged but action proceeds

Sync vs Async Hooks

ModeBlocks ActionReturns DecisionUse Case
Sync (async: false)YesYesValidation, policy enforcement
Async (async: true)NoNoLogging, notifications, side effects

Sync hooks within a single event run in parallel. If any sync hook returns deny or block, the action is prevented and all denial reasons are aggregated.

Async hooks are spawned as background tasks. They don't contribute decisions and their success or failure doesn't affect the action.

Example Scripts

Pre-Tool-Use Validator

Block dangerous bash commands:

#!/bin/bash
# scripts/validate-bash.sh
# Blocks rm -rf and other dangerous commands

CONTEXT=$(cat)
COMMAND=$(echo "$CONTEXT" | jq -r '.tool_input.command // empty')

if echo "$COMMAND" | grep -qE 'rm\s+-rf\s+/'; then
  echo '{"decision": "deny", "reason": "Refusing to run rm -rf on root paths"}'
  exit 0
fi

echo '{"decision": "allow"}'

Session Start Logger

Log session starts to a file:

#!/bin/bash
# scripts/on-session-start.sh
CONTEXT=$(cat)
SESSION_ID=$(echo "$CONTEXT" | jq -r '.session_id')
echo "[$(date)] Session started: $SESSION_ID" >> /tmp/swarm-sessions.log

Post-Tool-Use Notifier

Send a notification after tool failures:

#!/bin/bash
# scripts/notify-failure.sh
CONTEXT=$(cat)
TOOL=$(echo "$CONTEXT" | jq -r '.tool_name')
echo "[$(date)] Tool failed: $TOOL" >> /tmp/swarm-failures.log

Stall Detector

Auto-restart stalled agents:

#!/bin/bash
# scripts/handle-stall.sh
CONTEXT=$(cat)
AGENT=$(echo "$CONTEXT" | jq -r '.agent_name')
DURATION=$(echo "$CONTEXT" | jq -r '.stall_duration_secs')
echo "[$(date)] Agent $AGENT stalled for ${DURATION}s" >> /tmp/swarm-stalls.log

Complete Configuration Example

{
  "hooks": {
    "session_start": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/on-session-start.sh",
            "async": true
          }
        ]
      }
    ],
    "pre_tool_use": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/validate-bash.sh",
            "timeout": 10,
            "status_message": "Validating command..."
          }
        ]
      }
    ],
    "post_tool_use_failure": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/notify-failure.sh",
            "async": true
          }
        ]
      }
    ],
    "stall_detected": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/handle-stall.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

Troubleshooting

Hook not firing

  • Verify the event type key in config matches the event (e.g., pre_tool_use, not preToolUse)
  • Check the matcher regex matches the tool name or event source
  • Ensure the script is executable (chmod +x scripts/my-hook.sh)

Hook timing out

  • The default timeout is 30 seconds. Increase it with the timeout field.
  • On timeout, the child process is killed and the hook is treated as a non-blocking error.

Decision not being applied

  • Async hooks cannot return decisions — set async: false for policy enforcement
  • Ensure exit code is 0 and stdout contains valid JSON
  • Exit code 2 is a special blocking error; other non-zero codes are non-blocking

Multiple hooks conflicting

Sync hooks for the same event run in parallel. If any returns deny or block, the action is prevented. Reasons from all denying hooks are aggregated.

Beads Workflow

Using beads for issue tracking and workflow management in swarm.

Overview

Beads (bd) is an external CLI tool used by swarm for issue tracking and task management. It provides a git-native issue tracking system where issues are stored as files in a dedicated branch, making them accessible to both humans and AI agents without requiring an external service.

Swarm integrates with beads at two levels:

  1. Prompt injection — Available tasks are included in each agent's system prompt
  2. Agent workflow — Agents use bd commands to claim and close tasks during their sessions

Prerequisites

Beads must be installed before using swarm:

# Install beads
cargo install bd

# Verify installation
bd --version

Swarm checks for the bd binary at startup. If it's not found, swarm exits with an error and installation instructions.

How It Works

Task Discovery

During prompt building, swarm runs bd ready --json to fetch available (unclaimed) tasks. The results are injected into section 13 of the prompt pipeline:

## Available Tasks (from `bd ready`)

- #42: Implement health check endpoint [priority: high]
- #43: Add input validation to API routes [priority: medium]
- #51: Write integration tests for auth flow [priority: low]

This gives each agent awareness of what work is available without any manual coordination.

Agent Workflow

Agents interact with beads using these commands:

CommandDescription
bd readyList available (unclaimed) tasks
bd show <id>View full task details
bd update <id> --status in_progressClaim a task
bd close <id>Mark a task as complete
bd syncSynchronize with the shared beads branch

A typical agent workflow:

  1. Agent sees available tasks in its prompt
  2. Claims a task with bd update <id> --status in_progress
  3. Works on the task (writing code, running tests)
  4. Closes the task with bd close <id>
  5. Next prompt cycle picks up remaining tasks

Shared Beads Branch

All agents in a session share a beads branch at:

swarm/<session-id>/beads

This branch uses optimistic concurrency — agents read and write independently, and conflicts are resolved on sync. This works well because:

  • Issue files are small and rarely edited by multiple agents simultaneously
  • bd sync handles merge conflicts automatically
  • The worst case is a brief delay before an agent sees another's claim

Getting Started

Step 1: Initialize Beads

In your project directory:

bd onboard

This sets up the beads branch and initial configuration.

Step 2: Create Tasks

Create tasks for your project:

bd create "Implement health check endpoint" --priority high
bd create "Add input validation to API routes" --priority medium
bd create "Write integration tests for auth flow" --priority low

Or create tasks from spec documents:

bd create "Implement feature X per specs/042-feature-x.md"

Step 3: Start a Swarm Session

swarm start

Agents will automatically see available tasks in their prompts and can claim work.

Step 4: Monitor Progress

Check task status:

bd ready        # See unclaimed tasks
bd list         # See all tasks
bd show <id>    # View task details

Spec-Driven Workflow

Swarm's recommended workflow (from CLAUDE.md) is spec-driven:

  1. Plan — Design the feature or fix
  2. Write specs — Create spec documents in specs/ capturing contracts
  3. File beads tasks — Create tasks referencing the specs
  4. Run swarm — Agents pick up tasks and implement them
  5. Review — Verify agent work against the specs

This approach ensures agents have clear, well-defined tasks with acceptance criteria, rather than vague instructions.

Example Spec-to-Task Flow

  1. Write a spec:
<!-- specs/042-health-check.md -->
# Health Check Endpoint

## Contract
- GET /health returns 200 with JSON body `{"status": "ok"}`
- Response includes `uptime_seconds` field
- Endpoint requires no authentication
  1. File tasks from the spec:
bd create "Implement GET /health endpoint per specs/042-health-check.md"
bd create "Add health check integration test per specs/042-health-check.md"
  1. Start swarm — agents see the tasks, read the referenced spec, and implement accordingly.

Session Completion

When ending a work session, the recommended workflow is:

  1. File issues for remaining work — Create beads tasks for anything unfinished
  2. Update issue status — Close finished tasks, update in-progress items
  3. Sync and push:
bd sync
git push

This ensures the next session (or the next developer) has clear context on what's done and what remains.

Commands Reference

CommandDescription
bd onboardInitialize beads for a repository
bd create <title>Create a new task
bd readyList unclaimed tasks
bd ready --jsonList unclaimed tasks as JSON (used by prompt builder)
bd listList all tasks
bd show <id>Show task details
bd update <id> --status <status>Update task status
bd close <id>Close a completed task
bd syncSync with shared beads branch

Troubleshooting

"bd: command not found"

Install beads with cargo install bd and ensure it's in your PATH.

Tasks not appearing in agent prompts

  • Verify bd ready returns tasks when run manually
  • Check that bd ready --json produces valid JSON output
  • Ensure beads is initialized in the project (bd onboard)

Agents claiming the same task

This can happen due to the optimistic concurrency model. The impact is usually minor — both agents do the same work, and the duplicate can be discarded at review time. To reduce this:

  • Use specific task descriptions that make it clear which agent should handle them
  • Structure tasks around agent specializations (backend tasks for the backend agent, etc.)

Sync conflicts

bd sync handles most conflicts automatically. If a conflict can't be auto-resolved:

  1. Check bd list for the current state
  2. Manually resolve any remaining conflicts
  3. Run bd sync again

CLI Reference

Complete reference for all swarm CLI commands and flags.

Usage

swarm <COMMAND>

Commands

swarm init

Initialize swarm configuration for a project. Creates ~/.swarm/settings.json with a template entry if it doesn't exist.

FlagTypeDefaultDescription
--pathPathBuf.Path to the project directory

swarm start

Start a swarm session. Executes the 13-step orchestrator start flow.

FlagTypeDefaultDescription
--stashboolfalseAuto-stash uncommitted changes before starting
--initboolfalseInitialize git repo if not already initialized
--no-tuiboolfalseRun in headless mode (no terminal UI)

swarm stop

Stop a running swarm session. Sends SIGTERM to the orchestrator and waits for graceful shutdown.

FlagTypeDefaultDescription
--mergeboolfalseMerge agent branches into base (default if no flag set)
--squashboolfalseSquash-merge agent branches
--discardboolfalseDiscard agent branches without merging

The --merge, --squash, and --discard flags are mutually exclusive. If none are specified, merge is the default behavior.

swarm status

Show session status including agent states, uptime, and beads summary.

FlagTypeDefaultDescription
--jsonboolfalseOutput in JSON format

swarm logs

View agent logs.

ArgumentTypeRequiredDescription
agentStringYesAgent name
FlagTypeDefaultDescription
--followboolfalseTail the log (like tail -f)
--sessionu32nullView archived session log instead of current

swarm send

Send a message to a specific agent.

ArgumentTypeRequiredDescription
agentStringYesRecipient agent name
messageStringYesMessage body
FlagTypeDefaultDescription
--urgentboolfalseMark as urgent (triggers interrupt)

swarm broadcast

Send a message to all agents.

ArgumentTypeRequiredDescription
messageStringYesMessage body
FlagTypeDefaultDescription
--urgentboolfalseMark as urgent

swarm config

Show the resolved configuration for the current project.

FlagTypeDefaultDescription
--jsonboolfalseOutput raw JSON instead of formatted

swarm clean

Clean stale swarm artifacts (worktrees, branches, session files).

FlagTypeDefaultDescription
--forceboolfalseRemove artifacts without confirmation

swarm workflow

Manage workflow definitions and runs. This is a subcommand group:

swarm workflow list

List available workflow definitions.

swarm workflow run

Start a workflow run.

ArgumentTypeRequiredDescription
nameStringYesWorkflow name
FlagTypeDefaultDescription
--inputKEY=VALUE[]Input key=value pairs (repeatable)

swarm workflow status

Show status of running/completed workflows.

ArgumentTypeRequiredDescription
run_idStringNoSpecific run ID (omit to list all)
FlagTypeDefaultDescription
--jsonboolfalseOutput in JSON format

swarm workflow approve

Approve a human-approval gate.

ArgumentTypeRequiredDescription
run_idStringYesWorkflow run ID
stageStringYesStage name

swarm workflow reject

Reject a gate with optional feedback.

ArgumentTypeRequiredDescription
run_idStringYesWorkflow run ID
stageStringYesStage name
FlagTypeDefaultDescription
--feedbackStringnullFeedback message

swarm workflow retry

Manually retry a failed stage.

ArgumentTypeRequiredDescription
run_idStringYesWorkflow run ID
stageStringYesStage name

swarm workflow cancel

Cancel a running workflow.

ArgumentTypeRequiredDescription
run_idStringYesWorkflow run ID

swarm workflow show

Show detailed run info including outputs.

ArgumentTypeRequiredDescription
run_idStringYesWorkflow run ID

swarm workflow validate

Validate a workflow definition.

ArgumentTypeRequiredDescription
nameStringYesWorkflow name

swarm iterate

Run an iteration loop from a configuration file.

FlagTypeDefaultDescription
--configStringnullIteration config name (looked up in .swarm/iterations/<name>.yml)
--config-filePathBufnullPath to iteration config YAML file
--resumeStringnullResume a previous iteration run
--max-iterationsu32nullOverride max_iterations from config
--dry-runboolfalseValidate config and show plan without executing
--no-tuiboolfalseRun without TUI
--jsonboolfalseOutput progress as JSON lines

The --config and --config-file flags are mutually exclusive.

Config Schema

Complete reference for the ~/.swarm/settings.json configuration file.

Top-Level Structure

{
  "version": 2,
  "<project_path>": { ... }
}
FieldTypeRequiredDescription
versionu64YesSchema version (must be 1 or 2)
<project_path>ProjectConfigYesProject config keyed by absolute, canonicalized path

Multiple projects can be configured in the same file. Each is keyed by its absolute path.

ProjectConfig

FieldTypeRequiredDefaultDescription
agentsAgentConfig[]YesAt least one agent definition
supervisorSupervisorConfigNonullSupervisor configuration
defaultsDefaultsConfigNonullProject-wide defaults
providersMap<String, ProviderConfig>NoImplicit "default"Named provider definitions
permissionsPermissionsConfigNonullProject-level permission rules
hooksHooksConfigNonullHook event handlers
mcpServersMap<String, McpServerConfig>NonullMCP server connections
wasm_toolsWasmToolConfig[]NonullWASM sandboxed tools
sub_agent_defaultsSubAgentLimitsNonullDefault sub-agent spawning limits

AgentConfig

FieldTypeRequiredDefaultDescription
nameStringYesUnique name ([a-z][a-z0-9-]*)
promptStringYesSystem prompt text or @path/to/file
modelStringNodefaults.model / "sonnet"Model identifier
providerStringNodefaults.provider / "default"Provider name reference
permissionsPermissionsConfigNonullAgent-level permission overrides
delegate_modeboolNofalseLegacy delegate mode flag
modeStringNoSee cascadeAgent execution mode

SupervisorConfig

FieldTypeRequiredDefaultDescription
promptStringNoBuilt-in merge promptCustom supervisor prompt or @path
modelStringNodefaults.modelModel identifier

DefaultsConfig

FieldTypeRequiredDefaultDescription
modelStringNo"sonnet"Default model
providerStringNo"default"Default provider reference
session_timeoutu64NonullSession timeout (seconds)
commit_intervalu64No300Auto-commit interval (seconds)
max_consecutive_errorsu32No5Consecutive error limit
max_total_errorsu32No20Total error limit
modeStringNonullDefault agent mode
livenessLivenessConfigNoSee belowLiveness monitoring

ProviderConfig

FieldTypeRequiredDefaultDescription
typeStringYesProvider type ("anthropic")
api_key_envStringNo"ANTHROPIC_API_KEY"API key env var name
base_urlStringNonullCustom API base URL
max_retriesu32NonullMax retries for transient errors
timeoutu64NonullRequest timeout (seconds)

PermissionsConfig

FieldTypeRequiredDefaultDescription
allowString[]No[]Allow rules (e.g., "Bash(npm run *)")
askString[]No[]Ask rules
denyString[]No[]Deny rules
default_modeStringNonullDefault permission mode

LivenessConfig

FieldTypeRequiredDefaultDescription
enabledboolNotrueEnable liveness monitoring
idle_nudge_after_secsu64No120Seconds before first nudge
idle_nudge_interval_secsu64No300Seconds between nudges
max_nudgesu32No3Max nudges per idle episode
idle_warn_after_secsu64No600Seconds before warning hook
stall_timeout_secsu64No900Seconds before stall detection
auto_interrupt_stalledboolNofalseAuto-interrupt on stall

McpServerConfig

FieldTypeRequiredDefaultDescription
transportMcpTransportYesTransport configuration
envMap<String, String>No{}Environment variables

McpTransport (tagged union)

Stdio:

FieldTypeRequiredDescription
type"stdio"YesTransport discriminator
commandStringYesCommand to execute
argsString[]NoCommand arguments

HTTP:

FieldTypeRequiredDescription
type"http"YesTransport discriminator
urlStringYesServer URL
headersMap<String, String>NoHTTP headers

SSE:

FieldTypeRequiredDescription
type"sse"YesTransport discriminator
urlStringYesSSE endpoint URL
headersMap<String, String>NoHTTP headers

WasmToolConfig

FieldTypeRequiredDefaultDescription
nameStringYesTool name ([a-z][a-z0-9_-]*)
pathStringYesPath to .wasm component file
capabilitiesString[]No[]Granted capabilities
limitsWasmLimitsConfigNonullResource limit overrides
secretsString[]No[]Secret names the tool may query
workspace_prefixesString[]No[]Readable workspace paths
endpoint_allowlistString[]No[]Allowed HTTP endpoints
tool_aliasesMap<String, String>No{}Tool name aliases

WasmLimitsConfig

FieldTypeRequiredDefaultDescription
max_memory_bytesusizeNoRuntime defaultMax memory allocation
fuel_limitu64NoRuntime defaultComputation fuel limit
execution_timeout_secsu64NoRuntime defaultExecution timeout
max_log_entriesusizeNoRuntime defaultMax log entries
max_http_requestsusizeNoRuntime defaultMax HTTP requests
max_tool_invocationsusizeNoRuntime defaultMax tool invocations
max_file_read_bytesusizeNoRuntime defaultMax file read size

Valid Capabilities

  • Logging — Write log entries
  • WorkspaceRead — Read files from workspace
  • HttpRequest — Make HTTP requests
  • ToolInvoke — Invoke other tools
  • SecretCheck — Check secret existence

Complete Example

{
  "version": 2,
  "/home/user/my-project": {
    "providers": {
      "default": {
        "type": "anthropic",
        "api_key_env": "ANTHROPIC_API_KEY"
      }
    },
    "defaults": {
      "model": "sonnet",
      "commit_interval": 300,
      "max_consecutive_errors": 5,
      "liveness": {
        "enabled": true,
        "idle_nudge_after_secs": 120
      }
    },
    "agents": [
      {
        "name": "backend",
        "prompt": "@prompts/backend.md",
        "model": "sonnet"
      },
      {
        "name": "frontend",
        "prompt": "You are a frontend engineer.",
        "permissions": {
          "allow": ["Bash(npm *)"],
          "deny": ["Bash(rm *)"]
        }
      }
    ],
    "permissions": {
      "allow": ["Read(*)", "Glob(*)"],
      "default_mode": "default"
    },
    "mcpServers": {
      "filesystem": {
        "transport": {
          "type": "stdio",
          "command": "npx",
          "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
        }
      }
    }
  }
}

Environment Variables

Reference for all environment variables used by swarm — both user-provided and runtime-injected.

User-Provided Variables

These variables must be set in your environment before running swarm:

VariableRequiredDescription
ANTHROPIC_API_KEYYes (default)API key for the Anthropic provider. The env var name can be overridden per-provider via api_key_env in config.
BRAVE_SEARCH_API_KEYNoAPI key for the Brave Search tool (used by web_search)
HOMEYesUser home directory. Used to locate ~/.swarm/settings.json and global skills at ~/.claude/skills/.

Runtime-Injected Variables

These variables are set by swarm and injected into each agent's backend session environment:

VariableValueDescription
SWARM_AGENT_IDAgent name (e.g., "backend")Identifies the current agent within the swarm session
SWARM_SESSION_IDSession ID (e.g., "20250115-a3f2")Unique identifier for the current session
SWARM_DB_PATHPath (e.g., .swarm/messages.db)Absolute path to the SQLite mailbox database
SWARM_AGENTSComma-separated names (e.g., "backend,frontend,reviewer")List of all agent names in the session

These variables allow agent tools and hooks to interact with the swarm infrastructure programmatically.

Logging

VariableDefaultDescription
RUST_LOGinfoControls log verbosity via the tracing crate. Set to debug for detailed output, warn for quieter operation. Supports per-module filtering (e.g., swarm=debug,rusqlite=warn).

Provider-Specific Variables

The API key environment variable name is configurable per provider:

{
  "providers": {
    "default": {
      "type": "anthropic",
      "api_key_env": "ANTHROPIC_API_KEY"
    },
    "custom": {
      "type": "anthropic",
      "api_key_env": "CUSTOM_ANTHROPIC_KEY"
    }
  }
}

MCP Server Variables

MCP servers configured with the env field receive those environment variables when launched:

{
  "mcpServers": {
    "myserver": {
      "transport": { "type": "stdio", "command": "my-mcp-server" },
      "env": {
        "MY_SERVER_TOKEN": "secret123"
      }
    }
  }
}

Error Types

Swarm defines 6 error enums using thiserror, each covering a distinct subsystem. All error types implement std::error::Error and can be converted to anyhow::Error at the orchestrator level.

ConfigError

Configuration errors — fatal at startup.

VariantDisplay MessageWhen It Occurs
MissingFile { path }config file not found at {path}~/.swarm/settings.json doesn't exist
ParseError { reason }failed to parse config: {reason}JSON syntax error, file read failure, or path canonicalization failure
ValidationError { reason }config validation failed: {reason}Empty agents list, invalid names, duplicate names, bad provider references, invalid WASM capabilities
VersionMismatch { found, expected }config version {found} is not supported (expected {expected})Config version is newer than supported

GitError

Git prerequisite and worktree operation errors — fatal at startup or during worktree ops.

VariantDisplay MessageWhen It Occurs
NotARepo { path }{path} is not a git repositoryProject directory is not a git repo
DirtyTreeworking tree has uncommitted changes; commit or stash firstUncommitted changes without --stash flag
WorktreeOp { reason }git worktree operation failed: {reason}Worktree create/remove/lock/unlock/merge failure, HEAD is detached
VersionTooOld { found, required }git version {found} is too old; swarm requires git >= {required}Git version below 2.20

MailboxError

Mailbox and SQLite errors — DB-open is fatal, transient locks are retried.

VariantDisplay MessageWhen It Occurs
DbOpen { reason }failed to open mailbox database: {reason}SQLite connection failure, schema creation failure, invalid enum values
DbLocked { reason }mailbox database is locked: {reason}WAL lock contention, query failure
UnknownAgent { name }unknown agent: {name}Message sent to non-existent agent
SelfSendagent cannot send a message to itselfAgent tries to send a message to itself
NotFound { id }message not found: {id}Reply to a non-existent message ID

AgentError

Agent lifecycle errors — spawn failures may be retried with backoff.

VariantDisplay MessageWhen It Occurs
SpawnFailed { name, reason }failed to spawn agent {name}: {reason}Backend session failed to start
Timeout { name, seconds }agent {name} timed out after {seconds}sAgent session exceeded timeout

WorkflowError

Workflow definition errors — parse, validation, or inheritance failures.

VariantDisplay MessageWhen It Occurs
NotFound { name }workflow not found: {name}Referenced workflow doesn't exist
ParseError { reason }failed to parse workflow: {reason}YAML/JSON parse failure
ValidationError { reason }workflow validation failed: {reason}General validation failure
CyclicDependency { cycle }circular dependency detected in workflow stages: {cycle}Stage dependency graph has a cycle
CyclicInheritance { chain }circular inheritance detected: {chain}Workflow extends chain has a cycle
DuplicateStage { name }duplicate stage name: {name}Two stages have the same name
InvalidStageName { name }invalid stage name '{name}': must match [a-z][a-z0-9_-]*Stage name contains invalid characters
UnknownDependency { stage, dep }unknown dependency '{dep}' in stage '{stage}'Stage depends on non-existent stage
ParentNotFound { child, parent }parent workflow '{parent}' not found for extends in '{child}'extends references missing workflow
CyclicWorkflowRef { chain }circular workflow_ref detected: {chain}Nested workflow references form a cycle
MissingWorkflowRef { stage }workflow_ref stage '{stage}' is missing required 'workflow' fieldStage type is workflow_ref but no workflow specified
IoError { reason }I/O error: {reason}File system errors during workflow loading

SessionError

Session lifecycle errors — stale sessions prompt user action.

VariantDisplay MessageWhen It Occurs
ActiveSession { id, pid }session {id} is already active (pid {pid})Attempting to create session when one already exists
StaleLockfile { path }stale lockfile found at {path}; another session may be runningLockfile exists but process state is ambiguous
RecoveryNeededprevious session did not shut down cleanly; recovery is neededSession artifacts remain from crashed session
IoError { reason }session I/O error: {reason}File read/write/delete failures

Error Handling Strategy

Swarm follows ADR-009:

  • Library code uses thiserror enums for precise error types
  • Binary/orchestrator uses anyhow for ergonomic error propagation
  • All thiserror types automatically convert to anyhow::Error via the ? operator

State Transitions

Complete transition table for the agent state machine defined in agent::state.

Transition Table

From StateEventTo StateSide Effect
InitializingWorktreeReadyBuildingPromptNone
BuildingPromptPromptReady(prompt)SpawningStorePrompt(prompt)
SpawningSessionStarted(seq)Running { session_seq: seq }None (resets consecutive_errors to 0)
SpawningSessionExited(Error)CoolingDown or StoppedNone or LogFatal (if threshold reached)
SpawningSessionExited(Timeout)CoolingDown or StoppedNone or LogFatal (if threshold reached)
RunningSessionExited(Success)SessionCompleteNone (resets consecutive_errors to 0)
RunningSessionExited(Error)CoolingDown or StoppedNone or LogFatal (if threshold reached)
RunningSessionExited(Timeout)CoolingDown or StoppedNone or LogFatal (if threshold reached)
RunningUrgentMessageInterrupting { session_seq }CancelSession
InterruptingSessionExited(*)BuildingPromptNone (no error increment)
InterruptingGraceExceededBuildingPromptForceStopSession
SessionCompleteWorktreeReadyBuildingPromptIncrementSession (bumps session_seq)
CoolingDownBackoffElapsedBuildingPromptNone

Global Transitions

These events are valid from any state:

EventTo StateSide Effect
OperatorStopStoppedCancelSession if currently Running or Interrupting; None otherwise
FatalError(msg)StoppedLogFatal(msg)

Error Counters

The state machine maintains two counters that affect transitions:

CounterIncremented OnReset OnFatal Threshold
consecutive_errorsAny SessionExited(Error|Timeout) from Running or SpawningSessionStarted or SessionExited(Success)Default: 5 (max_consecutive_errors)
total_errorsAny SessionExited(Error|Timeout) from Running or SpawningNeverDefault: 20 (max_total_errors)

When a counter reaches its threshold, the state transitions to Stopped with LogFatal instead of CoolingDown.

Note: SessionExited(*) from Interrupting does not increment error counters — interrupts are intentional, not errors.

Backoff Formula

The CoolingDown duration uses exponential backoff:

duration_ms = min(2000 * 2^(n-1), 60000)

Where n = consecutive_errors after increment.

nDuration
12,000 ms (2s)
24,000 ms (4s)
38,000 ms (8s)
416,000 ms (16s)
532,000 ms (32s)
6+60,000 ms (60s, cap)

State Diagram

Initializing ──WorktreeReady──► BuildingPrompt ◄──BackoffElapsed── CoolingDown
                                     │                                  ▲
                                     │ PromptReady                      │
                                     ▼                                  │
                                  Spawning ──Error/Timeout──────────────┤
                                     │                                  │
                                     │ SessionStarted                   │
                                     ▼                                  │
                                  Running ──Error/Timeout───────────────┘
                                   │   │
                      Success      │   │ UrgentMessage
                         │         │   ▼
                         ▼         │ Interrupting ──SessionExited──► BuildingPrompt
                  SessionComplete  │              ──GraceExceeded──► BuildingPrompt
                         │         │
                         │ WorktreeReady
                         └──────────────────────────────────────────► BuildingPrompt

                  OperatorStop or FatalError from ANY state ──────► Stopped

ExitOutcome

The ExitOutcome enum describes how a backend session ended:

VariantDescription
SuccessSession completed normally
Error(String)Session failed with an error message
TimeoutSession exceeded its timeout

Message Schema

Complete SQLite database schema for the swarm messaging system.

Database Location

.swarm/messages.db

PRAGMAs

Set on every connection open:

PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMAValuePurpose
journal_modeWALWrite-Ahead Logging for concurrent readers
busy_timeout5000Wait up to 5 seconds on lock contention before returning SQLITE_BUSY

Table: messages

CREATE TABLE IF NOT EXISTS messages (
    id           INTEGER PRIMARY KEY AUTOINCREMENT,
    thread_id    INTEGER REFERENCES messages(id),
    reply_to     INTEGER REFERENCES messages(id),
    sender       TEXT    NOT NULL,
    recipient    TEXT    NOT NULL,
    msg_type     TEXT    NOT NULL DEFAULT 'message',
    urgency      TEXT    NOT NULL DEFAULT 'normal',
    body         TEXT    NOT NULL,
    created_at   INTEGER NOT NULL,
    delivered_at INTEGER
);

Column Details

ColumnTypeNullableDefaultDescription
idINTEGERNoAuto-incrementPrimary key
thread_idINTEGERYesNULLReferences root message in thread
reply_toINTEGERYesNULLReferences the message being replied to
senderTEXTNoSending agent name or "operator"
recipientTEXTNoReceiving agent name
msg_typeTEXTNo'message'One of: message, task, status, nudge
urgencyTEXTNo'normal'One of: normal, urgent
bodyTEXTNoMessage content
created_atINTEGERNoEpoch nanoseconds (creation time)
delivered_atINTEGERYesNULLEpoch nanoseconds (delivery time); NULL = pending

Enum Values

msg_type:

  • message — General inter-agent communication
  • task — Task assignment or delegation
  • status — Status updates
  • nudge — Liveness nudge

urgency:

  • normal — Standard delivery on next prompt build
  • urgent — Triggers router interrupt

Indexes

CREATE INDEX IF NOT EXISTS idx_messages_recipient_pending
    ON messages (recipient, delivered_at)
    WHERE delivered_at IS NULL;

CREATE INDEX IF NOT EXISTS idx_messages_urgency_pending
    ON messages (urgency, delivered_at)
    WHERE delivered_at IS NULL AND urgency = 'urgent';

CREATE INDEX IF NOT EXISTS idx_messages_thread
    ON messages (thread_id) WHERE thread_id IS NOT NULL;
IndexColumnsFilterUsed By
idx_messages_recipient_pending(recipient, delivered_at)delivered_at IS NULLconsume() — fetch pending messages for an agent
idx_messages_urgency_pending(urgency, delivered_at)delivered_at IS NULL AND urgency = 'urgent'poll_urgent() — router polling for urgent messages
idx_messages_thread(thread_id)thread_id IS NOT NULLthread() — retrieve all messages in a thread

Key Queries

Send Message

INSERT INTO messages (sender, recipient, msg_type, urgency, body, created_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)

Reply to Message

INSERT INTO messages (thread_id, reply_to, sender, recipient, msg_type, urgency, body, created_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)

The thread_id is inherited from the original message's thread (or uses the original's ID as the root).

Consume Pending Messages

-- Read pending messages
SELECT * FROM messages WHERE recipient = ?1 AND delivered_at IS NULL ORDER BY created_at;

-- Mark as delivered
UPDATE messages SET delivered_at = ?1 WHERE recipient = ?2 AND delivered_at IS NULL;

Both operations happen in a single transaction for atomicity.

Poll Urgent Messages

SELECT id, recipient FROM messages
WHERE urgency = 'urgent' AND delivered_at IS NULL;

Used by the router every 100ms to find messages that need interrupt delivery.

Prune Old Messages

DELETE FROM messages
WHERE delivered_at IS NOT NULL
  AND id NOT IN (
    SELECT id FROM messages
    WHERE delivered_at IS NOT NULL
    ORDER BY delivered_at DESC
    LIMIT 1000
  );

Keeps the 1000 most recently delivered messages; runs every 300 seconds.

WAL Checkpoint

PRAGMA wal_checkpoint(TRUNCATE);

Runs every 60 seconds to reclaim WAL file space.

ADR-001: Tokio Async Runtime

Status

Accepted

Context

Swarm orchestrates multiple concurrent agent processes, polls SQLite for messages, manages timers (backoff, checkpoints), and handles OS signals. We need an async runtime to avoid blocking the main thread.

Decision

Use Tokio with full features (rt-multi-thread, macros, process, signal, time, sync, io-util, fs).

Alternatives Considered

AlternativeWhy rejected
async-stdSmaller ecosystem, less mature process/signal support
Threads only (no async)Would require manual thread pools, select-like logic is painful
smolLighter, but we need Tokio's process::Command and signal support

Consequences

  • All I/O-heavy functions are async fn.
  • rusqlite is synchronous — wrap calls in tokio::task::spawn_blocking or accept brief blocking on the runtime (acceptable since DB ops are <1ms with WAL).
  • Process spawning uses tokio::process::Command.
  • The TUI event loop must integrate with Tokio (crossterm polling + tokio::select).

Tradeoffs

  • Tokio is a large dependency but well-maintained.
  • Learning curve for contributors unfamiliar with async Rust.

ADR-002: SQLite WAL for Inter-Agent Messaging

Status

Accepted

Context

Agents need to send messages to each other. The messaging system must support:

  • Direct messages (agent-to-agent)
  • Broadcasts (one-to-all)
  • Urgency levels (normal vs urgent)
  • Delivery tracking (consumed vs pending)
  • Concurrent readers/writers (orchestrator + N agents + swarm-msg)

Decision

Use SQLite in WAL mode as the messaging transport, stored at .swarm/messages.db.

Alternatives Considered

AlternativeWhy rejected
File-based (one file per message)Race conditions, no atomicity, glob storms
Unix domain socketsRequires a broker process, more complex
Redis/NATSExternal dependency, overkill for local orchestration
Named pipes / FIFOsNo persistence, no multi-reader, fragile
Shared memoryComplex, no persistence

Key Pragmas

PRAGMA journal_mode = WAL;       -- concurrent reads during writes
PRAGMA busy_timeout = 5000;      -- wait up to 5s if locked
PRAGMA synchronous = NORMAL;     -- safe with WAL, faster than FULL

Consequences

  • Single file for all messaging state — easy to inspect, backup, clean.
  • swarm-msg binary can write directly to DB without going through orchestrator.
  • WAL allows concurrent reads (agents polling) while writer inserts.
  • Must checkpoint periodically to prevent WAL file growth.
  • Must prune old delivered messages to bound DB size.

Invariants

  • A message is inserted once and never modified except to mark delivered_at.
  • consume_messages() reads + marks delivered in a single transaction.
  • Self-send is rejected at insert time (sender == recipient).
  • Broadcast creates N-1 rows (one per recipient, excluding sender).

ADR-003: AgentBackend Trait Abstraction

Status

Accepted

Context

Currently we target Claude Code CLI (claude -p) as the agent backend. However, we want the architecture to support future backends (e.g., other LLM CLIs, HTTP APIs, local models) without rewriting the agent engine.

Decision

Define an AgentBackend trait and an AgentProcess trait. The orchestrator interacts only with these traits.

Trait Design

#![allow(unused)]
fn main() {
trait AgentBackend: Send + Sync {
    fn id(&self) -> &str;
    fn spawn_session(&self, config: &SessionConfig) -> Result<Box<dyn AgentProcess>>;
}

trait AgentProcess: Send {
    fn pid(&self) -> u32;
    async fn wait(&mut self) -> ExitOutcome;
    fn terminate(&self) -> Result<()>;  // SIGTERM
    fn kill(&self) -> Result<()>;       // SIGKILL
    fn supports_injection(&self) -> bool;
}
}

Current Implementation: ClaudeBackend

  • Spawns claude -p <prompt> --dangerously-skip-permissions --model <model> --output-format json
  • Sets working directory to agent's worktree
  • Injects SWARM_* environment variables
  • Isolates process in own process group via setsid (pre_exec)
  • Redirects stdout+stderr to agent's current.log

Alternatives Considered

AlternativeWhy rejected
Direct Claude couplingNo testing, no future extensibility
Plugin system (dylib)Over-engineered for v1
Config-driven command templateLess type-safe, harder to test

Consequences

  • Easy to write a MockBackend for integration tests.
  • Adding a new backend requires only implementing two traits.
  • supports_injection() returns false for Claude (no stdin injection); future backends may return true.

ExitOutcome

#![allow(unused)]
fn main() {
enum ExitOutcome {
    Success,
    Error { code: Option<i32>, signal: Option<i32> },
    Timeout,
}
}

ADR-004: Fresh Agent Sessions (No --resume)

Status

Accepted

Context

Claude Code CLI supports --resume to continue a previous conversation. We must decide whether agents resume or start fresh each iteration.

Decision

Use fresh sessions every time. Each agent invocation is claude -p <prompt> with no --resume flag.

Rationale

  • Prompt injection is our primary control mechanism — we assemble a new prompt each iteration with current messages, beads state, and context.
  • --resume would carry stale context and make it harder to steer agents.
  • Fresh sessions give us a clean slate with precise control over what the agent sees.
  • Session sequence number (session_seq) tracks iterations for logging.

Tradeoffs

AspectFresh sessions--resume
Context controlFullPartial (can't unsay things)
Token costHigher (re-inject system prompt)Lower
SimplicitySimpler orchestrator logicMore complex state tracking
Failure recoveryClean restartMust handle corrupt resume state

Consequences

  • Every prompt must be self-contained (all context re-injected).
  • The 5-stage prompt pipeline (system + user prompt + messages + beads + session context) runs every iteration.
  • session_seq increments per spawn, used for log archival naming.
  • No need to manage conversation IDs or resume tokens.

ADR-005: Foreground Process (Not Daemon)

Status

Accepted

Context

The orchestrator could run as a background daemon or a foreground process attached to a terminal.

Decision

Run as a foreground process. The user starts swarm start in a terminal and the TUI takes over. The process exits when the user quits or sends SIGINT/SIGTERM.

Rationale

  • Simpler lifecycle management — no PID files, no orphan daemons.
  • TUI requires a terminal anyway.
  • Users can use tmux/screen for persistence.
  • --no-tui mode still runs in foreground, just without the UI.

Alternatives Considered

AlternativeWhy rejected
Daemon with swarm attachComplex (double-fork, PID management, socket for TUI attach)
systemd serviceOver-engineered, not portable

Consequences

  • Session lockfile contains the orchestrator PID for staleness detection.
  • swarm stop from another terminal sends SIGTERM to the orchestrator PID.
  • Signal handler catches SIGINT/SIGTERM and triggers graceful shutdown.
  • swarm status checks if the PID is alive to determine session liveness.

ADR-006: Git Worktree Isolation Per Agent

Status

Accepted

Context

Multiple agents work on the same repository concurrently. They must not interfere with each other's file changes.

Decision

Each agent gets its own git worktree branching from the base commit. The supervisor also gets its own worktree for integration.

Branch Naming

  • Agent branches: swarm/<session-id>/<agent-name>
  • Supervisor branch: swarm/<session-id>/supervisor
  • Beads branch: swarm/<session-id>/beads (shared, optimistic concurrency)

Worktree Location

Worktrees are created under .swarm/worktrees/<agent-name>/.

Lifecycle

  1. swarm start: Create worktrees for all agents + supervisor from HEAD.
  2. During session: Agents commit to their own branches freely.
  3. swarm stop:
    • Auto-commit any dirty worktrees.
    • Based on stop mode (merge/squash/discard), combine branches.
    • Remove worktrees and delete branches.

Locking

Worktrees are locked (git worktree lock) during active sessions to prevent accidental removal.

Alternatives Considered

AlternativeWhy rejected
Separate clonesWastes disk space, slow
Shared working directory with stashRace conditions, impossible with concurrent agents
Docker containers per agentHeavy, slow startup

Consequences

  • Requires git >= 2.20 for reliable worktree support.
  • .swarm/ must be in .git/info/exclude to avoid tracking.
  • Recovery must handle orphaned worktrees from crashed sessions.
  • Worktree creation/removal is async (tokio::process::Command).

Stop Modes

ModeBehavior
--mergeMerge each agent branch into the original branch (default)
--squashSquash-merge each agent's work into a single commit
--discardDelete branches without merging

ADR-007: Full TUI From Day One

Status

Accepted

Context

We could build a headless-only MVP first and add a TUI later, or build the TUI as a core feature from the start.

Decision

Build the full Ratatui TUI as a core feature. swarm start launches the TUI by default; --no-tui provides headless mode.

Rationale

  • Observability is critical for a multi-agent system — users need to see what agents are doing in real time.
  • Building the TUI later would require retrofitting state observation APIs.
  • Ratatui is lightweight and well-suited for this use case.

Stack

  • Ratatui for layout/rendering
  • Crossterm as the terminal backend

Consequences

  • TUI event loop must integrate with Tokio (poll crossterm events alongside async tasks).
  • Agent states are exposed via watch::Receiver<AgentState> channels for the TUI to observe.
  • Log viewing is file-based (tail agent's current.log).
  • The TUI is the primary shutdown trigger (user presses q).

ADR-008: Beads CLI as External Dependency

Status

Accepted

Context

Swarm agents need a task/work-item system to claim, work on, and close units of work. The beads CLI (bd) provides this functionality.

Decision

Require beads CLI (bd) to be pre-installed. Swarm does not bundle or install it. If bd is not found at startup, error with installation instructions.

Integration Points

  1. Prompt builder (Stage 4): Runs bd ready --json to get available tasks, includes in agent prompt.
  2. Agent prompt instructions: Agents are told to use bd claim, bd close as part of their workflow.
  3. Status command: May query beads summary for swarm status output.
  4. Shared beads branch: swarm/<session-id>/beads with optimistic concurrency.

Alternatives Considered

AlternativeWhy rejected
Built-in task systemReinventing the wheel, beads already works well
Bundle beads as a libraryTight coupling, harder to update independently
Make beads optionalCore workflow depends on task assignment

Consequences

  • Pre-flight check at swarm start: verify bd is in PATH and functional.
  • Beads state is captured as a subprocess call (stdout parsing).
  • Beads branch conflicts are handled by optimistic retry.

ADR-009: Error Handling Strategy

Status

Accepted

Context

The swarm orchestrator has multiple error domains (config, git, messaging, agents, sessions) that surface at different layers. We need a consistent strategy for error propagation, user-facing messages, and recovery.

Decision

Use anyhow for application-level error propagation and thiserror for domain-specific error enums in errors.rs.

Domain Error Types

#![allow(unused)]
fn main() {
// All defined in swarm/src/errors.rs
enum ConfigError    { MissingFile, ParseError, ValidationError, VersionMismatch }
enum GitError       { NotARepo, DirtyTree, WorktreeOp, VersionTooOld }
enum MessagingError { DbOpen, DbLocked, UnknownAgent, SelfSend }
enum AgentError     { SpawnFailed, BinaryNotFound, Timeout }
enum SessionError   { StaleLockfile, RecoveryNeeded }
}

Each variant carries a human-readable message via #[error("...")].

Propagation Rules

  1. Library-style modules (config.rs, messaging.rs, session.rs, worktree.rs) return Result<T, SpecificError> using domain error types.
  2. Orchestrator and runner use anyhow::Result and convert via ? (automatic From impls from thiserror).
  3. CLI layer (main.rs) catches anyhow::Error, prints user-friendly messages, and sets exit codes.

User-Facing Error Messages

  • All errors surfaced to the user include: what failed, why, and what to do.
  • Example: "Git version 2.17 is too old. Swarm requires git >= 2.20. Please upgrade git."
  • Internal errors (panics, unexpected states) are logged with full context to orchestrator.log and shown to the user as "internal error, see .swarm/orchestrator.log".

Recovery vs Fatal

CategoryBehavior
Config errorsFatal at startup. Fix config and retry.
Git prereq errorsFatal at startup. Fix environment and retry.
Agent spawn errorsPer-agent retry with backoff (see state machine).
Messaging DB errorsFatal if DB can't open. Transient locks retried via busy_timeout.
Session stalePrompt user: recover or clean and restart.

Alternatives Considered

  1. anyhow only (no domain types): Simpler but loses structured matching for tests and recovery logic.
  2. Custom error type with enum: More boilerplate than thiserror for the same result.
  3. eyre + color-eyre: Better panic reports but adds dependency; anyhow is sufficient with tracing.

Consequences

  • Domain errors are testable via pattern matching.
  • anyhow provides clean ?-chaining in orchestrator code.
  • Error messages are consistent and actionable.
  • thiserror derives keep boilerplate minimal.

ADR-010: Shared Beads Branch with Optimistic Concurrency

Status

Accepted

Context

Swarm agents need to coordinate task assignment through beads. Each agent runs in its own git worktree on its own branch. The beads database (.beads/) must be accessible to all agents for claiming, closing, and querying tasks.

The question: how do agents share beads state when they're on different branches?

Decision

Use a shared beads branch (swarm/<session-id>/beads) that all agent worktrees can access. Beads operations use optimistic concurrency: agents read from the branch, make changes, and push. If a push fails (another agent pushed first), the agent rebases and retries.

Mechanism

  1. At session start, create_beads_branch() creates swarm/<session>/beads from HEAD.
  2. Each agent's prompt includes beads state read from this shared branch.
  3. When an agent runs bd claim or bd close, beads writes to the local .beads/ directory and commits to the shared beads branch.
  4. If two agents try to update beads simultaneously, one push will fail. The beads CLI handles the retry (pull-rebase-push).

Why not per-agent beads?

Per-agent beads would require a merge step and could lead to conflicting claims (two agents claiming the same bead). A shared branch with optimistic concurrency prevents double-claims at the git level.

Alternatives Considered

1. Beads in SQLite (alongside messages)

  • Pro: No git contention, fast.
  • Con: Beads is an external tool with its own storage. Duplicating its state in SQLite would require constant syncing. Beads' git-native design is a feature, not a limitation.

2. Per-agent beads with supervisor merge

  • Pro: No contention between agents.
  • Con: Double-claims are possible (two agents claim same bead before supervisor merges). Adds complexity to supervisor and delays visibility.

3. File-locking beads

  • Pro: Simple mutual exclusion.
  • Con: Worktrees are on different filesystem paths. File locks don't work across worktrees easily. Also fragile under crash scenarios.

Consequences

  • Agents may occasionally see a brief retry delay when beads pushes conflict.
  • The shared branch is created/deleted per session (no permanent branch pollution).
  • The beads CLI must be pre-installed and support the shared branch workflow.
  • The prompt builder reads beads state from the shared branch (Stage 4 of prompt pipeline).

Tradeoffs

AspectImpact
SimplicityMedium — leverages git's existing conflict resolution
CorrectnessHigh — git push prevents double-claims
PerformanceSlightly slower than SQLite for high-contention scenarios
Crash safetyGood — git reflog provides recovery

Development

Development setup, build process, and contribution workflow for swarm.

Prerequisites

ToolVersionPurpose
RustLatest stableBuild the project
Git2.20+Required for worktree operations
bd (beads)LatestIssue tracking

Clone and Build

git clone <repo-url> swarm
cd swarm
cargo build

The workspace has a single member crate at swarm/.

Build with WASM Support

To enable the WASM sandbox feature:

cargo build --features wasm-sandbox

This requires wasmtime v41 and adds significant compile time. Only enable it if you're working on WASM tool functionality.

Release Build

cargo build --release

Project Structure

swarm/
├── swarm/                    # Main crate
│   ├── src/
│   │   ├── lib.rs            # Module declarations (22+ pub modules)
│   │   ├── main.rs           # Entry point
│   │   ├── cli.rs            # CLI argument parsing (clap)
│   │   ├── config.rs         # Configuration loading and validation
│   │   ├── orchestrator.rs   # Session lifecycle management
│   │   ├── session.rs        # Session info and lockfile
│   │   ├── router.rs         # Message routing and urgent polling
│   │   ├── mailbox.rs        # SQLite messaging
│   │   ├── worktree.rs       # Git worktree operations
│   │   ├── prompt.rs         # 14-section prompt pipeline
│   │   ├── errors.rs         # 6 error enums (thiserror)
│   │   ├── agent/
│   │   │   ├── state.rs      # Agent state machine (8 states)
│   │   │   ├── runner.rs     # Agent execution loop
│   │   │   └── registry.rs   # Agent registry
│   │   ├── tools/
│   │   │   ├── mod.rs        # Tool trait, ToolResult, ExecutionMode
│   │   │   ├── registry.rs   # ToolRegistry
│   │   │   ├── context.rs    # ToolContext
│   │   │   ├── bash.rs       # BashTool
│   │   │   ├── read.rs       # ReadTool
│   │   │   ├── write.rs      # WriteTool
│   │   │   ├── edit.rs       # EditTool
│   │   │   ├── glob.rs       # GlobTool
│   │   │   ├── grep.rs       # GrepTool
│   │   │   ├── web_fetch.rs  # WebFetchTool
│   │   │   ├── web_search.rs # WebSearchTool
│   │   │   ├── mailbox.rs    # MailboxTool
│   │   │   ├── task.rs       # TaskTool
│   │   │   ├── ask_user.rs   # AskUserTool
│   │   │   ├── skill.rs      # SkillTool
│   │   │   ├── notebook_edit.rs # NotebookEditTool
│   │   │   └── wasm/         # WASM sandbox (feature-gated)
│   │   ├── skills/           # Skill discovery and resolution
│   │   ├── permissions/      # Permission evaluation
│   │   ├── mcp/              # MCP server integration
│   │   ├── hooks/            # Lifecycle hooks
│   │   └── tui/              # Terminal UI (ratatui)
│   └── Cargo.toml
├── specs/                    # Specification documents
├── docs/                     # mdBook documentation
├── wit/                      # WASM Interface Types
├── Cargo.toml                # Workspace root
└── CLAUDE.md                 # Agent instructions

Key Dependencies

CratePurpose
tokioAsync runtime (full features)
clapCLI argument parsing
ratatui + crosstermTerminal UI
rusqlite (bundled)SQLite for messaging
anthropicAnthropic API SDK
reqwestHTTP client (rustls-tls)
serde + serde_jsonSerialization
anyhow + thiserrorError handling
tracingStructured logging
wasmtime (optional)WASM runtime

Running Tests

cargo test

Run tests for a specific module:

cargo test --lib tools::bash

Run with logging:

RUST_LOG=debug cargo test -- --nocapture

Logging

Swarm uses the tracing crate with tracing-subscriber. Control verbosity via RUST_LOG:

# Default
RUST_LOG=info cargo run -- start

# Debug output
RUST_LOG=debug cargo run -- start

# Per-module filtering
RUST_LOG=swarm=debug,rusqlite=warn cargo run -- start

# Quiet
RUST_LOG=warn cargo run -- start

Error Handling Conventions

Swarm follows ADR-009:

  • Library code: Use thiserror enums for precise, typed errors. There are 6 error enums in errors.rs covering config, git, mailbox, agent, workflow, and session errors.
  • Binary/orchestrator: Use anyhow for ergonomic error propagation with the ? operator.
  • All thiserror types automatically convert to anyhow::Error.

Contribution Workflow

Swarm uses a spec-first workflow. See Spec Workflow for full details.

Quick Summary

  1. Pick a task: bd ready
  2. Claim it: bd update <id> --status in_progress
  3. If the task involves new design, write specs first in specs/
  4. Implement the code
  5. Write tests
  6. Run quality gates: cargo test && cargo clippy
  7. Close the task: bd close <id>
  8. Commit and push:
git add <files>
git commit -m "feat: description of change"
git pull --rebase
bd sync
git push

Commit Messages

Use conventional commits:

PrefixWhen to Use
feat:New feature
fix:Bug fix
refactor:Code restructuring without behavior change
docs:Documentation only
test:Adding or updating tests
chore:Build, CI, tooling changes

Session Completion

When ending a work session, you must complete the landing sequence:

  1. File issues for remaining work (bd create)
  2. Run quality gates (cargo test, cargo clippy)
  3. Update issue status (bd close, bd update)
  4. Push to remote:
    git pull --rebase
    bd sync
    git push
    git status  # Must show "up to date with origin"
    

Work is not complete until git push succeeds.

CI

The project uses GitHub Actions for CI. The docs pipeline is configured in .github/workflows/docs.yml for building and deploying the mdBook site.

Quality gates that should pass before pushing:

  • cargo build — Successful compilation
  • cargo test — All tests pass
  • cargo clippy — No lint warnings

Spec Workflow

How to create and manage specification documents in the swarm project.

Overview

Swarm uses a spec-first development process. Before implementing a feature or making significant changes, you write specification documents that capture the design, contracts, and invariants. These specs serve as the authoritative source of truth for implementation and are stored in specs/.

Why Specs First?

  1. Clarity — Forces you to think through the design before writing code
  2. Reviewability — Easier to review a design document than a large code PR
  3. Agent-friendly — AI agents can read specs to understand what to implement
  4. Traceability — Each implementation task links back to its spec

Spec Categories

The specs/ directory contains three categories of documents:

Architecture Decision Records (ADRs)

ADRs document significant architectural choices with rationale. They live in docs/src/adr/ and are numbered sequentially:

docs/src/adr/
  001-tokio-async.md
  002-sqlite-messaging.md
  003-agent-backend-trait.md
  ...

Write an ADR when you need to:

  • Choose between competing approaches (e.g., SQLite vs Redis for messaging)
  • Make a decision that affects the whole system (e.g., async runtime choice)
  • Document why something was done a certain way for future contributors

Contract Documents

Contracts define the precise interface and behavior of a module or subsystem. They're the most common spec type:

specs/
  contract-config-schema.md
  contract-agent-state-machine.md
  contract-mailbox-schema.md
  ...

A contract includes:

  • File location (which source files implement it)
  • Type definitions (structs, enums, traits)
  • Method signatures and behavior
  • Invariants and validation rules
  • Error handling

Plan Documents

Plans describe a multi-step implementation strategy. They're used for larger efforts that span multiple sessions:

specs/
  plan-sdk-integration.md
  plan-mailbox-migration.md

Writing a Spec

Step 1: Choose the Right Type

SituationSpec Type
Choosing between approachesADR
Defining a module's interfaceContract
Planning a multi-session effortPlan

Step 2: Create the File

Use a descriptive name with the appropriate prefix:

# Contract
touch specs/contract-my-feature.md

# ADR (in docs)
touch docs/src/adr/011-my-decision.md

Step 3: Write the Content

Contract Template

# Contract: [Feature Name]

## File Location

`swarm/src/my_module.rs`

## Overview

Brief description of what this module does and why it exists.

## Type Definitions

```rust
pub struct MyStruct {
    pub field: Type,
}

pub enum MyEnum {
    Variant1,
    Variant2 { data: String },
}

Methods

MethodSignatureDescription
new()fn new() -> SelfCreate instance
process()async fn process(&self, input: Value) -> Result<Output>Process input

Invariants

  1. [Rule that must always hold]
  2. [Another rule]

Error Handling

ErrorWhenRecovery
MyError::NotFoundItem doesn't existReturn error to caller
  • [Link to related spec or ADR]

#### ADR Template

```markdown
# ADR-NNN: [Title]

## Status

Accepted | Proposed | Superseded by ADR-XXX

## Context

What problem are we solving? What constraints exist?

## Decision

What did we decide?

## Consequences

### Positive
- Benefit 1
- Benefit 2

### Negative
- Tradeoff 1

### Neutral
- Observation

Step 4: File Tasks from the Spec

After writing the spec, create beads tasks that reference it:

bd create "Implement MyStruct per specs/contract-my-feature.md"
bd create "Add tests for MyStruct per specs/contract-my-feature.md"
bd create "Add MyStruct to registry per specs/contract-my-feature.md"

Break large specs into small, independently implementable tasks. Each task should:

  • Reference the spec file
  • Describe a specific deliverable
  • Be completable in a single session

The Full Workflow

1. Identify need       →  "We need feature X"
2. Write spec          →  specs/contract-feature-x.md
3. Review spec         →  Get feedback, iterate
4. File tasks          →  bd create "Implement X per specs/..."
5. Implement           →  Pick up tasks with bd ready
6. Verify              →  Check implementation against spec
7. Update spec         →  If the implementation diverged, update the spec

Spec Conventions

Do

  • Be precise about types — Use real Rust type signatures, not pseudocode
  • Include invariants — Document rules that must always hold
  • List error cases — Every error variant with when it occurs
  • Link related specs — Cross-reference ADRs and other contracts
  • Keep specs updated — When implementation changes, update the spec

Don't

  • Don't write implementation code — Specs describe what, not how
  • Don't duplicate — Reference other specs instead of copying
  • Don't over-specify — Leave implementation details to the implementer
  • Don't write stale specs — Archive or update specs that no longer apply

Existing Specs

The specs/ directory currently contains 59 documents:

  • 10 ADRs — Major architectural decisions
  • ~46 contracts — Module interfaces and behavior
  • 3 plans — Multi-session implementation strategies

Use ls specs/ to see all spec files, or check specs/README.md for an index.

Adding Tools

How to add new built-in tools to swarm.

Overview

Tools are the primary way agents interact with the outside world. Each tool implements the Tool trait and is registered in the ToolRegistry. This guide walks through adding a new native tool from scratch.

The Tool Trait

Every tool implements this trait from swarm/src/tools/mod.rs:

#![allow(unused)]
fn main() {
pub trait Tool: Send + Sync {
    /// Unique tool name (e.g., "bash", "read", "my_tool")
    fn name(&self) -> &str;

    /// Human-readable description for the model
    fn description(&self) -> &str;

    /// JSON Schema describing the tool's input parameters
    fn input_schema(&self) -> Value;

    /// Whether the tool runs natively or in a sandbox (default: Native)
    fn execution_mode(&self) -> ExecutionMode {
        ExecutionMode::Native
    }

    /// Execute the tool with the given input and context
    fn execute<'a>(
        &'a self,
        input: Value,
        ctx: &'a ToolContext,
    ) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + 'a>>;
}
}

The five methods:

MethodRequiredPurpose
name()YesUnique identifier used in tool calls
description()YesShown to the model to explain what the tool does
input_schema()YesJSON Schema the model uses to construct input
execution_mode()No (default: Native)Native or Sandboxed
execute()YesPerforms the actual work

Step 1: Create the Tool File

Create a new file in swarm/src/tools/:

touch swarm/src/tools/my_tool.rs

Step 2: Implement the Tool Trait

#![allow(unused)]
fn main() {
use anyhow::Result;
use serde_json::Value;
use std::future::Future;
use std::pin::Pin;

use super::{Tool, ToolResult};
use super::context::ToolContext;

pub struct MyTool;

impl MyTool {
    pub fn new() -> Self {
        Self
    }
}

impl Tool for MyTool {
    fn name(&self) -> &str {
        "my_tool"
    }

    fn description(&self) -> &str {
        "A brief description of what this tool does. \
         The model reads this to decide when to use the tool."
    }

    fn input_schema(&self) -> Value {
        serde_json::json!({
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The search query"
                },
                "limit": {
                    "type": "integer",
                    "description": "Maximum number of results",
                    "default": 10
                }
            },
            "required": ["query"]
        })
    }

    fn execute<'a>(
        &'a self,
        input: Value,
        ctx: &'a ToolContext,
    ) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + 'a>> {
        Box::pin(async move {
            // Parse input
            let query = input["query"]
                .as_str()
                .ok_or_else(|| anyhow::anyhow!("missing required field: query"))?;

            let limit = input["limit"].as_u64().unwrap_or(10);

            // Use context for working directory, agent info, etc.
            let _working_dir = &ctx.working_dir;

            // Check permissions if needed
            let decision = ctx.check_permission("my_tool", &input);
            // Handle decision...

            // Do the actual work
            let result = format!("Found results for '{}' (limit: {})", query, limit);

            Ok(ToolResult::text(result))
        })
    }
}
}

Key Points

  • ToolResult::text(s) — Creates a successful result with text content
  • ToolResult::error(s) — Creates an error result (displayed to the model as an error)
  • ctx.working_dir — The agent's working directory (its worktree)
  • ctx.agent_name — The name of the calling agent
  • ctx.session_id — The current session ID
  • ctx.is_cancelled() — Check if the agent's session has been cancelled
  • ctx.check_permission(tool, input) — Check permission for this operation

Step 3: Register the Module

Add the module to swarm/src/tools/mod.rs:

#![allow(unused)]
fn main() {
mod my_tool;
}

Then register the tool in the default_registry() function in swarm/src/tools/registry.rs:

#![allow(unused)]
fn main() {
pub fn default_registry() -> ToolRegistry {
    let mut registry = ToolRegistry::new();
    registry.register(Box::new(bash::BashTool::new()));
    registry.register(Box::new(read::ReadTool::new()));
    // ... existing tools ...
    registry.register(Box::new(my_tool::MyTool::new()));
    registry
}
}

Step 4: Write Tests

Add tests in the same file or in a separate test module:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use crate::tools::context::ToolContext;
    use serde_json::json;
    use std::path::PathBuf;

    #[tokio::test]
    async fn test_my_tool_basic() {
        let tool = MyTool::new();
        let ctx = ToolContext::new(
            PathBuf::from("/tmp/test"),
            "test-agent".to_string(),
            "test-session".to_string(),
        );

        let input = json!({
            "query": "hello"
        });

        let result = tool.execute(input, &ctx).await.unwrap();
        assert!(!result.is_error);
    }

    #[tokio::test]
    async fn test_my_tool_missing_query() {
        let tool = MyTool::new();
        let ctx = ToolContext::new(
            PathBuf::from("/tmp/test"),
            "test-agent".to_string(),
            "test-session".to_string(),
        );

        let input = json!({});
        let result = tool.execute(input, &ctx).await;
        assert!(result.is_err());
    }

    #[test]
    fn test_schema_is_valid() {
        let tool = MyTool::new();
        let schema = tool.input_schema();
        assert_eq!(schema["type"], "object");
        assert!(schema["properties"]["query"].is_object());
    }
}
}

Step 5: Run Tests

cargo test --lib tools::my_tool

ToolResult Details

The ToolResult struct supports text and image content:

#![allow(unused)]
fn main() {
pub struct ToolResult {
    pub content: Vec<ToolResultContent>,
    pub is_error: bool,
}

pub enum ToolResultContent {
    Text(String),
    Image { media_type: String, data: String },
}
}

Helper constructors:

MethodCreates
ToolResult::text("output")Successful text result
ToolResult::error("message")Error result displayed to model

For richer results, construct ToolResult directly:

#![allow(unused)]
fn main() {
Ok(ToolResult {
    content: vec![
        ToolResultContent::Text("Found 3 files:".to_string()),
        ToolResultContent::Text("- src/main.rs\n- src/lib.rs\n- Cargo.toml".to_string()),
    ],
    is_error: false,
})
}

ToolContext Details

The ToolContext provides execution context:

#![allow(unused)]
fn main() {
pub struct ToolContext {
    pub working_dir: PathBuf,           // Agent's worktree directory
    pub agent_name: String,             // Name of the calling agent
    pub session_id: String,             // Current session ID
    pub env_vars: HashMap<String, String>, // Agent environment variables
    pub cancellation_token: CancellationToken, // Cancellation signal
    pub permissions: Option<Arc<PermissionEvaluator>>, // Permission checker
}
}

Use ctx.is_cancelled() to check for cancellation in long-running tools. This is important for tools that perform loops or wait for external resources.

Walkthrough: Existing Tool

Here's how the built-in GlobTool is structured (simplified):

#![allow(unused)]
fn main() {
pub struct GlobTool;

impl Tool for GlobTool {
    fn name(&self) -> &str { "glob" }

    fn description(&self) -> &str {
        "Fast file pattern matching tool that works with any codebase size"
    }

    fn input_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "pattern": {
                    "type": "string",
                    "description": "Glob pattern (e.g., **/*.rs)"
                },
                "path": {
                    "type": "string",
                    "description": "Directory to search in"
                }
            },
            "required": ["pattern"]
        })
    }

    fn execute<'a>(
        &'a self,
        input: Value,
        ctx: &'a ToolContext,
    ) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + 'a>> {
        Box::pin(async move {
            let pattern = input["pattern"].as_str()
                .ok_or_else(|| anyhow::anyhow!("missing pattern"))?;

            let base_dir = input["path"].as_str()
                .map(PathBuf::from)
                .unwrap_or_else(|| ctx.working_dir.clone());

            let full_pattern = base_dir.join(pattern);
            let matches = glob::glob(full_pattern.to_str().unwrap())?;

            let files: Vec<String> = matches
                .filter_map(|entry| entry.ok())
                .map(|p| p.display().to_string())
                .collect();

            Ok(ToolResult::text(files.join("\n")))
        })
    }
}
}

Pattern to follow:

  1. Parse input from JSON, validating required fields
  2. Use ctx.working_dir as default base path
  3. Perform the operation
  4. Return ToolResult::text() for success or ToolResult::error() for failures

Checklist

Before submitting your tool:

  • Implements all required Tool trait methods
  • Input schema has "type": "object" with documented properties
  • Required fields are listed in "required" array
  • Description is clear enough for the model to know when to use it
  • Tool handles missing/invalid input gracefully
  • Module is declared in tools/mod.rs
  • Tool is registered in default_registry() in tools/registry.rs
  • Tests cover basic usage and error cases
  • cargo test passes
  • cargo clippy has no warnings