Gas City: Orchestrating Fleets of Coding Agents

📖 31 min read

Gas City is an open-source toolkit for building systems that run many coding agents at once on long-lived engineering work. It does not supply a fixed team of agents. It supplies the machinery underneath one: durable work tracking, a supervisor that keeps agents running, a way to express multi-step methods that fan out across agents, and a configuration format for describing the team. Its documentation is the primary reference for everything below, and its architecture notes describe how each subsystem works in the code. By default, Gas City stores its work in Beads, a dependency-aware issue tracker for agents.

What Breaks When One Agent Becomes Twenty

Sessions Die and Take Their Work With Them

An agent session is a process with a context window. It crashes, hits a rate limit, runs out of context, or wanders off task, and when it ends, whatever it knew about its work in progress ends with it. With one agent, a person notices and restarts it. With twenty, sessions end continuously, and the question becomes whether the work they were doing survives them.

Someone Has to Hand Out Work

Parallel agents need a source of work that tells each one what to do next without handing two of them the same task. A person dispatching by hand becomes the bottleneck well before twenty agents, and a script that assigns tasks up front cannot react when a task fails, spawns follow-up work, or turns out to depend on another.

Stalled Agents Go Unnoticed

An agent that crashes is easy to detect. An agent that sits idle, exhausts its context and stops making progress, or keeps restarting and crashing is not, and each one quietly holds work that nothing else will pick up. At fleet scale, something has to watch every session and act on what it sees.

Hand-Rolled Coordination Doesn’t Travel

Teams that run several agents by hand accumulate the arrangement in scattered places, like role prompts in one directory, setup steps in a chat thread, and conventions in someone’s memory. The arrangement works for the person who built it and cannot be reviewed, versioned, or reused by anyone else.


From Gas Town to Gas City

Gas City grew out of Gas Town, the multi-agent orchestrator Steve Yegge released at the start of 2026. Gas Town ran a fixed cast of roles, each with a name and a job: a Mayor that coordinates, Polecats that do the coding, a Refinery that merges, a Witness and a Deacon that watch over the others, Dogs for maintenance chores, and Crew workspaces where a person works hands-on alongside the agents. The cast worked, but it was hardcoded, so a team that wanted a different shape of organization had to fight the tool.

Gas City, built by Julian Knutsen and Chris Sells, extracts the reusable machinery from Gas Town and removes the roles from the engine entirely. The orchestrator hardcodes no roles at all, with no built-in manager or reviewer. Every role is configuration, and Gas Town itself becomes one configuration that Gas City can load.

Gas Town role In Gas City
Mayor A configured agent with a coordinating prompt
Polecats A pool of interchangeable agents, sized by configuration
Crew A persistent agent configuration
Refinery A post-processing agent that runs as a workflow step
Deacon The orchestrator’s built-in health patrol, optionally paired with a configured agent
Witness Optional behavior built from events and formulas
Dogs Orders that run shell commands, or an agent for work that needs a model

The shift from roles to primitives is what makes Gas City a toolkit rather than a product. The same engine becomes a different orchestrator depending on which configuration it loads.


The Six Primitives

Everything in Gas City is built from six user-facing primitives.

Primitive Answers What it is
Agent Who A configured worker: name, provider, prompt template, and scope
Bead What One unit of durable work, stored in Beads
Formula How A reusable, written-down method that produces work when applied
Rig Where An external project, usually a Git repository, registered with the city
Pack Configures A directory of configuration declaring agents, formulas, orders, and more
Event Observe An immutable notification fired by activity, for people and agents to watch

Beads are the universal store. Tasks, mail between agents, session records, and convoys (batches of related work) are all beads that differ only by type. Because every piece of state lives in one durable store, anything that crashes can be rebuilt from what the store holds.

