Route bounded coding work across whichever installed agent CLI has quota to spare.
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.
analyze, build, and
chore describe what kind of work an action is; pools declare what they can
do, and the runtime picks among capable pools at dispatch time — there is no
fixed lane-to-pool table.INCUMBENCY_MARGIN, 10
points) — this stops the router from flapping between two pools of similar
pace.penalty when the floor set it and carries its measured basis
(history, bootstrap) when the projection was larger. An incumbent
carrying more in-flight work than a challenger keeps neither its 10-point
margin nor its cost guard, so a burst of parallel actions spreads across
pools instead of stacking on the single most-behind one.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).
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.)
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.