| title | Dispatch (Sling) |
|---|
Last verified against code: 2026-04-25
Dispatch is Gas City's work routing mechanism -- a Layer 2-4 derived
mechanism that composes primitives (Session, Bead Store, Event Bus,
Config) to route work to agents. The gc sling command resolves a target
agent or pool, optionally instantiates a formula as a wisp, executes the
agent's sling query to route each bead, optionally wraps single beads in
a tracking convoy, records telemetry, and nudges the target. Convoys are
expanded to their open children before routing.
-
Sling: The act of routing a bead to an agent or pool by executing the target's sling query. The sling query is a shell command template with
{}as a placeholder for the bead ID. Implemented incmd/gc/cmd_sling.go. -
Sling Query: A shell command template on each agent config (
sling_query) that routes a bead to that agent. Defaults tobd update {} --assignee=<qualified-name>for fixed agents andbd update {} --label=pool:<qualified-name>for pool agents. The{}placeholder is replaced with the actual bead ID at dispatch time. Defined ininternal/config/config.go:EffectiveSlingQuery. -
Container Expansion: When a convoy is slung, dispatch expands it to its open children and routes each child individually. Non-open children are skipped. Epics are ordinary beads and are not expanded. The container itself becomes the convoy -- no auto-convoy is created.
-
Auto-Convoy: When slinging a single bead (not a formula, not a container), dispatch automatically wraps it in a new convoy bead for batch tracking. Suppressed with
--no-convoy. -
Wisp Instantiation: When
--formulais set, dispatch creates an ephemeral molecule (wisp) from the named formula viaStore.MolCookand routes the wisp's root bead to the target. Variable substitution and custom titles are supported. -
Target Resolution: The 2-step resolution of agent names -- first literal match against qualified names, then contextual match using the current rig directory. Implemented in
cmd/gc/cmd_agent.go:resolveAgentIdentity. -
System Formula: A formula embedded in the
gcbinary that is materialized to.gc/system-formulas/at startup. System formulas are always overwritten to stay in sync with the binary version. Stale files are cleaned up. Implemented incmd/gc/system_formulas.go. -
Review Quorum Formula:
mol-review-quorumis a core packgraph.v2formula that dispatches exactly two read-only reviewer lanes using formula-supplied lane IDs, providers, models, and targets, and then routes a synthesis step. Dispatch treats it like any other formula-backed wisp; it does not givedx-reviewlifecycle ownership.
Dispatch is not a separate Go package. It is a composition of primitives
orchestrated by cmd/gc/cmd_sling.go. The dispatch pipeline has three
layers:
CLI layer (cmd/gc/cmd_sling.go)
|
+-- cmdSling() resolve city, config, agent, store
| |
| v
+-- doSlingBatch() container expansion (convoy -> children)
|
+-- doSling() single-bead dispatch pipeline:
|
+-- instantiateWisp() [if --formula] MolCook
+-- checkBeadState() [pre-flight] warn re-route
+-- buildSlingCommand() {} -> bead ID substitution
+-- runner(slingCmd) execute shell command
+-- telemetry.RecordSling()
+-- store.SetMetadata() [if --merge] merge strategy
+-- store.Create(convoy) [if auto-convoy] tracking wrapper
+-- doSlingNudge() [if --nudge] wake the agent
Single bead dispatch (gc sling <agent> <bead-id>):
cmdSlingresolves the city path, loads config, and resolves the target agent viaresolveAgentIdentity.doSlingBatchchecks if the bead is a container type. If not, falls through todoSling.doSlingwarns about suspended agents or empty pools (unless--force).- If
--formula, callsinstantiateWispwhich delegates toStore.MolCookto create the wisp and uses the root bead ID. - Pre-flight: warns if the bead already has an assignee or pool labels
(unless
--force). - Builds the sling command by replacing
{}in the agent'sEffectiveSlingQuery()with the bead ID. - Executes the sling command via
SlingRunner(shellsh -c). - Records telemetry via
telemetry.RecordSling. - If
--mergeis set, writes the merge strategy as bead metadata. - If auto-convoy is enabled (not
--no-convoy, not--formula), creates a convoy bead and sets the routed bead's ParentID to the convoy. - If
--nudge, sends a nudge to the target agent.
Container expansion (gc sling <agent> <convoy-id>):
doSlingBatchlooks up the bead and checksIsContainerType.- Lists all children via
querier.Children. - Partitions children into open (routable) and skipped (non-open).
- Routes each open child individually through
buildSlingCommand+runner. No auto-convoy is created -- the container IS the convoy. - Reports per-child success/failure and a summary line.
- Nudges once after all children are routed (if
--nudgeand at least one succeeded).
-
SlingOpts(cmd/gc/cmd_sling.go) -- All flags for the sling command:IsFormula,DoNudge,Force,Title,Vars,Merge,NoConvoy,Owned. -
SlingRunner(cmd/gc/cmd_sling.go) -- Function typefunc(command string) (string, error)that executes the sling shell command. Injectable for testing. -
BeadQuerier(cmd/gc/cmd_sling.go) -- Interface for retrieving a single bead by ID. Used for pre-flight checks. -
BeadChildQuerier(cmd/gc/cmd_sling.go) -- ExtendsBeadQuerierwithChildren(parentID)for container expansion. -
config.Agent(internal/config/config.go) -- Carries theSlingQueryfield andEffectiveSlingQuery()method that determines how beads are routed to this agent.
-
Sling query placeholder is always
{}. ThebuildSlingCommandfunction performs literal string replacement of all{}occurrences with the bead ID. No other placeholder syntax is supported. -
Container expansion routes only open children. Children with status other than
"open"are skipped and reported, never routed. -
Auto-convoy is suppressed for formulas and containers. When
--formulais set or the target bead is a container type, no auto-convoy is created. Formulas have their own molecule structure; containers are their own convoy. -
--ownedrequires a convoy. The--ownedand--no-convoyflags are mutually exclusive. The CLI rejects the combination before dispatch begins. -
Merge strategy is one of three values.
--mergeaccepts only"direct","mr", or"local". The CLI validates before dispatch. -
Pre-flight warnings are best-effort. If the bead store query fails, dispatch proceeds silently. Warnings never block routing.
-
Telemetry records every dispatch attempt.
RecordSlingis called on both success and failure paths with the target name, target type ("agent"or"pool"), method ("bead","formula", or"batch"), and error status. -
Pool nudge targets the first running instance. When nudging a pool, dispatch iterates pool instances in order and nudges the first one with a running session. If none are running, a warning is emitted.
-
System formulas are idempotent.
MaterializeSystemFormulasalways overwrites files to match the binary version and removes stale formula files that are no longer embedded. Non-formula files in the directory are left alone. -
Default sling queries differ by agent type. Fixed agents default to
bd update {} --assignee=<name>; pool agents default tobd update {} --label=pool:<name>. Customsling_queryoverrides the default entirely.
| Depends on | How |
|---|---|
internal/beads (Store) |
MolCook for wisp instantiation, Create for auto-convoy, Get/Children for container expansion, Update for ParentID linking, SetMetadata for merge strategy |
internal/config |
Agent resolution, EffectiveSlingQuery, pool detection via IsPool, PoolConfig for sizing, Suspended flag |
internal/runtime |
Provider.IsRunning and Provider.Nudge for agent nudging via doSlingNudge |
internal/agent |
SessionNameFor to compute session names |
internal/worker |
Handle.Nudge at the worker boundary for direct nudge delivery |
internal/telemetry |
RecordSling for metrics and log events on every dispatch |
cmd/gc/cmd_agent.go |
resolveAgentIdentity for 2-step target resolution (literal then contextual) |
| Depended on by | How |
|---|---|
cmd/gc/cmd_convoy.go |
Convoys are the batch tracking containers that dispatch creates and expands |
internal/orders |
Order dispatch creates wisps and routes them through the same formula instantiation path (Store.MolCook) |
cmd/gc/cmd_handoff.go |
Work handoff between agents uses similar agent resolution and bead routing patterns |
| Controller | The controller's reconciliation loop drives pool sizing via evaluatePool which determines how many pool instances exist to receive slung work |
| Path | Description |
|---|---|
cmd/gc/cmd_sling.go |
CLI command, SlingOpts, doSling, doSlingBatch, buildSlingCommand, instantiateWisp, checkBeadState, doSlingNudge |
cmd/gc/cmd_sling_test.go |
Unit tests: command building, single-bead dispatch, formula dispatch, container expansion, nudge behavior, merge strategy, auto-convoy, pre-flight warnings |
cmd/gc/cmd_convoy.go |
Convoy CRUD: create, list, status, add, close, check (auto-close), stranded, autoclose (hidden hook) |
cmd/gc/system_formulas.go |
MaterializeSystemFormulas, ListEmbeddedSystemFormulas, stale file cleanup |
cmd/gc/system_formulas_test.go |
Tests for materialization: empty FS, write, overwrite, stale cleanup, idempotency, orders |
cmd/gc/pool.go |
evaluatePool (scale check), poolAgents (instance expansion), expandSessionSetup (template context) |
internal/config/config.go |
Agent.SlingQuery, Agent.EffectiveSlingQuery(), Agent.EffectiveWorkQuery(), Agent.IsPool() |
internal/beads/beads.go |
IsContainerType, Store.MolCook, Store.Children, Store.SetMetadata |
internal/beads/bdstore.go |
BdStore.MolCook and BdStore.MolCookOn -- formula-backed wisp instantiation via bd mol wisp / bd mol bond |
internal/telemetry/recorder.go |
RecordSling -- metrics counter + structured log event for each dispatch |
cmd/gc/cmd_agent.go |
resolveAgentIdentity -- 2-step agent name resolution |
The dispatch mechanism is configured through agent-level fields in
city.toml:
[[agent]]
name = "worker"
# Custom sling query (optional -- has sensible defaults).
# {} is replaced with the bead ID at dispatch time.
sling_query = "bd update {} --assignee=worker"
# Custom work query (the read side of dispatch).
# Pool agents must set both sling_query and work_query, or neither.
work_query = "bd ready --assignee=worker --limit=1"
# Nudge text sent to wake the agent after routing.
nudge = "Work slung. Check your hook."Pool agents with default queries:
[[agent]]
name = "coder"
pool = { min = 1, max = 3, check = "echo 2" }
# Default sling_query: bd update {} --set-metadata gc.routed_to=coder
# Default work_query: bd ready --metadata-field gc.routed_to=coder --unassigned --limit=1System formulas are embedded in the gc binary and materialized to
.gc/system-formulas/ at startup. They form the lowest-priority formula
layer (Layer 0) in the formula resolution stack. Pack and city-level
formulas override system formulas by name.
mol-review-quorum is provided by the core pack formula layer. Its reviewer
lane IDs, providers, models, and dispatch targets are all supplied through
formula vars; the synthesis target is configured separately with
synthesis_target. Each reviewer lane is expected to produce durable structured
output with verdict, findings, evidence, usage, failure classification, and
read-only mutation-baseline delta.
The synthesis step writes the combined review-quorum.summary.v1 state for
future consumers such as dx-review summarize. The internal/reviewquorum
Go finalizer defines the durable contract but is not invoked by formula
synthesis yet.
Read-only mutation checks are baseline-relative. Dispatch and review consumers must compare reviewer after-state to the reviewer-recorded before baseline, not to an absolute clean-worktree expectation; pre-existing untracked files are not reviewer-created mutations.
Dispatch testing follows the philosophy in TESTING.md, relying heavily on injected fakes:
Unit tests (cmd/gc/cmd_sling_test.go): All dispatch logic is tested
through doSling and doSlingBatch with injected fakeRunner (records
shell commands), session.NewFake() (fake session provider), and
beads.NewMemStore() (in-memory bead store). Tests cover:
buildSlingCommandplaceholder substitution including multiple{}- Single-bead dispatch to fixed agents and pools
- Formula dispatch with
--formulaflag (wisp instantiation) - Container expansion: convoy beads expand to open children; epics are rejected
- Merge strategy metadata (
--merge=direct,--merge=mr,--merge=local) - Auto-convoy creation and suppression (
--no-convoy) - Owned convoy labeling (
--owned) - Pre-flight warnings for already-assigned beads and pool-labeled beads
- Suspended agent and empty pool warnings
- Nudge delivery to fixed agents and first running pool member
- Error paths: runner failure, MolCook failure, store failure
System formula tests (cmd/gc/system_formulas_test.go): Cover
materialization from embedded FS including empty FS (no-op), file
writing, overwrite semantics, stale file cleanup, idempotency, and
order subdirectory support.
Config tests (internal/config/config_test.go):
TestEffectiveSlingQuery* tests verify default sling query generation
for fixed agents, rig-scoped agents, pool agents, and custom overrides.
TestValidatePoolWorkQueryMismatch verifies that pool agents must set
both sling_query and work_query together or neither.
-
Sling query is a shell command, not a Go function call. Every dispatch forks a shell process via
sh -c. This is simple and flexible (any CLI tool can be a routing backend) but adds per-bead fork overhead during batch expansion of large containers. -
Container expansion is serial. When expanding a convoy, each child is slung sequentially. A single slow or failing sling command blocks subsequent children. Partial success is reported but not retried.
-
No built-in load balancing across pool instances. Sling routes to the pool as a whole (via label), not to a specific instance. Work distribution depends on the pool's work query and claim semantics (
bd ready --label=pool:<name> --unassigned --limit=1), which is first-come first-served. -
Nudge targets only one pool instance. After slinging to a pool,
--nudgewakes the first running instance found. Other instances discover work on their next poll cycle. -
No dry-run mode. There is no way to preview what a sling command would do without actually executing it. The pre-flight
checkBeadStateonly warns; it does not prevent routing.
- Architecture glossary -- authoritative definitions of sling, convoy, wisp, formula, and other terms used in this document
- Bead Store architecture -- the persistence substrate that dispatch reads and writes through, including MolCook molecule instantiation
- Health Patrol architecture -- the supervision model that keeps pool agents alive to receive dispatched work
- Config architecture -- how agent configuration (sling_query, pool, suspended) drives dispatch behavior
- CLAUDE.md -- design principles including "the controller drives all SDK infrastructure operations" (layering invariant 6)
- Formula file reference -- formula structure, layer resolution, and wisp instantiation inputs
- TESTING.md -- testing philosophy and tier boundaries for the fake-injection approach used in dispatch tests