A rig scopes work and agents. Each registered project gets its own bead namespace and its own agent scope, so rig-scoped agents working on one repository do not pick up work belonging to another. Each rig has its own bead store, by default a prefix-scoped namespace on the city’s shared Dolt server, and only city-scoped agents serve work across stores. The city runs one Dolt server, and each rig’s .beads/ configuration points at it with its own issue prefix, which bd applies as a filter on every read and write. Running bd list inside one rig does not show another rig’s beads, even though both sit in the same database.

C4 · Deployment

Where a Beads Claim Is Atomic

Claiming a bead in embedded clones versus one shared server.

Beads deployment topologies and the reach of an atomic claim Two panels. Across clones in embedded mode, each clone has its own local Dolt database and a claim is atomic only inside that database, so two agents can both claim the same bead before either pushes and the collision surfaces only at merge time. On one machine in server mode, every agent writes through one dolt sql-server to one database, so the first claim wins. Inside Gas City, one server serves the whole city and each rig is an issue prefix on it. ACROSS CLONES · EMBEDDED MODE git origin refs/dolt/data Clone A local Dolt database agent 1 claims here claim is atomic Clone B local Dolt database agent 2 claims here claim is atomic bd dolt push / pull ✗ both claim bd-a1b2 before either pushes Dolt merges cell by cell, so the collision surfaces only at merge: as a conflict, or as a silent merge of two identical claims. A change stays local until it is pushed. ONE MACHINE · SERVER MODE agent 1 agent 2 agent 3 dolt sql-server [Process: many writers] one database one shared ready frontier first claim wins Every claim runs against the same database, so claiming is a true lock. Parallel agents on one backlog belong here. Inside Gas City one server serves the whole city, and each rig is an issue prefix on it, not a database sync or SQL where a claim is atomic

The city is itself a pack. A pack is a directory whose pack.toml declares agents, prompt templates, formulas, orders, and supporting files. The city directory is the root pack, and it imports shared packs by name, so a team definition can be versioned, reviewed, and reused like code. Imported definitions read the same as local ones, and later layers override earlier ones in a fixed order, so a city can adopt a published pack and adjust only what it needs. For formulas, the layers run from system formulas embedded in the gc binary, through imported packs, to the city’s own formulas, and a rig adds its own imports and local formulas on top of the city’s. The winning file for each name is staged as a symlink in that scope’s .beads/formulas/ directory, where bd finds it.

Layering

Pack Layering and Scope

Which formula wins by name in each scope.

How Gas City layers packs and scopes formulas and agents Two stacks of formula layers, highest priority on top. The city scope stacks city-local formulas over the packs the city imports over the system formulas embedded in the gc binary. A rig scope stacks rig-local formulas over the packs the rig imports over the whole city stack as its base. For each scope, the winning formula per name is staged as a symlink in that scope's .beads/formulas directory. Agents scoped to the city get one instance for the city; agents scoped to a rig get one instance per rig. CITY SCOPE · TOP LAYER WINS RIG SCOPE · REPO-A city-local formulas the city's own formulas/ directory packs the city imports pinned, lower priority than local system formulas embedded in the gc binary, layer 0 rig-local formulas when the rig configures one packs the rig imports apply to this rig only city formula layers the whole city stack, as a base winner per name winner per name .beads/formulas/ in the city one symlink per name, pointing at the winner .beads/formulas/ in repo-a one symlink per name, pointing at the winner AGENT SCOPE scope = city one instance for the whole city scope = rig one instance per rig, such as repo-a/reviewer Imported definitions read exactly like local ones, so a city adopts a published pack and overrides only what it needs. The city directory is itself the root pack.

The Orchestrator

One Supervisor Hosts Every City

A machine runs one gc supervisor process. It hosts an orchestrator for each registered city, which the code calls the controller, along with a typed HTTP and Server-Sent Events API that the gc CLI and the built-in dashboard both use. The API listens on the loopback address by default. A lock file in the city’s .gc/ directory keeps a second controller from running against the same city.

C4 · Container

Gas City Containers on One Machine

One supervisor per machine, one controller per city.

