Multi-agent swarm with no one in charge
Independent agent roles coordinate through mailboxes and a single governance rule. No agent or person decides who acts next. This page replays a real run and recomputes every decision from its audit log.
Jump to controlsNo supervisor: the files decide who acts next
In the run replayed here, the roles built a six-milestone plan into another of my repos, with no supervisor assigning work. Each role is a fresh claude -p call that sees only the run objective, its own charter, the board and its own unread mail. Its answer must match a JSON schema. A small Python layer files the answer into folders and appends one line to the log. LangGraph then asks the router who can act, and starts all of them in parallel.
- Accent box: the router, the only place that decides who acts. No model call.
- Solid arrow: happens on every turn
- Dashed arrow: optional or after the fact (a human objection, observability)
Inside the swarm
Routing is a pure function of the files. route() reads the proposal folders and the append-only log, promotes what has cleared its objection window, and returns one Send per role that has a reason to act. It makes no model call and takes no human input, so the same files always give the same answer.
One objection sends an integrated proposal back to open
A proposal integrates (is accepted and handed to engineer to build) once every other role has finished an ok turn since it was filed and no objection is on file. engineer, the one role with write access, is left out of that count. An objection against an integrated proposal moves it back to open. The page recomputes all of it at every step, from the log alone.
Excerpts from a real run, trimmed and lightly edited for length. The model turns are recorded. The governance is computed here.
Proposals, derived from the log
| Proposal | State | Owes a turn | Objections |
|---|
Eligible to act next
These are the two triggers in activatable() that come from proposals and the log. It also wakes any role with unread mail. Mail recipients aren't fully recoverable from this run's files, so mail is left out here.
Audit log
A role sees its inbox and answers in a fixed schema
From packs/governance-pivot/config.json. The charter goes in as the system prompt. The command is what runtime/nodes.py builds from this config.
Charter excerpt, critic.md
Command
The structured_output field of the result, already parsed by the CLI. Long lines are wrapped for display. Below it, the run metadata from the same result.
The output has to pass two checks. Your browser checks it against the schema (schemas/contract.schema.json). Then file_result.py checks references against the folders as they stood at that point in the log.
Only "As recorded" is real output. The other buttons are edits you make to it here.
Nothing in this run failed the schema. These are the two real failure types it did log. Neither counts as a turn for any objection window, because the rule only counts status: "ok".
Hard failures handled in code
| Cause | Logged as |
|---|---|
No structured_output in the result | error: no structured_output in result (schema validation may have failed) |
role doesn't match the activated role | error: role mismatch (ValueError in file_result.py) |
| CLI reports a 429 | error, plus a marker file that makes the router stop the run |
| Subprocess runs past 900 s | error: claude -p timed out |
Where the money and the waiting went
Cost per role, USD
| Role | Turns | Cost | Per turn |
|---|
Objection matrix
Proposal lifecycle
| Proposal | End state | To integrate | To done |
|---|
How it works
Method
A run is a folder: mailboxes, tensions, proposals (open/, integrated/, done/, objections/) and an append-only log/events.ndjson. Roles come from a pack: a roster, one charter per role, and a config with tools, model and budget per role. A new use case needs a new pack and no framework code.
The router imports its rules unchanged from scripts/next.py. sweep_proposals promotes an open proposal when it has no objection and every role other than its filer and the write role has an ok log entry with a later timestamp. activatable lists who has a reason to act: unread mail, an open proposal they haven't had a turn on yet, or (write role only) an integrated proposal to build. LangGraph sends every one of them a turn at once and loops until nobody is left.
The comparison is strict string order on ISO timestamps, and only status: "ok" counts. A turn that finishes with warnings, or fails, does not clear anyone's window.
Production setup
- Each turn is
claude -pwith a per-role tool list,--max-budget-usdand--json-schema. Every role except research also runs under--restricted. The CLI returns the output already parsed, so nothing has to regex free text. - Permission scoping was tested against the CLI directly. Under
--restricted, writes in headless mode were silently denied until--permission-mode acceptEditswas added.--restrictedalso blocks web tools even when named, so the research role runs with--allowedToolsinstead.--allowedToolsalone did not restrict anything on that machine. - Every failed turn is logged, so the activation cap counts it. An earlier version only logged successes, and one run retried a rate-limited call thousands of times. A 429 now stops the run.
- A human is just another objector. The dashboard's Reject button writes the same objection file a role would.
Limits
- Cost. Every turn is a full model call. Many turns end with "nothing to do", and they still cost money.
- Latency. A proposal waits for every non-write role to finish a turn, including roles with nothing to say about it.
- Races. Parallel turns of the same role don't see each other. In this run, parallel architect turns filed duplicate proposals for four of the six milestones, and architect then objected to its own duplicates. Several engineers built the same milestone at once.
- Schema rigidity. The contract is fixed. A role can't express anything the schema has no field for, and an unknown id is dropped, not interpreted.
About this page
The log lines, costs and full turn are copied from the run folder. Run over the full log, the JavaScript port of sweep_proposals and activatable ends with every proposal in the same folder the real run left it in. Injected objections change what the rule computes, not what the agents said.