Bullswarm

Route bounded coding work across whichever installed agent CLI has quota to spare.

View the Project on GitHub cowcow02/bullswarm

This page gathers every routing rule that decides which pool runs a given piece of work — pace, 5-hour headroom, expiring-soon urgency, in-flight load, and quarantine — in one place. A pool is one installed agent CLI, or one account of that CLI. A lane is the nature of the work (analyze, build, or chore). The source of truth is the numbered doctrine comment at the top of src/lib/route.js; this page restates it in prose.

The rules

How dispatch shows its work

An assignment from bullswarm strategy assign is only a preference: quarantine, exhaustion, burst gates, 5-hour headroom, and capability checks still win. Routing prefers pools that are not near-limit over pools that are, ahead of pace, an approved assignment, and incumbency. A pool is near-limit when its 5-hour forecast is at or above FIVE_HOUR_NEAR_LIMIT_PCT (75) and ahead of that window’s elapsed share. A near-limit pool is still picked when it is the only eligible one, and a pool with no 5-hour reading counts as having headroom. The routing reason and every candidate row name the utilization that decided the pick, and meters and quarantines are re-read before each dispatch — and again, live, right after a usage limit — so a long run never routes off the snapshot it launched with.

Those thresholds apply to the forecast, not the last reading: a pool projected at or above 75% and ahead of the window’s elapsed share drops to the near-limit tier even while its reading is lower, and one projected at or above BURST_BLOCK_PCT (90) is left out of selection entirely as forecast-gated. The 90% burst gate ignores the clock.

bullswarm pools shows each pool’s inflight=<n> count and its 5-hour column as 5h=<reading>%-><projected>% whenever in-flight work is expected to move it, bullswarm assignments lists what those agents are, bullswarm run --dry-run prints the forecast the pick was made on without registering anything, and every candidate row carries pace, effectiveSurplus, inflight, projectedFiveHourPct, forecastFiveHourPct, ratePerMinute, estimateSource and forecastGated, so a surprising pick can be read back number by number.

The rates behind pace and forecasting come from real records: every live meter reading is retained as a capped per-pool series (~/.bullswarm/meters/history/<pool>.jsonl) and paired with the worker-minutes dispatched between readings. Until at least five worker-minutes of dispatch are attributable to a window there is no rate at all — null, not a ratio of percentage points to seconds — so a fresh machine routes on pace and the flat penalty until it has measured something. That penalty is config.inflightPenaltyPct in ~/.bullswarm/state.json (default 3; 0 turns the tie-breaker off).

Quarantine on a usage limit

A provider that reports a usage limit — You've hit your session limit · resets 8:20pm (Asia/Hong_Kong), usage_credits_required, rate limit exceeded, quota exceeded — is its own mechanical failure kind, quota, never process, semantic, or auth. The attempt is killed immediately even if the CLI would otherwise hang, and the pool is quarantined until the reset time parsed from the message, falling back to that pool’s cached 5-hour resets_at and then to 30 minutes. The quarantine record carries kind: 'quota' and excludes the pool from every later dispatch, in this run and in others, until it expires; the action is immediately re-dispatched on another pool with quota and never retried on the one that hit the limit. An agent report that merely discusses usage limits, or tool output that quotes them, is not a limit: detection is shape-gated to lines that look like a provider notice. (bullswarm workflow watch surfaces this live — see Dashboard.)

Quarantine on an upstream auth failure

A relayed credential fails upstream, not in the CLI. When a provider’s event stream reports an error whose payload names an upstream auth phrase — auth_unavailable, authentication_error, invalidated oauth token, no available channel for model, plus anything the connector declares in authSignatures — the verdict is the auth kind with a quarantine hint, not the generic provider kind, and the pool is benched for the flat 10-minute re-probe window. Those phrases are read only on a stream that already declared a failure, and only on an error-shaped line or on the raw JSON error event itself, so an agent that merely reads or discusses auth code is still never quarantined.

Some pools are several names for one credential. A connector may declare an upstreamGroup, and every pool sharing that string is benched together on the same deadline when one of them hits an upstream auth failure — the quarantine reason reads sibling of <pool>: <why>. The three Relay pools (opencode2, opencode2:relay-2, opencode2:relay-3) get relay:relay.example from src/lib/opencode-relay.js: they relay through one host, which relays through one pool of OAuth accounts. On 2026-09-11 that account was invalidated at 12:24 UTC and the retry of a failed action walked from one of those names to the next, burning both attempts on the same dead credential and blocking the dependent actions. Claude account pools deliberately have no group — each home is its own subscription, and one seat’s auth failure says nothing about the next. A quota quarantine never spreads either: a sibling’s window is its own, and an existing quarantine with a later deadline is never shortened by a borrowed one.