Gas City containers on one machine One gc supervisor process per machine hosts the HTTP and SSE API with the dashboard, and one city controller per registered city. The controller reloads the city and pack config when it changes, reads and writes session beads, pool demand, and order tracking beads in the city's single Dolt server, records to and reads from the append-only event log, and starts, stops, interrupts, and peeks sessions through the runtime provider. The sessions are worker pools, an always-on named session, and a deterministic control-dispatcher that runs v2 control beads. Every session claims, closes, and mails through the bead store. No edge runs from a session back to the controller: the loop closes through the store. Operator [Person] ONE MACHINE gc supervisor [Container: Go process, one per machine] API + dashboard [Component] typed HTTP + SSE on 127.0.0.1:8372 CLI and UI share it city controller [Component: CityRuntime, one per registered city] each tick: reload · build agents · reconcile · wisp GC · orders holds .gc/controller.lock, so one controller per city crash, idle, and order state live only in memory gc CLI · dashboard city.toml · pack.toml [Config: city pack + imports] agents, formulas, orders watched by fsnotify Dolt sql-server [Store: one per city] all beads: work, mail, sessions rigs by prefix: riga-*, rigb-* .gc/events.jsonl [Store: append-only log] seq-numbered, replayable feeds event-triggered orders reload on change session beads · demand records, reads start · stop · interrupt · peek RUNTIME PROVIDER · TMUX BY DEFAULT · ALSO SUBPROCESS, K8S, EXEC worker sessions [Container: harness CLI, pool 1–3] hooks: prime, mail, nudges claim routed pool work mayor session [Container: named, always-on] a person can attach to it plans and slings onward control-dispatcher [Container: gc process, no LLM] claims v2 control beads one per store, like any agent every session: bd claim · close · mail sessions run in rig directories (repo-a/, repo-b/), and each rig's .beads/ points at the city's server Nothing runs from a session back to the controller: the loop closes through the store. acts on, reads and writes watches or records deterministic, not a model

Reconciling Desired State Against Running Sessions

The orchestrator is a control loop. On each tick, 30 seconds by default, it reloads configuration if it has changed, works out which agents and how many sessions should be running, compares that against what is running, and starts, stops, or restarts sessions to close the gap. It then evaluates orders and dispatches any whose trigger has fired. This is the same reconciliation pattern that Kubernetes applies to containers, applied to agent sessions.

The steps run in a fixed order. A file watcher marks the configuration dirty between ticks, and a reload that fails validation keeps the previous configuration instead of stopping the city. Pool sizes come from each pool’s scale_check command, and the checks run in parallel, though a check that hangs holds up the whole tick. Sessions are themselves tracked as session beads, so reconciliation compares three things: the session beads, the live processes the runtime provider reports, and the configuration. Between reconciling sessions and dispatching orders, the tick also deletes closed wisps older than a configured time-to-live.

C4 · Component

One Controller Tick

The five steps of one tick, in order.

The five steps of one Gas City controller tick A ticker every 30 seconds starts each tick, and an fsnotify watcher on the config and pack directories sets a dirty flag. Each tick runs five steps in order: reload the config if dirty, keeping the old one on error; build the desired agent set, running pool checks in parallel; reconcile session beads against live sessions; garbage-collect closed wisps past their TTL; and dispatch due orders, writing a tracking bead before each run. The crash tracker, idle tracker, and order dispatcher state live only in memory and reset when the controller restarts, following Erlang/OTP supervisor semantics. ticker patrol_interval 30s fsnotify watcher config and pack dirs sets dirty flag EVERY TICK, IN ORDER 1 · reload only if dirty bad config: keep the old one rebuild trackers 2 · build agents desired set from config; pool checks run in parallel a hung check stalls the whole tick 3 · reconcile session beads vs live sessions: start, drain, stop, restart on drift 4 · wisp GC delete closed wisps past wisp_ttl (off if unset) 5 · orders check triggers, write tracking bead, then run in a goroutine IN MEMORY · RESET WHEN THE CONTROLLER RESTARTS crash tracker starts per window idle tracker last I/O per agent order dispatcher in-flight runs Erlang/OTP semantics: a supervisor restart clears its children's restart counts Reconciliation is idempotent: a live session whose config hash matches is skipped. The tracking bead is written before the order runs, so a slow run cannot re-fire on the next tick. runs next sets or feeds in-memory state

