@goodman-b/pi-swarm
Multi-agent swarm for the pi coding agent: board-coordinated peers, an independent harvest verifier, and an optional sandboxed writer. pi extension
Package details
Install @goodman-b/pi-swarm from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@goodman-b/pi-swarm- Package
@goodman-b/pi-swarm- Version
0.4.5- Published
- Nov 5, 2026
- Downloads
- 724/mo · 614/wk
- Author
- goodman-be
- License
- MIT
- Types
- extension
- Size
- 367.5 KB
- Dependencies
- 0 dependencies · 3 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
Pi swarm
Board-coordinated peer swarm for Pi. You give a goal and a measurable definition of done; peers claim their own slices of the work on a shared append-only board; a separate harvest session that never participated re-derives the findings and writes the report. No daemon, no profiles, no workflow dependency — just the public Pi SDK and Python 3.
What this is
Most "multi-agent" tools are a dispatcher: you write the work items, it hands
each one to a worker, it collects the results. A swarm inverts that — nobody
assigns work. Peers are ephemeral equals with the same tools and
instructions: they read the board, announce and claim slices, attack each
other's claims with evidence, and post findings as they go. slices is
candidate vocabulary and a hint, not an assignment. The board is the only
shared state (append-only), and the harvest is an independent check, not
a consensus: a session that never participated re-derives findings from
source and rules on whether the goal was met. A cancelled run is not a lost
run — board, lane reports and transcripts survive on disk.
Quickstart
pi install npm:@goodman-b/pi-swarm
Alternatively: pi install git:github.com/goodman-b/pi-swarm or
pi install ./pi-swarm for a local checkout.
Requires Pi, Node ≥ 22.19 and Python 3 on POSIX. The optional writer also
requires Linux with working bwrap overlay support.
Then /reload in an idle Pi session, open /swarm new, and fill in the
form below. Fresh defaults are 4 read-only peers, the session's model and a
one-hour deadline per phase; existing settings may override them:
/swarm new
goal: "Review this repository's TypeScript sources for correctness problems."
done: "Every finding in REPORT.md cites file:line plus a code excerpt
re-verified from source, and at least one checked-and-clean area
is named."
Watch it on the dashboard, then read REPORT.md. Write a measurable done,
not a quota — "every finding cites file:line and I re-read it" is good;
"find 10 bugs" is not. A default run is source inspection only (no shell,
no network). Do not require peers to run npm test: they cannot execute
shell commands. The optional writer's verification gate is a separate step.
What you watch, and what it costs
From /swarm, select the current run to open its live dashboard:
Overview / Board / Peers / Report tabs
(Tab switches; f filters the board; Enter/c opens a peer's live
transcript; s steers a running peer; x cancels). Durable artifacts:
board.jsonl (full event log), lanes/*.md (one report per peer),
REPORT.md (harvest summary — or, without a harvest, a deterministic digest
that never claims the goal was met), and full SDK transcripts.
A swarm runs multiple model sessions: peers work concurrently up to
maxConcurrent, followed by a separate harvest. Cost depends on peer count,
turns and context size. Measured locally, a 4-peer
audit of a ~1,800-line package: 260k–460k tokens, 20–40 minutes,
per-peer spread 43k–130k — these are observations, not guarantees. Start
with 4 peers; adding peers increases cost without guaranteeing better coverage.
Commands and tools
| Command / tool | What it does |
|---|---|
swarm_start({goal, done, agents?, model?, effort?, runId?, toolkit?, tools?, apply?, reduceGate?, slices?}) |
Start a run in the background. Read-only unless apply: true adds one staged writer. Only use on an explicit swarm request. |
swarm_status({runId?}) |
Progress and transcript/artifact paths. |
swarm_steer({peer, message}) |
Live correction; peer names include harvest and reduce. |
swarm_cancel({}) |
Stop the current run. Artifacts survive. |
/swarm |
Home menu: current/latest run, new swarm, runs, settings. |
/swarm new |
Editable launch form: goal/DoD, peers, model, effort, toolkit, grants, candidate slices, writer toggle and gate field. Confirmation renders the resolved toolkit (grants + effects + excluded) before the first peer starts. |
/swarm <run> or /swarm status [run] |
Bordered live dashboard. |
/swarm board · /swarm peek peer-1 |
Board tab / select a peer. |
/swarm runs · /swarm all |
Session history; other sessions too. |
/swarm settings · /swarm cancel |
Validated settings / confirmed stop. |
swarm_start's tool description carries the usage rules: peers choose the work; measurable DoD; read-only means source inspection; no delegated subagents or role profiles in place of a swarm.
Toolkits
toolkit (arg or defaultToolkit setting) resolves the peer tool set:
minimal(default).read,grep,find,lsplusswarm_board— no shell, no network, no general writes (only scoped board events and that peer's lane artifact), enforced by the tool list the runtime hands the SDK, not by a prompt asking peers to behave.grants. Adds the named capabilities from yourgrantssetting (§ Grants); the registry ships empty, every capability is declared data.inherit. The parent session's tool set minus delegation/board tools, with shell class dropped (listed underexcludedin the launch preview) — an interactive parent hasbash, so refusing would make inherit unusable. A named shell-class grant still fails the launch, because that one is deliberate. Unresolvable toolkits fail with a named error; no silent fallback.
Granted/inherited tools are trusted host-process tools running in the
parent Node process. The exclusion lists (shell: bash/write/edit;
delegation/run-control: Agent, swarm_start, …) match names, not
effects — hooks, process-global state and internal shell calls are not
guaranteed away ("Inherited extension effects are undeclared.").
In a writer (reduce) session, read shows the staged overlay view, but
grep, find and ls show the original tree — use read or cat to
see staged state.
Budgets
peerMaxTurns(80) stops one peer spinning on its own slice — blind to queueing and turn cost.wallSeconds(3600) is a per-phase deadline — peer, harvest and reduce each get a fresh budget.0turns the wall off. Reaching the peer deadline stops peers but lets harvest run. If harvest cannot finish, a deterministic board digest remains. Local turns cost ~20–45 s, so size it for the queue:peers / maxConcurrentwaves × expected turns × per-turn latency.runTokenCap(1,000,000) is a stop trigger on aggregate peer usage, checked at message boundaries — not a hard whole-run cap: harvest and writer spend is extra, and in-flight work can overshoot the threshold.0turns it off; with both budgets0, onlypeerMaxTurnsstops a run. A cut-short run settlesabortedand names the unfinished peers, without reporting a verified goal as unmet.- Budgets are re-read at the start of each run (a
~/.pi/swarm.jsonedit needs no/reload);run.jsonrecords the limits actually in force.
The writer and its safety boundary
The writer is optional and needs explicit authorization: apply: true and a
reduceGate command, honoured only when the independent harvest verified the
goal — otherwise apply is skipped with a stated reason. Peers never get
the built-in bash tool; the writer session (called reduce in run
records) gets a sandboxed shell. Extension tools remain outside that sandbox.
Upgrade before using
applyon a published version. Up to and including 0.4.2 the gate was fail-open: Pi's bash tool resolves (it does not throw) on a non-zero exit, andrunGate()never inspected that flag — so a failing gate read as a pass and the staged tree was promoted anyway. 0.4.3 throws on any non-zero exit and discards staging, and 0.4.5 closes the other half: the gate runs underset -eo pipefail, so every statement of a multi-line gate must pass (writecmd || truewhere a step is allowed to fail). Read-only runs never touch the workspace and are not affected. If you ranapply: trueon 0.4.2 or earlier: open the run dir (~/.pi/swarm-boards/<session>/<run>/) and look forgate.txt. If it is there and ends inCommand exited with code N, the gate failed and the promoted diff should not have landed — re-check that diff against the workspace. Nogate.txtmeans the gate never resolved (it rejected, timed out, or never ran) and nothing was promoted.
Write the goal as a diagnosis, not a fix. A peer told to "fix it" discovers it
cannot write, posts blocked and releases its claim (seen live in sw-5ff5cc23);
one unfinished peer is enough for harvest to withhold verification, and apply is
then skipped and the workspace left untouched. The writer is the only session that
changes files, and it acts only on what the harvest verified.
bwrap with --unshare-all — network unshared, and deliberately no
--share-net path in the code — plus read-only /usr, fresh
/proc//dev/tmpfs /tmp, the workspace as an overlay with writes going to
a staging upper dir, --disable-userns where the kernel allows (else weaker,
said in the run record), --new-session --die-with-parent. No host /home is
bound in: the blast radius is the workspace. apply: true with no working
bwrap is refused, not downgraded. sandbox: "off" likewise refuses
apply; it does not change extension isolation — granted/inherited tools
run in-process anyway, outside any box.
What the box does not protect against
- Extension tools run host-side: a peer cannot call
bash, but a granted extension hook that shells out does so unsandboxed. - Peer reads are not filesystem-confidentiality sandboxed: they run with the host user's read access. The writer's shell sees the mounted workspace and system paths; secrets in the workspace remain readable.
- Promotion is per-file, not atomic. Staging → read-only gate →
promote()copies the diff in file by file; a failure mid-promote can leave the workspace partially changed — staging kept for inspection, no rollback. - The gate sees a read-only workspace. Commands that write build outputs
inside the workspace fail there; point output at
/tmp(a fresh tmpfs in the box) where suitable. No blanket guarantee that any command —npm testincluded — passes in the gate. - A grant's process-global state (a fetch cache) is shared across sessions in this process.
- Release status: the apply/reduce/gate/promote path is unit-tested, and one live
writer run is on record:
sw-f8dfb540(2026-11-04) settleddonewithmetGoal: truethrough reduce → gate → promote, promoting a staged one-line fix whosegate.txtheld the gate command's own output. That run used the canary checkout, not a published version; promotion to a real workspace is still lightly travelled, so treatapplyaccordingly.
Settings
~/.pi/swarm.json (editable via /swarm settings; grants is JSON-only,
§ Grants). Unknown/invalid fields drop to defaults; a broken grants block
drops only itself; an unreadable file runs on in-memory defaults and refuses
to save until fixed. SWARM_SETTINGS_PATH overrides the path.
| Key | Default |
|---|---|
defaultAgents / maxAgents |
4 / 16 — roster size vs. per-launch ceiling; no fixed 16 limit, accepted up to the roster's structural array-length maximum (2^32−1) |
defaultModel / defaultEffort |
(the calling session's model) / low |
peerMaxTurns / harvestMaxTurns / reduceMaxTurns / graceTurns |
80 / 30 / 30 / 3 |
maxConcurrent |
8 — peer sessions prompting at once; the rest queue |
wallSeconds |
3600 — per-phase deadline, 0 = off (§ Budgets) |
runTokenCap |
1000000 — aggregate peer-usage stop trigger (0 = off); harvest/writer spend excluded, in-flight turns may overshoot |
boardRoot |
~/.pi/swarm-boards |
widget / notifyOnSettle |
true / true |
refreshMs |
2000 |
defaultToolkit |
minimal (grants or inherit also allowed) |
defaultTools |
[] (grant names, validated against your grants registry; an unknown name refuses the whole list) |
grants |
{} — the grant registry. Fail closed: an absent, null or unparseable block resets it, so a revoked grant cannot survive into a later run |
sandbox |
auto (off refuses apply; granted/inherited tools run unboxed either way, with a warning) |
defaultAgents is the default roster; maxAgents is the operator's per-run
ceiling; maxConcurrent limits simultaneous peer prompts. There is no fixed
16-peer limit: roster settings accept integers ≥ 2 up to the structural array
length maximum (2^32−1, the largest JS array the peer roster can be); the
operator must keep counts practical — the roster is allocated upfront, so huge
values are a memory cost, not a scalability guarantee. Concurrency settings
accept safe integers ≥ 1; queueing is a limit on parallel prompts, not a memory
bound. Tune these to your provider's rate limits, latency, context size and
budget, and size wallSeconds for queued peers.
All run paths are <boardRoot>/<operator-session>/<run>/. run.json is
authoritative runtime state.
Model
explicit model arg → defaultModel setting → the calling session's
model; all phases use that one resolved model, and an unresolvable or
unauthenticated name fails launch validation, never a silent fallback.
Grants
A grant is a named capability — an extension file path plus the tool names it
exposes — declared in settings. The registry ships empty (a package must
not guess where another package got installed); delegation/run-control tools
cannot be exposed by name; shell-class names are refused on a read-only run;
existence is checked at launch; null removes an entry; names match
^[a-z][a-z0-9_-]{0,31}$, path absolute, tools non-empty:
{ "grants": { "web": { "path": "/home/me/somewhere/web-extension/index.ts",
"tools": ["web_search", "fetch_content"],
"effects": ["public-network"], "label": "Web" } } }
effects is declarative: stated to the peers, not an enforcement mechanism.
Requirements and tests
Node ≥ 22.19, Python 3 on PATH (the board binary is invoked with argv, never
through a shell), bwrap only for apply: true. npm test runs against
whatever Pi SDK it finds — Pi 1.5.0 or newer, since the suite pins that release's
argument-coercion contract and says so plainly on an older SDK — and prints it first as
pi SDK <version> — <path>, so
a green run always says which Pi it means (CI pins 1.5.0, the release this fleet runs); toolkit: "inherit"
additionally needs a Pi build exposing
getAllTools()/getCommands(), else a named launch error.
npm test — no network, no model calls; the runner finds the Pi SDK in
$PI_MANAGED_INSTALL_ROOT/releases/<current-version> (or ~/.pi/agent/install),
then pi on PATH, and PI_CLI=/path/to/releases/<v>/node_modules/.bin/pi npm test
forces one. Pass the SDK's own bin, not the pi launcher script. Fake sessions plus
the real Pi resource loader and the bundled Python board; covers registration,
settings, scoped tools, protocol failures, turn/wall caps, cancellation
including mid-create, steering, claim cleanup, reduce and gate failure, plus
UI checks (navigation, launch confirmation, validation, history, guarded
actions); grants use in-repo fixtures, so the suite runs without the
author's extensions installed. A live model smoke — a bounded swarm
against a real model (peers claim work, harvest settles, effects stay []
on a read-only run) — is the real proof of the SDK integration, separate
from npm test. The writer smoke runs the same way against a throwaway fixture
repo — a deliberately broken function plus a test that fails on it (the fleet keeps
one at ../applytest, outside this package) — driven headlessly so the run's cwd
is the fixture: cd <fixture> && pi --print --tools 'swarm_*,bash,read' with a
brief that calls swarm_start({agents: 2, apply: true, reduceGate: "node test.mjs"})
and polls swarm_status until it settles (the run belongs to the session that
started it, so that session must outlive it). Proof it worked: gate.txt in the run
dir holding the gate's own output, and a workspace diff of nothing but the intended
line. Recorded: sw-f8dfb540, 2026-11-04, ~100k tokens (2 peers + harvest + writer),
under 4 minutes.
Usage/cost is recorded in run.json, not merged into Pi's /cost;
compaction overhead may be missing. There is one active run per extension
instance, no cross-process admission control, and no automatic whole-peer
retry. For write and extension risks, see the safety boundary.
Credits
Built on Pi's public SDK. The following are design influences, not dependencies:
@tintinweb/pi-subagents— the swarm's first working form ran on top of it; none of its code is in this package.- tcclaviger (Rob) — suggested using bubblewrap (
bwrap) for the writer sandbox. - Blackboard architecture for LLM agent teams: arXiv 2507.01701, 2510.01285.
- Why Do Multi-Agent LLM Systems Fail? (MAST), arXiv 2503.13657 — the failure taxonomy behind the
done/blockedprotocol and the independent harvest. - Cognition, Don't Build Multi-Agents — peers exchange artifacts and traces, not conversation.
- IndyDevDan's Astra Swarm — the original spark; unrelated to this implementation.