Open questions
Status: living- OQ-1 Name / npm package. ✅ RESOLVED (2026-08-20): the project is
swarm. A swarm of agents is the product's mental model and fits the dashboard's Fleet view. Published scoped as@ra3orblade/swarm(the bareswarmnpm name is taken by Docker/Ethereum); CLI commandswarm, daemonswarmd, hookswarm-hook, MCPswarm-mcp. Barenpx swarmwould need the unscoped name — revisit only if we want it. - OQ-2 Hook install scope default. User-level (every session everywhere, matches "pick any folder") vs. project-level opt-in (less surprising for people trying it). Leaning user-level with
swarm install --projectas the alternative. - OQ-3 Fail-open vs fail-closed when the daemon is down. Proposal: fail open except for rules tagged
critical;criticalrules are evaluated locally in the shim from a cached rules file so they work without the daemon.Decision (2026-08-23, M8.1c): fail open by default; fail closed only for rules the org policy locks. The daemon keeps
~/.swarm/policy.cache.json(locked modes + live-session/held-worktree snapshot, sha256 integrity hash) while any rule is locked; the hook shim evaluates it with the sameguardBash/guardWritewhen the daemon is unreachable. Unlocked rules still fail open. Cryptographic signing of the cache waits for the team daemon's key distribution (M8.3). - OQ-4 Worktree location.
.worktrees/inside the repo (needs gitignore + test-runner exclusions) vs~/.swarm/worktrees/<project>/<task>(zero footprint, matches repo-agnostic). Leaning outside the repo by default,.swarm.tomlcan override.Decision (2026-08-20): default
~/.swarm/worktrees/<project>/<task>;.swarm.tomlworktrees =overrides. Zero footprint in the monitored repo and no vitest/gitignore exclusions needed. - OQ-5 Task source for M1.6. Markdown tables only, or also a
tasks.json? Keep markdown-table only until a second format is actually needed.Decision (2026-08-22): markdown tables only.
[tasks] sourcein.swarm.tomlnames one markdown file; everyID | Task | … | Statustable in it is parsed (✅ done / 🟡 active / ⚪ todo,Dependsmay name a task or a milestone prefix). A second format waits for a real request. - OQ-6 Web stack. Vite+React+shadcn (familiar, batteries-included) vs plain HTML/htmx over SSE (no build, smaller). Dashboard will get interactive (stdin, run control); React.
Decision (2026-08-27, M11): React + TypeScript, bundled by
Bun.build. Still no Vite. The vanilla dashboard reached 3,551 lines in oneapp.jswith 40 render functions, and its update model was one HTML string per view assigned to#main.innerHTML. Measured on Fleet, a single idle repaint destroyed and rebuilt 1,059 elements — 99<svg>and 27<img>— to change 11 text nodes. That is the visible flicker, and it forced three workarounds intorender(): hold the frame while a menu is open, save/restore the caret in filter inputs, skip the project list mid-drag.refresh()had grown to 21 hand-trackedxChangedbooleans compared byJSON.stringify. None of that is a bug to fix; it is the cost of hand-rolling reconciliation.React rather than Solid because the menus island is already React 19 and the toolchain already compiles TSX — this widens an existing seam instead of adding a second framework. No Vite because
Bun.buildalready produces the bundle, and a dev server buys nothing over the daemon's static serving. Views import their domain types straight from@swarm/core, so the API layer is typed by the same code that produces the responses.Style: Google's TypeScript style guide, enforced by Biome where a rule exists (named exports only, no
any, no non-null assertions,constby default) and by review where it does not. - OQ-7 Schema drift. How to pin tested Claude Code versions and detect new hook/stream event types —
doctorwarning plusrawpassthrough is the proposal. - OQ-8 Non-Claude agents. Out of scope for v1; keep the adapter seam but do not design for it.
- OQ-9 Memory search. ✅ RESOLVED (2026-08-22): SQLite FTS5 (BM25) first, no embedding model.
bun:sqliteships FTS5, so memory search needs no native extension, no model download and no network — it works on a fresh install, offline, and stays in the oneswarm.db. Indexed: handoffs, incidents (the stopped command + reason), gate runs (rubric + evidence) and what each session last said; never the monitored codebase. Queries are sanitised into MATCH expressions that cannot error (core/memory.ts). sqlite-vec + local embeddings stay on the table as a second ranker if keyword recall proves insufficient on real use — revisit with evidence, not upfront. Original note: Vector search over Swarm's own data only (handoffs, incidents, gate evidence, notes, denied commands), local embeddings via sqlite-vec, never over the monitored codebase (Claude Code greps better than RAG). Scheduled under M4; decide embedding model then.
Decision: FTS5/BM25 over Swarm's own data; revisit vectors only with evidence of missed recall.
OQ-10 First non-Claude agent. ✅ RESOLVED (2026-08-20): Codex CLI first (real
~/.codexrollout logs, closest shape to Claude Code), then Gemini CLI, Aider, opencode/Cline, and Grok — all behind the sameAgentAdapterinterface. Original note: Which agent gets the first adapter (M5.3)? Candidates with a machine-readable session/telemetry surface a local daemon can read: OpenAI Codex CLI (JSONL session/rollout logs), Gemini CLI, Aider (.aider.*.history), opencode, Cline. GUI tools (Cursor) are harder (no hooks; would need their local DB/logs). Decides M5.3–M5.5.OQ-11 Desktop packaging & signing. (Partly resolved 2026-08-21: reuse the existing Apple Developer account creds — same cert + notarization as the sibling Electron project — mapped to Tauri var names in
docs/11-desktop-signing.md; the Tauri updater keypair is new. Remaining: set the CI secrets and cut a real signed release; Windows signing later.) Tauri (M6): tray+window vs window-only; sidecar daemon (bun build --compile) vs embedded runtime; and signing — macOS notarization needs an Apple Developer account, Windows needs a code-signing cert. Update channel: single stable vs beta/stable. These gate M6.3–M6.4.OQ-12 Message delivery channel (M7.6). How does a message reach a running Claude Code session? Options: (a)
additionalContexton the next hook response — zero new surface, but only arrives when the agent next calls a tool andPreToolUsecontext is advisory; (b) the agent pollsswarm_inbox— reliable but only if the agent is told to poll (SessionStart context can say so); (c) stdin for spawned runs only (M3.1 already streams user messages). Leaning (a)+(b) together with (c) for spawned runs; interactive sessions cannot be interrupted, only informed. Decides M7.6–M7.7.Decision (2026-08-24, M7.6): (a)+(b)+(c) together. Messages are
kind = 'message'rows in the questions table; delivery isadditionalContexton the recipient's next hook (UserPromptSubmit / PreToolUse / PostToolUse), immediate stdin for a live spawned run on the target task, and pull viaswarm_inbox(which now returns answers and messages). Addressing: a session id (or unique prefix), a task (resolved to the session inside its held worktree, queued until one appears), or"lead"(the project's latest live interactive session). Interactive sessions are informed, never interrupted.OQ-13 Where do workflow steps and executed gates run (M7.4, M7.8)? Executed gates could run inside the daemon (simple, but the daemon is the only DB writer and should not block on a 10-minute test suite) or as a detached process in the registry (M1.4
proc, pid + start-time liveness, logs in~/.swarm/logs) with the result posted back. Leaning the process registry: gates and workflow steps are just registered processes with anon_exitthat records the gate run / advances the workflow; the daemon never blocks. Also: is a workflow's state a new table or derived from gate runs + runs per task? Leaning a smallworkflow_runstable (task, name, step index, status) — derivation gets ambiguous on retries.Decision (2026-08-23): executed gates (and later dispatch/workflow steps) run as detached processes in the registry (
kind: gate, pid + start-time liveness, log in~/.swarm/logs/<project>/gate-<task>-<gate>.log); the daemon records the gate run when the process exits and never blocks. Workflow state (0.8) gets its ownworkflow_runstable.OQ-14 Dispatch safety (M7.5). What stops a dispatched run from flipping a task ✅ on its own? Proposal: the run never edits the task source; "done" is derived (required gates pass + PR opened, M7.3) and shown on the Board, and only a human (or a workflow's final built-in step) writes status back. Also the concurrency cap: per project only, or global (machine-wide token spend)? Leaning per project with a global default in
~/.swarm/config.toml.Decision (2026-08-23): a dispatched run never edits the task source. "Done" on the Board is derived — required gates pass (executed or recorded) and a PR is open — and only a human flips status. Concurrency cap per project:
.swarm.toml [dispatch] max_parallel(default 2), global default in~/.swarm/config.toml.OQ-15 Commercial model / open-core boundary (M8). Swarm can become open-core without betraying local-first: everything that runs on one machine stays free and Apache-2.0 (daemon, dashboard, claims/worktrees/gates, rules, spawned runs, transcripts/cost, all agent adapters,
.swarm.tomlpolicies); everything that needs a second person is the paid tier (team daemon, SSO/OIDC, RBAC, org-level non-overridable policy, audit export / SIEM sinks, chargeback, fleet install, support). Constraints that must hold: (a) never relicense the existing packages — the team daemon lives in a separate package/repo under a source-available or closed license; (b) the free tier keeps "no telemetry, no account" — the account lives in the team daemon only; (c) self-hosted first — the buyers who chose Swarm because code never leaves the machine want the team daemon on their own infra (one binary, SQLite or Postgres); a hosted offering can wrap the same binary later if demand appears; (d) the free tier is never crippled to sell the paid one — M8.1/M8.2 below are free and land incore.Decision (2026-08-23): open-core on the single-machine / multi-person line; self-hosted team daemon first, hosted later; no relicensing of existing packages; free tier stays telemetry- and account-free. Pricing, license text and package split are open until M8.3 starts. Decision (2026-08-24, M8.3 start): the team daemon lives in this monorepo as
packages/team(@ra3orblade/swarm-team, binswarm-teamd) under FSL-1.1-ALv2 (the current SPDX id of the FSL "Apache-2.0 future" variant) — source-available, competing use restricted, each release converting to Apache-2.0 after two years; a per-package LICENSE.md overrides the repo Apache-2.0 for that directory only, andtools/build-pkg.tsexcludes it from the free bundle. Design in 14-teams. Pricing remains open.OQ-16 Daemon auth default (M8.2).
daemon.authisloopback-optionalin 0.x so existing installs, the dashboard and the Tauri app keep working without a token. Shouldrequiredbecome the default at 1.0, and how does the desktop app obtain the token (readdaemon.jsonfrom the shell, or a one-shot/v1/session-tokenon loopback)? See 14-teams.OQ-17 Local human identity before OIDC (M8.2).
actor.kind = "human"needs an id on a machine with no login: OS username (spoofable on shared boxes),git config user.email(what the forge already attributes commits to), or just "laptop owner". Proposal: OS username now, replaced by the OIDC subject in M8.3; record both when available. See 14-teams.OQ-18 Team daemon storage: when does Postgres land (M8.3)? v1 is SQLite (
~/.swarm/team.db, WAL, the versionedmigrate()pattern) behind theTeamStoreclass boundary — one binary, zero external services, which is what "self-hosted first" buyers asked for. Postgres becomes worth it when a deployment outgrows one node (HA, >~50 machines, SIEM-scale retention); the protocol (/t1/*) must not change when it does. Revisit with a real deployment that hurts, not upfront.OQ-19 Cluster project identity + shared machines (M8.3). Two identities cross machine boundaries: projects — proposal: the normalized
originremote URL (the forge parsing incore/forge.tsalready normalizes github/gitlab remotes) as the cluster key, path-hash fallback keeps unpushed repos local-only; and machines — a machine token is bound to the user who ranswarm login, so a shared build box re-binds on re-login and per-user attribution on shared machines relies on the local actor rows riding along in forwarded events. Good enough for v1? Decides M8.3b/M8.3d.OQ-20 OTLP schema (M12.1). Mirror Claude Code's beta trace names (
claude_code.interaction,llm_request,tool,tool.blocked_on_user) so Grafana's first-party dashboard works unchanged, or emit the OpenTelemetry GenAI semantic conventions (gen_ai.*,gen_ai.conversation.id= session) that W&B, Datadog and Arize read, withswarm.*attributes for claims, incidents and gates? Proposal: GenAI semconv for spans and metrics, plus Claude Code's span names as a[otel] compat = "claude-code"option, because Swarm exports six agents and only one of them is Claude Code.Decision (2026-09-18, M12.1): the proposal, with both vocabularies verified that day against their sources (open-telemetry/semantic-conventions-genai; code.claude.com monitoring-usage). GenAI by default: a trace per session keyed by
gen_ai.conversation.id,invoke_agent {agent}root,execute_tool {tool}children,gen_ai.client.token.usagebygen_ai.token.type(input includes cached input), cost asswarm.cost.usage.compat = "claude-code"switches toclaude_code.token.usage/claude_code.cost.usage/claude_code.tool/claude_code.tool.blocked_on_userwithsession.id; the root staysswarm.session, because Claude Code's own root is a per-prompt interaction Swarm does not reconstruct. Content (commands, paths) is opt-in, as in Claude Code. OTLP/HTTP JSON only — no SDK, no new dependency; validated against an OpenTelemetry Collector (0.161, debug exporter), not yet against Grafana Cloud.OQ-21 Sandbox engine (M12.6). Wrap spawned runs in Anthropic's
sandbox-runtime(srt, Apache-2.0, macOS Seatbelt / Linux bubblewrap, filtering proxy), pass Claude Code's ownsandboxsettings through for Claude runs only, or ship nothing and keep the guide's "hooks see the command, not the syscall" line? Proposal:srtas an optional dependency reported bydoctor, because it is agent-neutral and the same engine Claude Code ships; Claude Code's settings remain the answer for interactive sessions.Decision (2026-10-01, M12.6): the proposal.
srtis an optional dependency:swarm run --sandbox/[dispatch] sandboxwrap the spawned agent in it when it is installed, refuse with the install line when it is not, anddoctorreports it. Interactive sessions keep Claude Code's ownsandboxsettings; Swarm does not write them.OQ-22 Reviewer authority (M12.7). May a model decide an ask prompt? Codex (
auto_review), Cursor and Claude Code (auto mode) all do, and all say the classifier can be wrong. Proposal:adviseis the highest default anyone gets without opting in;decideis per repo, never for locked org rules, and every decision is an event with the reviewer's reason so rule effectiveness (M9.10) can score the reviewer like a rule.Decision (2026-10-01, M12.7): the proposal.
[broker] reviewerdefaults tooff;adviseis the most the user config can turn on for every repo;decideis only read from a repo's.swarm.toml, is never applied to a locked org rule, and every review is apermission.reviewedevent with the reviewer's reason so rule effectiveness (M9.10) scores the reviewer like a rule. A person can always overrule it.OQ-23 Quota source (M12.3). The subscription windows come from an OAuth usage endpoint the popular monitors poll with the user's Claude credentials, or from the statusline payload Claude Code hands to
statusLinecommands. Proposal: statusline only — Swarm has no account and never sends the user's token anywhere; if the payload lacks the windows on the installed version, the tile says so instead of estimating.Decision (2026-09-12, M12.3): statusline only. The windows are stored as samples when a session on a plan reports them and shown while it does; there is no tile at all when nothing reports (an API-key machine has no windows to show, and a stale number would be worse than none). Burn rate is computed inside the newest reset period only, so a reset never reads as a negative pace. Warnings are once per window per reset period, at
[budget] window_warn_atand at 100%, attributed to the project whose session last reported.OQ-24 How many times may Swarm refuse a stop (M13.1). A
Stophook that answersblockkeeps the agent working; an agent that cannot make the gate pass would loop forever, burning the budget the gate was meant to protect. Proposal:max_blocks = 3per session, then agate_failedincident and the stop goes through; never onSubagentStop(a subagent's parent decides), never without a held claim (no task, no required gates), andon_stop = "record"stays the default so nothing changes until a repo opts in. The count is per session, not per task, so a resumed session starts fresh.Decision (2026-09-12, M13.1): as proposed.
max_blocks = 3per session (0 disables), counted from the session'sgate.blockedevents so a daemon restart cannot reset it; onegate_failedincident per session when the refusals are used up;on_stop = "record"by default;SubagentStopis not routed through the decision at all; a Stop waits at moststop_timeout(300 s) for the gates and is let through when they outrun it (the runs still record).stop_hook_activeis not used as the bound — Claude Code sets it after one refusal, and one is too few for a fix-and-rerun.OQ-25 The card and the terminal prompt race (M13.2).
PermissionRequestlets a hook answer allow / deny, and the shim can wait on a person for up to the hook timeout. If Claude Code draws the terminal prompt only after the hook returns, a long wait reads as a stall to someone sitting at the keyboard; if it draws both, the card is a second answerer and the wait costs nothing. Proposal: verify which it is on the installed version first; defaultinteractive_wait = 30if the prompt is drawn concurrently,0(card is informational, terminal decides) if it is not, and in both cases asystemMessagenames the other place to answer.Decision (2026-09-12, M13.2): verified on 2.1.269: the dialog is not drawn while the hook runs, and a timeout or empty answer falls through to it unchanged. So the wait is real, and it is made safe two ways rather than by defaulting it off: the daemon waits only while a dashboard has polled or streamed in the last 45 s (nobody watching, nobody waits), and the card carries Answer in terminal plus a countdown. Default
interactive_wait = 30, capped at 120,0= off. The pass-through is recorded aspermission.resolved { by: "terminal" }at once, so the M9.4 waiting pairs measure the card, and the terminal's own wait keeps coming from Claude Code'sNotificationhook as before.OQ-26 Waking an idle session (M13.4).
asyncRewakehooks run in the background and wake Claude with their output when they exit 2, but the reference does not say whether a hook armed atSessionStartsurvives the turn boundary or must be re-armed atStop, nor whether the reminder reaches a session sitting idle at the prompt. Proposal: arm at both events, verify on 2.1.269 with a message sent to an idle session, and keep next-hook delivery (the OQ-12 decision) as the fallback the Fleet marks queued rather than delivered.Decision (2026-09-12, M13.4): the reference settles the semantics: an
asyncRewakecommand hook runs in the background, wakes the session from idle at the prompt when it exits 2 (stderr, else stdout, shown as a system reminder), itstimeoutis enforced (Claude Code cancels it), and nothing persists across turns — a fresh instance starts per event. So the waiter is armed onSessionStartandStop(the two moments an idle period begins), gives up at 55 min under a 3600 s timeout, and the daemon keeps one waiter per session, replacing older ones and cancelling on activity or end. Next-hook delivery stays as the mid-turn path and the fallback. The in-terminal wake is exercised by contract and at the shim level; a live idle-session run remains to be watched on a real terminal.OQ-27 Distributing the team daemon (M13.12).
packages/teamis FSL-1.1-ALv2 and excluded from the free npm bundle by design, which today means it runs only from a clone. Options: publish@ra3orblade/swarm-teamto npm as its own package (npm carries any SPDX license; the free package never depends on it), aghcr.io/ra3orblade/swarm-teamdimage built byrelease.ymlon the samev*tag, a per-platformswarm-teamdbinary attached to the GitHub release that the app downloads on demand, or all three. Proposal (revised 2026-09-12, after "why not in the interface of the app?"): all three, same version as the free package. The app hosts a team by fetching the release binary into~/.swarm/bin— so the free download stays Apache-only and the FSL binary arrives with its license on screen the moment someone chooses to host — and the image + npm package serve teams that start on a server. Bundling the FSL binary inside the free app is the one option ruled out: it would put source-available code in every Apache download whether or not a team is ever hosted.Status (2026-09-12, M13.12): the decision stands and is not executed yet. Hosting from the app shipped by resolving
swarm-teamdas a command (a clone, or PATH), which keeps FSL code out of the free bundle exactly as intended, but none of the three distribution channels exists: publishing a second npm package needs its own trusted-publisher rule on npmjs.com, and the image and the release binary needrelease.ymlchanges. Tracked as M13.13. Status (2026-09-18, M13.13): executed in the repo.bun run build:team(tools/build-team.ts) buildsnpm-team/— the@ra3orblade/swarm-teambundle and, with--binaries, five compiledswarm-teamd-<os>-<arch>binaries plusswarm-teamd-SHA256SUMS; the team page's assets are embedded because a binary has nothing beside it.packages/team/Dockerfileis theghcr.io/ra3orblade/swarm-teamdimage (state on/data).release.ymljobteampublishes npm + ghcr on eachv*tag andteam-release-assetsattaches the binaries to the draft release. The app downloads the binary only after the license is shown and accepted (POST /v1/team/teamd {accept: true}), checksum-verified, into~/.swarm/bin. Two one-time steps remain outside the repo (until the first is done, only the npm step of theteamjob fails — it runs last, so the image and binaries still ship): the firstnpm publishof the new package by hand (npm's trusted publishing can only be configured for a package that exists) followed by its trusted-publisher rule, and making the ghcr package public after its first push.OQ-28 Claiming Claude Code's own worktrees (M13.7). The roadmap planned
WorktreeCreate/WorktreeRemovehooks that register Claude Code's worktrees as claims. The hooks reference (verified 2026-09-18) says a configuredWorktreeCreatehook replaces Claude Code's defaultgit worktreecreation: the hook must create the directory and print its path, any failure aborts the creation, and.worktreeincludeis no longer processed. Installed user-wide that would put Swarm in charge of everyclaude --worktree, every subagent withisolation: "worktree"and every background session — and a shim that must fail open cannot also be the thing that has to succeed for a worktree to exist. WhetherWorktreeRemovefires at all for plain git worktrees without a create hook is not stated. Options: take over creation in the shim (no daemon needed), or observe.Decision (2026-09-18): observe. No worktree hook is installed. The first hook from a session whose cwd is inside
<repo>/.claude/worktrees/<name>(a real git worktree) claims it ascc/<name>, ownersession:<id>,claims.origin = "claude-code"; the usual auto-renew keeps it,SessionEndor a lapsed lease closes it, and a lapsed adoption is taken over by the next session there. Release, reap and gc never remove an adopted worktree — Claude Code decides whether it stays. The refuse-a-dirty-removal half is dropped: Claude Code already keeps a subagent worktree that has changes and asks before removing a--worktreesession's.OQ-29 An
askrule on an agent that cannot ask (M12.4). Codex'sPreToolUseparsespermissionDecision: "ask"but does not support it yet, Gemini CLI'sBeforeToolonly allows or denies, and Cursor'spreToolUseacceptsaskwithout enforcing it (all verified 2026-09-18). A rule inaskmode — five of the defaults are — has to become either allow or deny there.Decision (2026-09-18): deny, with the rule's reason plus "Swarm would ask the user here;
's hooks cannot ask, so the call is refused — ask the user to run it, or to turn the rule off." Allowing would make the rule a no-op on exactly the agents M12.4 exists for; the reasons already tell the agent what to do instead (stage explicit paths, kill by pid), so it can usually recover without the person. A rewriteis refused the same way, with the fixed command offered. Revisit when an agent's hook grows a realask—effectiveDecisionincore/src/agenthooks.tsis the one place to change.OQ-30 What a scheduled workflow may do on its own (M13.10). A
[[schedules]]entry starts a workflow on a cron with nobody at the keyboard. Itsreview-prsstep would post on GitHub PRs under the user's account, and itsgatesstep was planned to run "on the main checkout's branch", where someone may be working. Projects are also discovered from any session on the machine, so a cloned repo's.swarm.tomlcould schedule work by itself. Options for posting: never post, post a plain comment when the schedule opts in, or post approve / request-changes verdicts. Options for gates: the main checkout when it is clean and idle, or a scratch worktree.Decision (2026-10-01, M13.10):
- Posting is a plain
COMMENTreview, and only when the schedule setspost = true. It never approves or requests changes. It is posted once per PR head (aswarm-review:<sha>marker is checked first), and only on PRs from the repo itself: forks are never reviewed at all. gatesruns in a detached scratch worktree under~/.swarm/scratchat the default branch's head (origin/HEAD). The result is recorded as<branch>@<sha7>and the worktree is removed afterwards. The main checkout is never touched.- Arming. A schedule from
.swarm.tomlstays inert until armed (swarm schedule arm). Arming pins a hash of the definition, so any edit (cron, workflow, task orpost) disarms it.swarm schedule runstarts one by hand without arming, because a person asked.
- Posting is a plain
Source: docs/07-open-questions.md · v0.16.0.