State Lives in the Store, Not in Callbacks

The orchestrator acts on sessions but never waits for sessions to report back to it. It reads progress from the bead store and the event bus. This is what makes the system survive crashes on both sides. When an agent dies, its work stays in the store, still in progress. When the orchestrator itself restarts, it adopts the sessions it finds still running rather than respawning them, and it resumes from the ground truth in the store.

The event bus is separate from the bead store. By default it is an append-only JSON Lines file in the city’s .gc/ directory, and every event carries an increasing sequence number, so a watcher can replay the stream from any point. Bead, session, convoy, and order activity all record events there, and event-triggered orders read the same stream people watch.

C4 · Dynamic

A Crash on Either Side

What survives a session crash, and a controller restart.

How Gas City recovers when a session or the controller crashes Two lanes. When a session dies mid-task, its claimed bead stays in progress and assigned in the store; the next tick sees the dead process and restarts the agent, and hook tier 1 finds the unfinished bead first, so the agent resumes. When the controller restarts, sessions keep running in tmux; the new controller takes the lock, adopts the live sessions by creating session beads rather than respawning them, starts with empty crash and quarantine state, and resumes from the store as ground truth. A SESSION DIES MID-TASK claimed bd-42 in_progress, assignee worker-1, work underway ✗ process dies crash, rate limit, or context gone store unchanged bd-42 still in_progress and assigned next tick patrol sees the dead process and restarts the agent resume hook tier 1 finds bd-42 first, so it carries on THE CONTROLLER RESTARTS controller down sessions keep running in tmux with their work new controller takes the flock, reloads config adopt, not respawn live sessions get session beads; none restarted trackers empty crash counts and quarantines reset, like an OTP restart carry on the store is ground truth; the next tick reconciles Work belongs to the store, not to the session that happened to be running it.

Sessions are disposable by design. An agent with on-demand sessions spins them up when work arrives and lets them go when idle. An always-on named session stays available for a person to attach to and talk with. A pool of identical sessions scales between a configured minimum and maximum against one shared queue of work.

Health Patrol Supervises Like Erlang

Health patrol is the orchestrator’s supervision logic, modeled on the Erlang/OTP supervisor. On every tick it looks for conditions including these:

Condition How health patrol recognizes it
Crashed The session’s process is gone. Health patrol captures its terminal output for diagnosis before restarting it
Stalled No activity past the session’s idle timeout, which each agent opts into
Drifted A hash of the session’s command and environment no longer matches the current configuration
Context exhausted The agent asks to be restarted

Each one is corrected by restarting or replacing the session. Sessions that no longer belong to the configuration are drained gracefully when they are surplus pool members and stopped outright when they are true orphans.

The model is “let it crash.” Agents are not expected to recover themselves. They die and are replaced. Restarts are one-for-one in OTP terms, meaning only the failed session restarts, with no cascade to agents that work alongside it. To stop a broken agent from restarting forever, health patrol counts restarts in a sliding window and quarantines an agent that reaches the limit (five restarts within an hour, by default) until the window passes. The crash counts, idle timers, and in-flight order state are all held in memory, following OTP’s rule that a restarted supervisor starts its children’s counts from zero, so quarantine resets if the orchestrator itself restarts.

State machine

Health Patrol States

Running, crashed, restarted, quarantined, and the moves between them.

