Roadmap
Status: livingM0 — See everything (observe only)
Goal: one dashboard shows every Claude Code session on the machine, live, across repos. No enforcement yet.
| ID | Task | Depends | Status |
|---|---|---|---|
| M0.1 | Scaffold monorepo, CI, Biome, Vitest, Apache-2.0, README skeleton | — | ✅ 2026-08-20 — 7 packages, in-memory event log + SSE, hook→daemon→SSE smoke |
| M0.2 | core: event types, Claude Code hook adapter, project identity (git common dir) |
M0.1 | ✅ 2026-08-20 |
| M0.3 | daemon: SQLite schema, /v1/hook/* ingestion, /v1/events SSE, port file, auto-start |
M0.2 | ✅ 2026-08-20 SQLite persisted; port file + auto-start done (M0.9.1); unix socket deferred |
| M0.4 | hook shim + `swarm install |
uninstall` (user-level settings edit, idempotent, reversible) | M0.3 |
| M0.5 | cli: add, ls, status, tail, doctor |
M0.3 | ✅ 2026-08-20 incl. tail, setup, start/stop/restart |
| M0.6 | web: Fleet + Session views over SSE, served by daemon |
M0.3 | ✅ 2026-08-22 vanilla HTML/JS served from web/public (one React island for menus, M5.8); views: Fleet / Board / PRs / Timeline / Spend / Stats + Session detail; data-grid everywhere (M0.9.9); add/remove/pin project |
| M0.7 | Smoke test: fake hook events → SSE assertions; dogfood on a real project | M0.4–M0.6 | ✅ 2026-08-20 (tools/smoke.ts) |
M0.8 — What agents are doing (transcript intelligence) ✅ 2026-08-20
Landed ahead of M1 by request. Reads each session's transcript JSONL (path from the hook) plus its subagents/*.jsonl, tailing on hook activity and on a 5s timer (long turns emit no hooks).
| ID | Task | Status |
|---|---|---|
| M0.8.1 | core/pricing: static table + fromLiteLLM, longest-prefix match, cache tiers |
✅ |
| M0.8.2 | core/transcript: JSONL → turns (model, per-tier usage, thinking, text, tools, sidechain); collapses streamed lines |
✅ |
| M0.8.3 | daemon: turns table, offset-tracked tailer, subagent files, session title/model/branch/context | ✅ |
| M0.8.4 | daemon: cost via pricing overlay (~/.swarm/pricing.json, LiteLLM refresh); reprice on refresh |
✅ |
| M0.8.5 | web: Fleet shows title/model/out/ctx/cost; Session detail (cost, context %cached, thinking, tool histogram, live reasoning stream); Spend view (by project/model, today/all-time, 14-day bars) | ✅ |
| M0.8.7 | web: design system pass (theme-aware tokens light/dark, cards, refined type/tables/status); pin/unpin discovered projects; per-session live model + multi-model +N; latest-turn model via SQL |
✅ 2026-08-20 |
| M0.8.6 | tooling: migrate vitest→bun:test, stop emitting per-package dist, biome 2.5 | ✅ |
M0.9 — Ship it (community-ready) ✅ 2026-08-22
Goal: a stranger clones or npx-installs Swarm and it works in under two minutes, with no dev paths, no manual daemon, and docs that answer the obvious questions. This is the open-source release track.
| ID | Task | Depends | Status |
|---|---|---|---|
| M0.9.1 | Daemon lifecycle: ~/.swarm/daemon.json (port/pid/version), graceful shutdown, swarm start/stop/restart, client ensureDaemon() auto-spawn |
M0.3 | ✅ 2026-08-20 daemon.json (port/pid/version), graceful SIGTERM cleanup, start/stop/restart, client ensureDaemon() auto-spawn |
| M0.9.2 | Portable install: write bin commands that work both from a clone and from a global install (no hard-coded dev paths); swarm setup one-shot (ensure daemon → install hooks → open UI) |
M0.9.1 | ✅ 2026-08-20 install writes portable command (bare bin under node_modules, else bun+abs path); swarm setup one-shot |
| M0.9.3 | Community scaffolding: real README (quickstart, what/why, screenshots), CONTRIBUTING, CODE_OF_CONDUCT (Contributor Covenant), SECURITY.md, LICENSE headers, issue/PR templates | — | ✅ 2026-08-20 README, CONTRIBUTING, CODE_OF_CONDUCT, SECURITY, issue/PR templates |
| M0.9.4 | Package metadata for publish: description, keywords, repo/bugs/homepage, engines, files; decide publish name (OQ-1) |
M0.9.1 | ✅ 2026-08-21 @ra3orblade/swarm (npm/package.json): metadata, engines, files, publishConfig |
| M0.9.5 | swarm doctor as the setup guide: checks bun/claude/daemon/hooks/db and prints the exact next command for each gap |
M0.9.1 | ✅ 2026-08-20 doctor checks bun/claude/daemon/hooks and prints the fix per gap |
| M0.9.6 | Release: bundle bins with bun build into one publishable package; optional standalone single-file binaries per OS on GitHub Releases; CI publish workflow |
M0.9.4 | ✅ 2026-08-21 tools/build-pkg.ts → npm/ (4 bundled bins + web, zero deps); resolveBin() makes hooks/MCP/daemon spawn work from clone, global install and bunx; verified by packing + installing into a clean prefix; npm job in release.yml (provenance, tag==version check) + CI builds the bundle; tools/version.ts bumps all version fields. Standalone per-OS CLI binaries left optional (desktop already ships the compiled sidecar) |
| M0.9.7 | Config: .swarm.toml loader (optional, per-repo) + ~/.swarm/config.toml (global: port, lease TTL, offline) |
M0.9.1 | ✅ 2026-08-22 core/config.ts: two TOML layers deep-merged over defaults, lenient validation (bad values → default + warning, bad TOML ignored), repo config cached ~30s, port preference SWARM_PORT > [daemon].port > 7777; keys today are [daemon].port + [rules].* (see 13-config) — lease TTL / offline still ⚪ |
| M0.9.8 | Website: getswarm.vercel.app landing page + rendered docs and changelog (site/, built by tools/build-site.ts, bun run site:build) |
M0.9.3 | ✅ 2026-08-22 |
| M0.9.9 | Data-grid everywhere: sortable / resizable / filterable columns with column-visibility menu, layouts persisted per table; design tokens (12-design-tokens) with no raw values in CSS rules | M0.6 | ✅ 2026-08-22 web/public/table.js; Fleet, Board, PRs, Spend, Stats all on the grid |
M1 — Hold things (ledger) ✅ 2026-08-22
| ID | Task | Depends | Status |
|---|---|---|---|
| M1.1 | Claims: claim/renew/release/list/reap with worktree create/remove; fail-closed; dirty/unpushed refusal | M0 | ✅ 2026-08-20 daemon claims table + real git worktree add/remove, heldWork dirty/unpushed gate, events; swarm claim/renew/release/claims/reap CLI; validated end-to-end incl. fail-closed + dirty-refusal |
| M1.2 | Auto-renew on holder activity; orphan detection + incidents | M1.1 | ✅ 2026-08-22 shouldAutoRenew in core/ledger.ts: a session working inside a claimed worktree (cwd) renews the lease on any hook / transcript activity once it is past half-way (throttled to once a minute per session); daemon tick sweeps expired leases holding work every minute → orphaned + incident.opened (orphaned_claim); detection only, worktree removal stays an explicit reap/release |
| M1.3 | Handoff/resume payloads; injected via SessionStart context |
M1.1 | ✅ 2026-08-22 handoffs table; swarm handoff <task> --done --remaining [--files] [--verify] / swarm resume; MCP swarm_handoff / swarm_resume; GET/POST /v1/handoffs; the SessionStart hook returns additionalContext built by store.sessionContext(cwd): what the session holds (+ lease), the latest handoff, gate status, held resources, and the rule modes (always when a rule is deny) |
| M1.4 | Runtime resources + process registry (serve, proc; port allocation; pid-based liveness) (port of serve.ts, workers.ts) |
M0 | ✅ 2026-08-22 Phase 1: named singletons (acquire/release/reap, pid+lease liveness, held ports auto-protected); MCP + CLI + Board. Phase 2: swarm serve start [--name] [--from-port] -- <cmd> allocates a free port (ledger + bind probe), spawns detached with PORT, logs to ~/.swarm/logs, registers pid + start time and acquires the singleton; `serve ls |
| M1.5 | mcp server with the claim tools; registered by swarm install |
M1.1 | ✅ 2026-08-21 stdio MCP server: swarm_status/claim/renew/release/reap forwarding to the daemon (project from cwd); registered in ~/.claude/settings.json by install; integration-tested via an MCP client → fail-closed claim into a worktree |
| M1.6 | Claims board in the dashboard (lease countdown, orphan highlighting, release/force-release); .swarm.toml task source |
M1.1 | ✅ 2026-08-22 Claims section on the Board (held/expired/orphaned, lease countdown, release + force-release); task source: .swarm.toml [tasks] source = "plan.md" parsed by core/tasks.ts (markdown `ID |
| M1.7 | Board view: one page per project with Claims + Worktrees + Resources + Incidents (the "View 2 — Project" of 08-interface, minus tasks/processes/gates) | M1.1, M1.4 | ✅ 2026-08-22 |
| M1.8 | PRs / merge queue: open PRs/MRs across every tracked repo via the locally-authenticated gh / glab (GitHub + GitLab, no tokens stored), gentle per-project polling, merge from the dashboard; GET /v1/prs, POST /v1/prs/merge |
M0.6 | ✅ 2026-08-22 core/forge.ts (remote parsing + normalizers), daemon/forge.ts, PRs view |
M2 — Enforce (rules) ✅ 2026-08-22
| ID | Task | Depends | Status |
|---|---|---|---|
| M2.1 | Rule engine + built-ins: shared-tree-readonly, no-pattern-kill, claim-required-to-write, protected-ports, no-foreign-worktree |
M1 | ✅ 2026-08-22 rules v2 in core/rules.ts, each configurable per repo as ask / deny / off (13-config): shared_tree / destructive_git / pattern_kill / protected_ports on Bash; no_foreign_worktree (default ask) and claim_required_to_write (opt-in) on Write/Edit/MultiEdit/NotebookEdit paths and Bash cwd, with holding inferred from the session cwd being inside a claimed worktree; held resource ports auto-protected; deny returned to Claude Code as a real permission denial |
| M2.2 | Gates: record/query, latest-run-wins, rubric required; swarm gate + MCP tool |
M1 | ✅ 2026-08-22 core/gates.ts (validateGateRun rejects a missing rubric, gateStatus latest-wins with id tie-break, gatesSatisfied); gates table, GET/POST /v1/gates, a fail opens a gate_failed incident; .swarm.toml [gates] required; `swarm gate record |
| M2.3 | Incidents view; ack; denied-action feed | M2.1 | ✅ 2026-08-22 every rule hit is recorded (incident.opened); Incidents view = the denied-action feed (Open / All, per-rule counts, reason, session link) with per-row Ack and Ack-all (incident_acks table, POST /v1/incidents/:seq/ack, POST /v1/incidents/ack); GET /v1/incidents?open=1&project=; open count in the nav badge and /v1/state.openIncidents; the Board keeps a short open-only section |
M3 — Drive (spawned agents) ✅ 2026-08-22
| ID | Task | Depends | Status |
|---|---|---|---|
| M3.1 | swarm run: spawn claude -p stream-json in a claimed worktree; ingest; stdin steering |
M1 | ✅ 2026-08-22 daemon/runner.ts: claim (reuses the caller's held worktree), claude -p --output-format stream-json --input-format stream-json --session-id <uuid> spawned by the daemon in the worktree with stdin kept open; prompt + run send go in as stream-json user messages; session pre-registered as spawned so hooks/transcripts ingest it like any session; result lines → run.result (cost, turns, error); pid in the process registry (stop = close stdin, then TERM/KILL by pid + start time); GET/POST /v1/runs, POST /v1/runs/:id/send, DELETE /v1/runs/:id; CLI `swarm run --task --prompt |
| M3.2 | Permission broker via --permission-prompt-tool → rules → dashboard |
M3.1, M2.1 | ✅ 2026-08-22 spawned runs pass --permission-prompt-tool stdio; the runner intercepts control_request{can_use_tool} and evaluates the tool through the shared store.evaluateTool (same rules as the hook): deny → auto-control_response deny with the reason, allow → auto-allow (spawned agent, no TTY), anything the rules flag as ask → held pending and surfaced (permission.requested), resolved by a human via POST /v1/runs/:id/permissions/:reqId {allow} → deny/allow control_response (permission.resolved); Allow/Deny cards on the spawned session; wire format verified against Claude Code 2.1.240 |
| M3.3 | Run from dashboard; stop/kill; cost + token rollups per project | M3.1 | ✅ 2026-08-22 Run on ready/held task rows → drawer (prompt prefilled from the task, permission mode, model, max turns; ⌘⏎) → POST /v1/runs → opens the session; spawned sessions get a stdin box (Enter to send) + Stop with run id / pid / cost so far; cost + tokens roll up through the transcript like any session (Spend by project) |
M4 — Learn (the data pays off) ✅ 2026-08-22
| ID | Task | Depends | Status |
|---|---|---|---|
| M4.1 | Session replay: scrub per tool call, diff per step; "while you were away" digest per project | M0 | ✅ 2026-08-22 Replay on the session page: a stepper over the session's tool.requested events, each showing the full toolInput + paired toolResponse (lazy-fetched from /v1/events/:seq); Prev/Next, slider, ←/→ keys. "while you were away" digest still ⚪ |
| M4.2 | Cost/token attribution per task, gate, rule; repeated-read detector (context budget view) | M0, M1 | ✅ 2026-08-22 store.attribution(project): cost/tokens/turns per task (sessions matched to a claim by cwd inside its worktree) + a context-budget list (sessions ranked by re-processed cache-read tokens = re-reading); GET /v1/attribution; By task + Context budget on Spend |
| M4.3 | Incident → rule: generate hook predicate from an incident; "write lesson to CLAUDE.md" | M2 | ✅ 2026-08-22 core/lessons.ts: each incident → a .swarm.toml rule snippet + a CLAUDE.md lesson (recurring ask escalates to deny via per rule/target counts); Codify action on the Incidents feed with copy buttons; no repo writes (user applies) |
| M4.4 | Structured auto-handoff at Stop/SessionEnd; "resume where this died" spawns with handoff + tail | M1.3, M3.1 | ✅ 2026-08-22 deriveHandoff in core/ledger.ts (files edited, last verify-looking command, last prompt, last assistant text); the daemon upserts one auto:<session> handoff per session on every Stop/SessionEnd inside a held worktree, silenced by a manual handoff from that session. GET/POST /v1/sessions/:id/resume builds a prompt from the latest handoff + the session's last 12 actions and spawns a run on the task (reusing the held claim); swarm run resume <session>; Resume where it died on ended sessions |
| M4.5 | Memory search over Swarm data (sqlite-vec, local embeddings) — see OQ-9 | M4.4 | ✅ 2026-08-22 FTS5/BM25, not vectors (OQ-9 decision): memory virtual table in swarm.db over handoffs, incidents, gate runs and session last-text, indexed at each write + one-time backfill; core/memory.ts builds docs and sanitises queries (kind: / task: filters, phrase quotes, last-word prefix). GET /v1/memory?q=, swarm search, MCP swarm_search, Search view with snippets |
| M4.6 | Rule dry-run over historical events; flaky-signal detection | M2.1 | ✅ 2026-08-22 core/dryrun.ts replays recorded tool.requested calls through guardBash/guardWrite under any modes, liveness reconstructed from the stream; a flaky signal is a rule that fired ≥3× on the same command and the call ran anyway ≥80% of the time. GET /v1/rules/dryrun?project=&<rule>=<mode>, swarm rules dryrun [--set rule=mode], Dry-run rules on the Incidents view. Never records incidents |
| M4.7 | Desktop notifications with Allow/Deny actions | M3.2 | ✅ 2026-08-22 web Notification API (browser + desktop webview): permission prompts on spawned runs and orphaned claims fire a native notification, click opens the Allow/Deny card / Board; enabled from the settings menu, quiet while focused. Native Tauri notification-action buttons not used (M4.2 permission broker card covers the action) |
| M4.8 | Task-source adapters: GitHub Issues, Linear | M1.6 | ✅ 2026-08-22 [tasks] source = "github" (via the authenticated gh issue list; labels filter; ids GH-<n>, closed=done, in-progress label=active, depends on #n in the body) or "linear" (GraphQL with LINEAR_API_KEY from the daemon env, never stored; team key; blocked-by relations → depends). core/tasks.ts normalizers; daemon/task-sources.ts cached + background-refreshed so the Board never blocks; a source error shows on the Board/swarm tasks instead of an empty list |
M5 — Beyond Claude (multi-agent) ← direction set 2026-08-20
Goal: Swarm observes and coordinates AI coding agents generally, not only Claude Code. The event model already carries raw + normalized fields, and adapters live under core/adapters/<name>.
| ID | Task | Status |
|---|---|---|
| M5.1 | Provider-agnostic pricing: price any model (Anthropic/OpenAI/Google/DeepSeek/…), not just claude-* |
✅ 2026-08-20 static table + LiteLLM refresh generalized; 10 tests |
| M5.2 | AgentAdapter interface + registry in core; Claude Code refactored behind it |
✅ 2026-08-20 adapters/types.ts (AgentAdapter/LogParseResult), registry, claude-code wrapper |
| M5.3 | First non-Claude adapter — Codex CLI: parser + daemon discovery/tailing, agent-tagged sessions in the dashboard | ✅ 2026-08-20 tails ~/.codex rollout logs (bounded scan, offset-tracked, one-time backfill), maps to projects by cwd, prices gpt-5.5; validated on real sessions (integral 70 turns/$3.52, brainstorm 9/$0.42) |
| M5.4 | More adapters behind the same interface | 🟡 2026-08-20 Grok (xAI) done — ACP updates.jsonl, cost via grok pricing, validated on real sessions (grok-4.5); Gemini CLI, Aider, opencode/Cline next |
| M5.5 | Dashboard: agent badge, Fleet agent-filter chips, unified per-agent spend breakdown | ✅ 2026-08-20 badge + filter chips + Spend 'by agent' (Claude/Codex/Grok in one view); Grok session titles from summary.json |
| M5.6 | Visualisations (inline SVG, no chart lib): stacked daily cost by agent with 7/14/30/90d range, weekday×hour activity heatmap, KPI tiles, Fleet sparklines (output/turn), Session token-composition bar + cost-per-turn strip + tool-mix bars, Timeline view (session lanes per project, coloured by agent, 3–72h) | ✅ 2026-08-20 web/public/viz.js; daemon adds daily.agent, hourly, sessions[].spark |
| M5.7 | Visualisation follow-ups: turn ticks / idle gaps on Timeline lanes, claim & lease overlays once M1 lands, gate pass/fail history (M2), per-project spend sparkline in sidebar, log-scale toggle when one day dwarfs the rest | ⚪ |
| M5.9 | Stats view (engagement / "funny numbers"): all-time spend + tokens + turns + streak KPIs, playful equivalents (words written ≈ novels, context re-read ≈ War and Peace, thinking share, cost in coffee), 52-week activity calendar with current/longest streak, tokens-per-day by class (30/90/365d), output-per-day, cumulative spend line, turns by hour of day, model mix, token composition, tool leaderboard, record holders (costliest / longest / biggest turn / busiest day) | ✅ 2026-08-22 GET /v1/stats?project= (store.stats()), viz.line/calendar/streaks; fetched per view-open, not in the 5s snapshot |
| M5.8 | Dashboard chrome: Phosphor icon system (one family, inline SVG subset generated at build), fancy-menus (@react-fancy-menus/core) as a React island — project ⋯/right-click menu, session ⋯/right-click menu, settings menu with theme (system/light/dark), pricing refresh, docs; bun run build:web + generic static route in the daemon |
✅ 2026-08-20 web/tools/build.ts, web/src/menus.tsx; first React in the web package (on-plan: Vite+React is the web stack) |
M6 — Desktop app (Tauri) + autoupdate ← direction set 2026-08-20
Goal: ship Swarm as a real desktop app with automatic updates, not just a CLI + browser tab.
Approach: a Tauri v2 shell hosting the dashboard, with the daemon shipped as a sidecar (the daemon compiled to a single binary via bun build --compile) that the app starts on launch; the webview points at the local daemon. A tray icon shows live-session count and opens the window. Autoupdate via Tauri's built-in updater against signed artifacts + an update manifest on GitHub Releases.
| ID | Task | Depends | Status |
|---|---|---|---|
| M6.1 | Compile the daemon to a standalone binary (bun build --compile); app supervises it as a sidecar |
M0.9.6 | ✅ 2026-08-21 bun build --compile → swarmd; SWARM_WEB_DIR lets it serve the bundled dashboard; Tauri spawns it via tauri-plugin-shell sidecar |
| M6.2 | Tauri v2 scaffold: window + tray, points at the daemon; dev + build scripts | M6.1 | ✅ 2026-08-21 apps/desktop (Rust: sidecar spawn, health-wait, window at daemon URL, tray with Open/Quit); pixel-logo icon set; desktop:prep/dev/build scripts |
| M6.3 | Autoupdate: Tauri updater, signed artifacts, update manifest on GitHub Releases | M6.2 | ✅ 2026-08-22 updater plugin + signed keypair; v0.2.2 shipped the updater artifacts (latest.json + signatures); user-visible since M6.5 |
| M6.4 | Signing/notarization (macOS); CI release pipeline (macOS + Windows + Linux) | M6.3 | ✅ 2026-08-21 signed+notarized macOS shipped (v0.0.4); release.yml now a 3-OS matrix building .dmg / .msi+.exe / .deb+.rpm, native sidecar per runner; Windows/Linux unsigned (no Windows cert yet). v0.2.2 (2026-08-21): mac + Windows + updater artifacts green; Linux AppImage disabled — linuxdeploy fails on GH runners even with libfuse2 + NO_STRIP, Tauri hides its stderr; suspect patchelf on the bun-compiled sidecar. Next: a workflow_dispatch diagnostic job running tauri build --verbose --bundles appimage on a branch. Linux has no auto-update until then |
| M6.5 | Desktop "Check for Updates…": tray item that queries GitHub Releases, native dialogs (up to date / new version), install-and-restart | M6.3 | ✅ 2026-08-22 apps/desktop/src-tauri/src/lib.rs |
M7 — Orchestrate (agents drive Swarm) ← direction set 2026-08-23
Goal: close the loop. Today agents report to Swarm (claim, hand off, gate, search); in M7 one session can dispatch work into worktrees, agents talk to each other and to the human through the daemon, and gates are executed rather than self-reported — so autonomous dispatch is safe. Worktrees become first-class so every spawned run starts warm.
v0.7.0 cut (decided 2026-08-23): M7.1–M7.5, M7.7, M7.10 plus budgets and run permission profiles; M7.6 (full messaging), M7.8 (workflows) and M7.9 (review gate) move to 0.8 — swarm_ask ships with a minimal messages table that M7.6 extends.
Ordering: worktree lifecycle (M7.1–M7.3) and executed gates (M7.4) first — they are what makes dispatch (M7.5) trustworthy; messaging and ask-human (M7.6–M7.7) make it steerable; workflows (M7.8) compose it. Everything is optional .swarm.toml + ~/.swarm/ state — nothing required in the monitored repo.
| ID | Task | Depends | Status |
|---|---|---|---|
| M7.1 | Worktree bootstrap: .swarm.toml [worktree] setup = "<cmd>" + copy = [".env.local", …] run after git worktree add (claims and swarm run alike); output logged to ~/.swarm/logs, failure recorded as an incident, claim still held |
M1.1 | ✅ 2026-08-23 core/worktree.ts plans (repo-relative only, deduped), daemon/bootstrap.ts copies synchronously + runs setup via sh -c in the background (SWARM_WORKTREE/SWARM_TASK env, log ~/.swarm/logs/<project>/bootstrap-<task>.log); store.claim returns bootstrap (log path) and worktree.bootstrapped event on exit, bootstrap_failed incident on non-zero; runner awaits store.awaitBootstrap(worktree) before spawning; CLI/MCP claim replies mention the log |
| M7.2 | Worktrees first-class: `swarm wt create | ls | open |
| M7.3 | Worktree diff + PR in one step: GET /v1/worktrees/:id/diff (stat + per-file), Diff tab on the Board row and on the session page (what did this agent change); "Open PR" = push + gh pr create / glab mr create prefilled from the task + latest handoff |
M7.2, M1.3 | ✅ 2026-08-23 daemon/git.ts worktreeDiff (vs merge-base with the main checkout's branch: commits, numstat + name-status, working tree, untracked) / worktreePatch; core/forge.ts parseNumstat + prDraft (task title, handoff summary, gates checklist, files); ForgeService.openPR (push -u, gh pr create / glab mr create, refuses dirty/detached/main, reuses an open PR); GET /v1/worktrees/diff, GET /v1/prs/draft, POST /v1/prs/open (+ pr.opened event); swarm wt diff, swarm pr open [--dry-run], MCP swarm_pr_open; Board: Diff drawer (file list + coloured patch) and PR drawer on worktree rows, Diff on session pages inside a worktree |
| M7.4 | Executed gates: .swarm.toml [gates.<name>] cmd = "<cmd>" (+ optional timeout, cwd); swarm gate run <name> / POST /v1/gates/run executes in the held worktree, records the run with rubric = command, evidence = tail of output; auto-gate on Stop/SessionEnd in a held worktree writes results into the auto-handoff (M4.4); Run gates on the Board |
M2.2, M4.4 | ✅ 2026-08-23 parseGateDefs + executedGateInput in core (rubric = ran \cmd` — exit N in Ns, evidence = log tail); store.runGatespawnssh -cin the held worktree via the process registry (kindgate, singleton gate:, log ~/.swarm/logs/, timeout→ kill + fail) and records on exit (OQ-13);runGatessequential;[gates] auto = "session-end"|"stop"|"off"runs the executable required gates after Stop/SessionEnd inside a held worktree and writesauto-gates: …into the auto-handoffverify; POST /v1/gates/run, swarm gate run, MCP swarm_gate_run`, Gates on held task rows |
| M7.5 | Dispatch: swarm dispatch <task…> [--ready] [--max N] / MCP swarm_dispatch / Dispatch on the Board — claims a worktree per task and spawns a swarm run in each with the task text as prompt (M3.1), respecting a per-project concurrency cap ([dispatch] max_parallel, default 2) and the required gates: a run whose gates fail does not get "done" flipped, it gets a handoff + an incident; dispatch status rolls up per task on the Board |
M7.1, M7.4, M3.3 | ✅ 2026-08-23 core/dispatch.ts: planDispatch (ready-only, reasons for every refusal, --max, slots), taskPrompt (worktree-only, gates split executable/recorded, handoff, PR, stop-if-blocked), dispatchOutcome (stopped > crashed > gates-failed > no-pr > done); daemon/dispatcher.ts hooks Runner.onEnd: re-runs unmet executable gates, checks the forge for a PR on task/<id>, dispatch.queued/started/finished events, dispatch_failed incident, claim kept, queue drained; [dispatch] max_parallel/permission_mode/model/max_turns/require_pr; GET/POST/DELETE /v1/dispatch; swarm dispatch [--ready|tasks] [--max] [--parallel] | status | clear; MCP swarm_dispatch; Board Dispatch chip → picker + Dispatch section. Swarm's own .swarm.toml now dispatches itself (tests/lint gates executable, bun install on new worktrees) |
| M7.6 | Agent messaging: messages table; MCP `swarm_send(to: session |
task | "lead", text)+swarm_inbox(); delivered to running sessions as additionalContexton the next hook and as stdin to spawned runs (M3.1), pulled on demand viaswarm_inbox; **Messages** thread on the session page and a compose box; swarm msg send |
| M7.7 | Ask the human: MCP swarm_ask(question, options?) parks the session's question as a card (same shape as the permission card, M3.2) + desktop notification (M4.7); the answer lands in the session's inbox (M7.6) and is injected on the next hook; unanswered questions show on Fleet as waiting; swarm questions / swarm answer <id> |
M7.6, M4.7 | ✅ 2026-08-23 shipped ahead of M7.6 on a minimal messages table (kind question; M7.6 extends it). core/questions.ts (validation, context formatting); store ask (task inferred from the held worktree) / answer (once) / inbox (delivered-once) / questionContext for SessionStart; answers ride as additionalContext on the next UserPromptSubmit/PreToolUse/PostToolUse, or stdin for a live spawned run; events question.asked / question.answered; GET/POST /v1/questions, POST /v1/questions/:id/answer, GET /v1/inbox; MCP swarm_ask / swarm_inbox; CLI swarm questions / swarm answer; session page waiting on you cards with option buttons, Fleet Asking badge, notification |
| M7.8 | Workflows: .swarm.toml [[workflows]] name = "ship" steps = ["implement", "gate:test", "gate:review", "pr"] — a declarative per-task sequence the daemon advances: each step is a spawned run (with a prompt template), an executed gate, or a built-in (pr = M7.3); a failed step stops the workflow with a handoff + incident; swarm run --workflow ship --task X, Workflow progress on the task row — see OQ-13 |
M7.3, M7.4, M7.5 | ⚪ → 0.8 |
| M7.9 | Review as a gate: built-in gate:review spawns a read-only claude -p review of the worktree diff (M7.3) with a fixed rubric, records pass/fail + findings as the gate evidence (never edits); Review on the Board row |
M7.3, M7.4 | ✅ 2026-08-23 [gates.review] builtin = "review" (+ model, timeout): core/src/review.ts prompt/argv/verdict parsing, daemon runReviewGate (read-only claude -p --output-format json, registry + log + timeout like executed gates), findings as evidence, rubric-derived verdict; Board shows it as any gate |
| M7.10 | swarm_context MCP tool: returns exactly what SessionStart injects (holds, lease, latest handoff, gates, resources, rule modes, pending messages/questions) so an agent can refresh mid-session; the same server registered for Codex / Gemini CLI once M5.4 adapters can host MCP (coordination becomes two-way for non-Claude agents) |
M1.3, M5.4 | ✅ 2026-08-23 store.contextFor(cwd, session) = sessionContext + undelivered answers (delivered) + open questions; GET /v1/context; MCP swarm_context. swarm install also registers the stdio server in ~/.codex/config.toml ([mcp_servers.swarm], replaced in place) and ~/.gemini/settings.json (mcpServers.swarm) when those dirs exist; uninstall removes exactly those; doctor reports them. Tested idempotent + non-destructive |
Follow-ups likely to fold into earlier milestones once M7 lands: budgets ✅ 2026-08-23 ([budget] daily/weekly/warn_at/on_exceed; core/budget.ts budgetStatus; store.checkBudgets on the tick opens one budget incident per level per day; on_exceed = "ask" makes evaluateTool ask on Bash/Edit/Write, "stop" halts spawned runs + dispatch queue; GET /v1/budget; Spend tile) and run permission profiles ✅ 2026-08-23 (RUN_PROFILES full / no-edits / read-only → --disallowedTools/--allowedTools; swarm run --profile, swarm dispatch --profile, [dispatch] profile, Run + Dispatch drawers), custom rule DSL in .swarm.toml ([[rules.custom]] match = … mode = …, path globs for writes; makes Codify M4.3 write a format the daemon reads; M2.x), secrets guard (ask on writes to .env* / *.pem and on Bash that prints them; M2.x), "while you were away" digest (M4.1 leftover), (profiles: tests-only became no-edits — Claude Code deny rules beat allow rules, so Bash can't be narrowed to test commands).
M8 — Teams (enterprise) ← direction set 2026-08-23
v0.8.0 cut (decided 2026-08-23): the trust release — M8.1 (✅), M8.2 complete (a: actor + schema versioning ✅, b: daemon token + auth, c: audit view/export + retention/redaction), M7.9 review-as-a-gate, plus the interface pass (row menus, Board kanban + worktree map, project settings). M7.6 messaging and M7.8 workflows move to 0.9; M8.3 team daemon starts after 0.8.
Goal: one view for a team, not a laptop — without giving up local-first. The local daemon stays the hook target (fast, fails open) and forwards to a self-hosted team daemon; policy gets an owner; the ledger gets an actor. Gap analysis that produced this list: multi-user (shared daemon, identity, RBAC, org/team/project hierarchy), governance (policy precedence, tamper detection, approval routing), audit & compliance (append-only log with actor, export/SIEM, retention/redaction), cost control (budgets, chargeback, model allow-lists), operations (fleet install, air-gapped pricing, migrations/backup, /metrics), and agent coverage (M5 adapters matter more for teams). Open-core boundary: OQ-15. Ordering: M8.1–M8.2 are free, pure core/daemon, and are prerequisites for anything shared; M8.3 is the paid milestone; M8.4 rides on it.
| ID | Task | Depends | Status |
|---|---|---|---|
| M8.1 | Policy precedence + tamper detection (free): org policy > repo .swarm.toml > user config, with org rules marked locked that user/repo config cannot relax; swarm doctor detects hooks removed from settings.json / rule modes downgraded and records an incident; critical rules evaluated from a signed local cache when the daemon is down (resolves OQ-3); folds the custom rule DSL + secrets guard follow-ups |
M2, M4.3 | ✅ 2026-08-23 — M8.1a 2026-08-23: org layer ~/.swarm/policy.toml / SWARM_POLICY, locked dotted keys, loadConfigDetailed() provenance + overridden (core/src/config.ts, docs/13); M8.1b 2026-08-23: core/src/policy.ts hook coverage + findings, doctor per-event/override lines, daemon checkPolicy on SessionStart → incident.opened {rule:"policy"}, SWARM_GUARD=off ignored under locked rules, GET /v1/policy; M8.1c: policy.cache.json (locked modes + snapshot, sha256) written by the daemon, hook shim evaluates locked rules offline (OQ-3 resolved) |
| M8.2 | Auth + actor + audit export (free): bearer token in daemon.json required for every non-loopback request (and optional on loopback); actor (human user / agent session / spawned run) on every ledger record — claims, gate runs, permission answers, incident acks; append-only audit view; swarm audit export --since --format jsonl + retention / redaction settings (transcript text, secrets on ingest) |
M1, M3.2 | ✅ 2026-08-23 — M8.2a 2026-08-23: core/src/actor.ts Actor {kind,id,session?}, actor on every event, actor_kind/actor_id on claims/resources/processes/handoffs/gates/incident_acks/sessions, versioned migrate() + meta.schema_version with v1 back-fill, /v1/health.schema; M8.2b 2026-08-23: ~/.swarm/token (daemon-created, 0600), [daemon] auth = loopback-optional|required, /v1/* middleware (Bearer / ?token= for SSE, wrong token always 401, non-loopback always required), authedFetch in client → CLI/MCP/hook, dashboard takes ?token= from swarm ui; M8.2c: core/src/audit.ts (audit types, built-in + [privacy] redact redaction, CSV/JSONL), store.audit(), GET /v1/audit, swarm audit export, [events]/[audit] retain_days retention split, store_prompts / store_reasoning |
| M8.3 | Team daemon (paid, separate package, self-hosted): [team] url = … in config makes the local daemon forward events, claims, gates and cost (never transcript text unless opted in) to a team daemon; cluster-wide claims and resource leases; OIDC login; viewer / developer / admin roles; org → team → project hierarchy; team dashboard reuses web |
M8.1, M8.2 | ⚪ |
| M8.4 | Budgets + chargeback: per-project / team / user $ ceilings → warn / ask rule / stop spawned runs; cost by team, task and ticket ID (M4.8 sources) exportable monthly; model allow-lists in policy |
M8.1, M8.3 | ⚪ |
| M8.5 | Fleet operations: non-interactive swarm install --config-url, pinned versions, signed single-file binaries with SBOM/provenance, Windows; offline mode with vendored pricing table; versioned SQLite migrations + swarm doctor --migrate, backup/restore of ~/.swarm; /metrics on the team daemon; incident → Slack / Jira / PagerDuty webhooks |
M8.3 | ⚪ |
Later (not scheduled)
Hosted (SaaS) team daemon wrapping the M8.3 binary · adapters for other agent CLIs · Linear/GitHub task sources · plan-gate-check (✅ unreachable without passing gate) as a rule.
Source: docs/06-roadmap.md · v0.8.0.