Swarm v0.4.0
The enforcement release: rules that watch file writes, not just Bash; a backlog Swarm can read; servers Swarm starts and stops by pid; and an Incidents feed you can clear.
Added
- Rules on file writes — two new rules evaluated on
Write/Edit/MultiEdit/NotebookEditpaths (and Bash working directories), not only Bash commands.no_foreign_worktree(defaultask) stops a session from editing inside a worktree another claim holds — never touch a worktree you don't hold is now a hook decision, with holding inferred from the session's cwd.claim_required_to_write(opt-in) makes a repo's shared checkout read-only without a claim: claim a task, get a worktree, write there. Both per repo asask | deny | off. - Incidents view — the denied-action feed as its own tab: Open / All, per-rule counts, reason and session per row, Ack and Ack all; the open count sits in the nav.
GET /v1/incidents?open=1&project=,POST /v1/incidents/:seq/ack,POST /v1/incidents/ack,/v1/state.openIncidents. The Board keeps a short open-only section. - Task source —
.swarm.toml[tasks] source = "docs/plan.md"points at a markdown file whoseID | Task | Depends | Statustables are the backlog (✅ / 🟡 / ⚪, dependencies by task id or milestone prefix). The Board gets a Tasks section (Ready / Open / All, Claim per row), the CLIswarm tasks [--ready], and agentsswarm_next_task— the first unclaimed task whose dependencies are done. Swarm's own roadmap is its task source. Markdown only (OQ-5 decided). swarm serve/swarm proc—swarm serve start --name web -- npm run devallocates a free port (ledger + bind probe), runs the command detached withPORTset and logs under~/.swarm/logs/<project>/, registers pid + start time, and acquires the singleton — so a secondwebfails closed and the port is protected for every other session with no config.serve ls | stop [name|pid],proc start | ls | stopfor workers without a port. Stop signals registry pids only, verified by start time; nothing is ever killed by pattern.POST /v1/ports/allocate,GET/POST/DELETE /v1/processes; Processes section on the Board with Stop.- Star nudge — once a month at most, never on first open, the dashboard asks for a GitHub star. Later snoozes 30 days, Don't ask again is final; localStorage only.
- Sidebar drag-and-drop — pinned projects reorder by dragging; the order persists on the daemon (
PUT /v1/projects/order,Project.order). - Desktop app menu — a real application menu (Swarm / Edit / View / Window): ⌘C/⌘V work, View › Zoom In / Zoom Out / Actual Size (
⌘+⌘−⌘0) scale the dashboard (persisted), plus Reload and Full Screen.
Fixed
- Shared-tree rules no longer lose sight of a session mid-turn.
shared_tree/destructive_gitkeyed on a 2-minute last-seen window fed only by hooks, so a neighbour three minutes into a long turn became invisible — and its uncommitted work unguarded. Transcript growth now counts as activity (the tailer bumpslast_seen_at), and the liveness window is the daemon's 10-minute idle threshold (LIVE_WINDOW_MS). A false positive costs one confirmation; a false negative cost someone's work. destructive_gitalso matchesgit stash drop,git stash clearandgit branch -D.- Dashboard type scale is one step larger across the board (base 13 → 14 px; the smallest labels 10 → 11 px) — it had drifted too small, especially in the desktop app.
- The PRs tab icon (and the branch/commit glyphs) were near-invisible at 15 px; they use the pixelarticons sharp variants now.
- The daemon dot stayed red for up to 15 s after load on a healthy connection (the SSE stream sent nothing until its first heartbeat); the stream now flushes immediately.
- The nav flashed "Fleet" before the restored tab was applied; session-detail event kinds (
userpromptsubmit) no longer overflow into the message column; "Unpinned · seen, not pinned" keeps its spacing.
Changed — docs
- README and site now say what the code does: Codex CLI and Grok sessions are tailed alongside Claude Code; the requirements and architecture diagram list all three.
- Rules are described as guardrails against accidents, not a sandbox — the guide has a new "What rules are — and aren't" section spelling out that a denied Bash command can be routed around (script, heredoc, direct file edit), and that worktree isolation via claims is the real fix. The site's feature cards lead with claims, rules and resources instead of Fleet and Spend.
Changed — performance
- Daemon never spawns
giton a request. Worktree status (git worktree list+status/rev-listper worktree, ~0.8 s across a fleet) moves to a 15 s background refresh with asyncBun.spawn;/v1/stateserves the cache (612 ms → ~15 ms). Claim/release invalidate it. - Hook round-trips are two indexed statements, not two
git rev-parsespawns plus a transcript-directory scan:cwd → projectis cached 60 s, the inline transcript tail is debounced to once per 2 s per session (the 5 s tailer covers steady state), and subagent directories are re-listed only when their mtime moves. - Events store ~2 KB, not ~10 KB.
tool_inputis clipped at 2 KB andtool_responseat 4 KB inpayload({truncated, bytes, preview}), and the tool I/O is no longer duplicated inraw. Existing databases are rewritten once on boot andVACUUMed (96 MB → 27 MB here). Retention: events older than 30 days are pruned daily (incidents kept),rawis cleared after 7 days. - Wire shape. SSE frames,
GET /v1/eventsreplays andGET /v1/sessions/:id/eventscarryseq/ts/type/projectId/sessionId/payload{hook,summary,…}only — noraw, no tool I/O (a 5.5 MB session fetch is now ~150 KB).GET /v1/events/:seqreturns one stored event in full;?full=1on the SSE replay does the same.?since=0replays the last 200 events, not the table. - Incremental session view —
GET /v1/sessions/:id/events?after=<seq>&afterTs=<iso>; the dashboard appends instead of re-fetching 500 events + 500 turns on every hook. - Dashboard render loop — one
requestAnimationFramescheduler, snapshotseqshort-circuit, paused while the tab is hidden, exponential SSE reconnect backoff; session log merges two sorted lists and caches rendered rows; data-grid memoises persisted layout and uses oneIntl.Collator; charts memoise the turn strip. - SQLite: indexes on
events(type, seq)andturns(ts),mmap_size256 MB, cached prepared statements (db.query),sessions/spend/incidentsmemoised per write generation;/v1/spendis its own endpoint. - Background tick: Codex/Grok discovery every 15 s when idle; Grok
summary.jsonre-read only on mtime change.
Install: bunx @ra3orblade/swarm setup · Docs: https://getswarm.vercel.app/docs/ · Changelog: https://getswarm.vercel.app/changelog