Gas City health patrol session states A desired session that is not running is started and becomes running and healthy. Each tick checks a running session in order for a requested restart after context exhaustion, an idle timeout, and config drift, and acts on the first that applies. A session whose process has gone is recorded as crashed, with its pane captured. The crash tracker then decides: if starts inside the restart window are below max_restarts, the session restarts; otherwise it is quarantined and skipped silently until the window ages out. Sessions not in the desired set, such as orphans, suspended agents, and pool excess, are drained or stopped. not running desired, should wake running, healthy alive, hash matches start session.woke CHECKED EACH TICK, IN ORDER 1 restart requested context exhausted → stop + start 2 idle past idle_timeout opt-in per agent → stop 3 config drift fingerprint differs → drain + restart crashed process gone: capture pane session.crashed crash tracker starts in the window below max_restarts? yes: restart quarantined skipped silently until the window ages out no window passes NOT IN THE DESIRED SET orphan, suspended, excess pool excess: drain gracefully true orphan: stop now defaults: max_restarts 5, restart_window 1h, tick 30s quarantine lives in memory, so a controller restart clears it

How Work Moves

Formulas Become Graphs of Beads

A formula is a TOML file describing a method: its steps, the dependencies between them, the variables that parameterize it, and the control flow around it. Applying a formula materializes it as beads, and from then on the run is independent of both the file and any session.

Gas City has two formula contracts. A formula uses the first unless it declares the second, and the documentation recommends the second for new work.

  v1 v2 (workflows)
Executed by One agent, end to end The orchestrator, across agents and pools
Steps Fixed when the formula is applied Independently routable units
Control flow None once started Check, retry, fan-out, and tally loops
Shape Parent and child steps A flat graph of blocking edges

Under v2, the orchestrator drives the run, fanning ready steps out to agents and pools and holding each downstream step until its dependencies close. Under v1, the agent the run was sent to works through every step itself.

A v2 formula compiles into three kinds of bead. The workflow root represents the run. Step beads are ordinary work, and each can be routed to a different agent or pool. Control beads hold the method’s control flow, such as a drain that fans work out, a check, a retry, and a final workflow-finalize step. No model executes a control bead. The control dispatcher does, a deterministic gc process that the built-in core pack declares as an agent with no prompt. It runs as a session like any other, one for each bead store, and it claims routed control beads through the store the same way agents claim work. The root depends on the finalize step, so it becomes ready only when the whole workflow has finished.

Structure

Formula Contracts: v1 and v2

Who executes each bead under v1 and v2.

Who executes a formula under the v1 and v2 contracts Two panels. Under v1, a formula compiles to a molecule whose steps are children of one root, and the single agent it was slung to works every step inside its own session; conditions and loops resolve at cook time and nothing drives the run afterward. Under v2, a formula compiles to a flat graph: plain step beads routed to different pools, and control beads for drain, check, and workflow-finalize that the control-dispatcher executes. The drain fans units out to a worker pool, the check re-runs review until its script passes, and the workflow root goes ready only when finalize closes. V1 · ONE AGENT WORKS THE MOLECULE molecule root .1 design .2 implement .3 test .4 release agent A slung once, works every step itself cook resolves conditions and loops; after that the molecule is inert data. No engine runs it: progress happens only inside the one session it was slung to. ✗ no checks, retries, or fan-out once the run has started V2 · THE ORCHESTRATOR DRIVES A FLAT GRAPH step: plan control: drain unit unit unit step: review control: check workflow-finalize root: ready last planner worker pool reviewer control-dispatcher gc process, no LLM control beads: run by the control-dispatcher step beads: routed to any agent or pool check re-runs review until its script passes work bead, run by an agent control bead, run by the orchestrator blocks or works executes or loops

Sling Creates and Routes in One Step

Applying a formula has three verbs. Cook compiles it and writes the beads without routing them. Sling cooks and routes in one motion, naming a target agent or pool and letting Gas City resolve the rest. Orders apply a formula automatically each time a trigger fires. Sling is the everyday dispatch verb, used by people and by agents alike when they hand work onward.

Routed Work Finds Its Session

Dispatch does not hand work to a particular session. It stamps each bead with the agent or pool it is routed to and leaves it in the store. Work for a pool gets a gc.routed_to metadata field naming the pool, and work for a named agent gets that agent as its assignee. On each tick, the orchestrator counts ready, unassigned work routed to each pool and starts sessions to meet the demand, up to the pool’s maximum.

When a session starts, its hook looks for work in three tiers and takes the first it finds:

  1. Work already assigned to this agent or session and in progress, which is how a restarted agent resumes a task it was in the middle of
  2. Ready work assigned to this agent or session
  3. Ready, unassigned work routed to it, highest priority first and oldest first within a priority, which the session then claims

A session that finds nothing exits cleanly.

This ordering is what makes “let it crash” work in practice. When a session dies mid-task, its bead stays in the store, still in progress and still assigned. When the agent comes back, the first thing its hook finds is that unfinished bead, so it resumes rather than starting something new. Work belongs to the store, not to the session that happened to be running it.

The claim itself is a bd update --claim that the agent’s prompt tells it to run, and bd performs it as an atomic compare-and-swap. Gas City’s own code does not enforce the claim. It makes the work visible and routes it, and the prompt tells the agent to run whatever it finds.

C4 · Dynamic

How Routed Work Finds a Session

A sling traced from sender to pool session.

How slung work reaches a pool session in Gas City A numbered sequence across the sender, the bead store, the controller, and a pool session. The sender slings work, which creates a bead stamped with gc.routed_to. On its tick the controller's scale_check counts routed demand and starts a session. The session's hook runs its work_query, claims one bead with a compare-and-swap, works in the rig, and closes the bead. The close is recorded as a bead.closed event and releases dependents. When the next hook finds nothing, the session exits. sender [Person, agent, or order] bead store [Dolt via bd] controller [Tick loop] pool session [Harness + hook] 1 sling: create + gc.routed_to 2 scale_check counts demand 3 start a session 4 gc hook: work_query tiers 5 bd update --claim (CAS) 6 works in the rig 7 bd close 8 bead.closed: dependents ready 9 next hook: nothing ready exits cleanly command or query event on the bus agent at work

Spawning and Claiming Must Agree

Two readers ask the store the same question. The orchestrator’s scale_check counts ready, unassigned work routed to a pool to decide whether the pool needs another session, and a new session’s third hook tier claims the first bead of that same set. Gas City builds both queries from one shared predicate. When an earlier version let them drift apart, the orchestrator counted beads that sessions would never claim, so sessions started, found nothing, exited, and were started again on the next tick, in a loop the project calls a spawn storm. A team that overrides scale_check or work_query for a pool steps outside that shared predicate and inherits the job of keeping the two in agreement. Configuration validation covers only the write side, rejecting a pool that customizes sling_query without also customizing work_query.

Rules

Claim Tiers and the Shared Predicate

The order a hook claims work in.

The hook claim tiers and the shared routing predicate Two panels. The gc hook claims the first match across three tiers: work in progress and assigned to this session, which is how a restarted agent resumes; ready work assigned to it; then ready, unassigned work routed to its pool, highest priority first and oldest within a priority. A session that finds nothing exits. The second panel shows that scale_check and the third hook tier must read one predicate, because when they drifted apart sessions spawned, found nothing, exited, and respawned in a storm. gc hook tiers, first match wins 1 in progress and assigned to me: resume after a crash 2 ready and assigned to me 3 ready, unassigned, routed to my pool: highest priority first, oldest within it a session that finds nothing exits cleanly one predicate, two readers scale_check counts it: should a session spawn? work_query tier 3 claims its first row both read ready, unassigned, non-epic work whose gc.routed_to names the pool ✗ let them drift and sessions spawn, find nothing, exit, and respawn: a spawn storm

Orders Decide When Work Happens

Formulas describe what work looks like, and orders describe when it happens. An order pairs a trigger with an action, and the orchestrator checks every order’s trigger on each tick.

Trigger Fires when
Cooldown A set interval has passed since the last run
Cron The wall clock matches a schedule
Condition A shell command exits successfully
Event A named event appears on the event bus
Manual Only when invoked by hand

A formula order sends work to a pool of agents. An exec order runs a shell script directly on the orchestrator with no agent involved, which suits mechanical chores like pruning branches or running a linter. Before dispatching, the orchestrator checks that the order has no open work already, so a slow run is not buried under repeat firings. Orders replaced Gas Town’s plugins, and they are how a city does anything on its own initiative, like a scheduled dependency upgrade or periodic cleanup.

Agents Coordinate Through the Store

Agents never reference each other directly. They coordinate through two indirect channels, mail and slung work, and a third, lighter mechanism for live sessions.

Channel Stored as Survives a crash Suits
Mail A bead Yes Messages that must be read, with a subject
Slung work Beads routed to an agent or pool Yes Delegating a task
Nudge Input typed into the session’s terminal No Waking a session or redirecting it right away

Gas City wires these channels into each agent through hooks in the agent’s harness. It installs them automatically for Claude Code, and for other harnesses when the agent lists them in its install_agent_hooks setting. The hooks fire at session start, before each turn, and just before the harness compacts its context. They prime the agent, deliver unread mail into its context, drain queued nudges, and save a handoff before a context cycle, so messages arrive without the agent polling for them. Mail does not wake a sleeping recipient, though. It waits for that agent’s next turn unless the sender asks for a notification or follows up with a nudge.

Flow

Agent Coordination Channels

The channels agents use, and the one that bypasses the store.

How Gas City agents coordinate without referencing each other The mayor session reaches other agents only through the bead store. Mail is a bead of type message, injected into the recipient's context by a hook on its next turn. Slung work is a bead routed to a pool, claimed by a worker's hook. Both survive a crash. A nudge is the one channel that bypasses the store: it types text straight into the recipient's terminal and is lost if that session is down. A strip below shows where the harness hooks fire: at session start, on each turn, and before compaction. mayor [Session] decides and delegates bead store mail = bead, type message work = bead, routed to pool both survive a crash reviewer [Session] reads mail next turn worker pool [Sessions] claims via its hook gc mail send gc sling injected next turn claimed by hook nudge: typed into the terminal, lost if the session is down WHERE THE HARNESS HOOKS FIRE session start prime, then claim routed work each turn inject unread mail, drain nudges before compaction save a handoff

Reliability Comes From the Method

Because any single agent can misread a task or produce broken work, v2 formulas build reliability into the method. A check re-runs a step until a verification script passes, up to a limit. A retry re-runs only on transient failures. A review loop fans out several independent review lanes, synthesizes their findings, applies fixes, and repeats until a check passes. Each of these costs more model usage, so reliability becomes a setting the formula’s author chooses per workflow instead of a property of the model.


Where Agents Run

Gas City separates what an agent is from where its session runs. The harness is the coding agent CLI that does the work, and Gas City ships built-in support for sixteen, including Claude Code, Codex CLI, Gemini CLI, Cursor Agent, and GitHub Copilot. Each agent chooses its own, so one city can run a mixed fleet of harnesses.

The runtime provider decides where sessions live, and it is set once for the whole city. The default runs each session in a tmux pane on the local machine, which a person can attach to and watch. Other providers run sessions as headless subprocesses, as Kubernetes pods, over SSH on another host, or through a custom script, so the same city configuration can move from a laptop to a cluster.


Trust Boundaries

Gas City runs shell commands in many places, like exec orders, session providers, and dispatch, and its documentation is explicit that “those commands are a feature, not a sandbox.” They can do anything their execution context can do.

The trust model separates three main kinds of input. City configuration is trusted operator code and deserves review like any code that runs commands. Imported packs are trusted dependency code, so they should be pinned to a version and reviewed before use, the same care owed to any third-party script. Bead titles, pull request text, and other user-controlled content are untrusted data and must never be concatenated into a shell command, because an agent that files a bead titled with a shell payload has just written a command for the orchestrator to run. The orchestrator strips environment variables with names like TOKEN and API_KEY from the shell commands it runs itself, like order checks and exec orders, but values placed explicitly in configuration pass through by design.

Trust boundary

Gas City Trust Boundaries

Trusted code, untrusted data, and the paths to a shell.

Gas City command trust boundaries City config is trusted operator code and imported packs are trusted dependency code; both define the shell commands Gas City runs, on the orchestrator side (work_query, scale_check, order checks and exec, sling_query, on_boot and on_death) and inside sessions (the agent command, pre_start, and session setup). Work content such as bead titles, mail, formula variables, PR text, and API fields is untrusted data: it may reach those commands only as environment variables, JSON, stdin, or argv, never concatenated into sh -c. The ambient environment passes through a filter that strips secret-looking keys before orchestrator-side shells run, while values placed explicitly in configuration pass through by design. City config [Trusted operator code] city.toml, pack.toml, exec scripts: review like code Imported packs [Trusted dependency code] pin a version and review before importing Work content [Untrusted data] bead titles and descriptions, mail, formula vars, PR text Shell execution surfaces [Commands, not a sandbox] orchestrator side work_query, scale_check order checks and exec sling_query on_boot, on_death session side agent command, pre_start session_setup scripts defines defines ✓ as env, JSON, stdin, or argv ✗ never concatenated into sh -c Ambient env [CI or maintainer shell] may carry secrets secret filter strips inherited keys named like TOKEN, SECRET, API_KEY, … (orchestrator side) explicit config values pass through by design Commands are a feature, not a sandbox: whoever writes the config writes code the orchestrator runs. An agent that files a bead titled with a shell payload has written a command if that title reaches sh -c.

When Gas City Is Worth It

Gas City solves the problems of many long-running agents working a shared, evolving body of work. Most agent use does not have those problems yet.

Situation Fit
One agent at a time, however many sessions the work takes No orchestration needed
A batch of independent, similar tasks A fresh session per task, launched by a script, is simpler and cheaper
One agent on a long-running project Beads alone gives durable memory without an orchestrator
Several agents on a shared backlog, occasionally Beads on a shared database, with agents started by hand
A standing team of agents with recurring workflows, reviews, and automation that must run unattended Gas City

What Adoption Costs

Gas City is free and MIT-licensed, and the direct cost is model usage, which scales with every pool, review lane, and retry the configuration adds. The operational cost is less visible. A city depends on tmux, Git, jq, Dolt, and the Beads CLI, among other tools (a file-based store can stand in for Dolt and Beads in minimal setups), keeps its own state on disk, and has runbooks for problems like Dolt history growing out of control. The project also reached 1.0 only in April 2026, so its concepts and configuration format are still settling, and a team adopting it should expect to track upstream changes.


Common Pitfalls

Running an important workflow through one agent. A single agent is a single point of failure for quality as well as uptime. Use a v2 formula with a check or review loop for any workflow whose output ships.

Keeping the team in prompts instead of packs. Role prompts, setup steps, and conventions scattered across files and chat history are the hand-rolled coordination Gas City exists to replace. Put them in the city’s pack, where they are versioned and reviewable.

Starting new work on v1 formulas. A v1 run is bound to one agent and cannot be routed to a pool or recover with checks and retries. New formulas should declare the v2 contract.

Letting untrusted text reach a shell. Anything an agent or an outside contributor wrote, including bead titles and PR descriptions, is data. Pass it through environment variables or files, never through command interpolation.

Sizing pools without a budget. A pool’s maximum size also caps how much it can spend at once. Set limits against what the work is worth, and let queues wait rather than scaling every pool to its ceiling.

Found this guide helpful? Share it with your team:

Share on LinkedIn