Gas City
“Composable orchestration infrastructure for multi-agent coding workflows.” An open-source Go platform for building your own multi-agent system: you declare a city of AI coding agents in TOML, and an orchestrator keeps reality matching that declaration — starting and stopping sessions, routing work, running scheduled automation — while every unit of work lives as a durable, queryable record called a bead. Write a formula and that orchestrator runs it as a graph across your whole fleet: decomposing the job into beads, fanning ready ones out to many agents, gating each step on its dependencies, retrying failures, and driving it to completion outside your session.
All facts verified against the linked sources on 2026-08-09, at the
v1.4.0 tag (released 2026-07-24) — every source link on this page is pinned to
v1.4.0, so quotes stay checkable. The project moves fast — trust the
live docs over this page for
anything newer.
v1.4.1 released 2026-08-15, after this page's verification pass — not yet re-checked against it.
The idea: orchestration as infrastructure
Every serious multi-agent setup ends up rebuilding the same machinery: something to start and supervise agent processes, something to track work that outlives any one session, something to let agents message each other, something to run jobs on a schedule and restart what breaks. Gas City extracts exactly that machinery from Gas Town — Steve Yegge's 20-to-30-agent “coding factory” — into a primitive-first platform. Six primitives (agent, bead, formula, rig, pack, event) are generic; every behavior, including all of Gas Town's famous roles, is just configuration layered on top. The engdocs state the design goal plainly: “The same SDK can express Gas Town, Ralph, or any other pack” — “the orchestrator hardcodes zero roles — no built-in ‘manager’ or ‘reviewer.’” [README · how-gas-city-works · nine-concepts]
Concretely, the README's feature list is the whole story in five bullets: declarative city
configuration via city.toml; work routing with Beads-backed tracking, formulas,
molecules, waits and mail; runtime providers “tmux, subprocess, exec, ACP, Kubernetes, and
herdr”; a controller/supervisor loop that reconciles desired state to running state; and packs,
overrides, and rig-scoped orchestration for multi-project setups.
[README]
Where it came from
Gas Town launched in January 2026 as Yegge's opinionated “industrialized coding factory manned by superintelligent robot chimps”: seven fixed worker roles, merge queues, patrol loops, operational complexity he compared to Kubernetes. Gas City v1.0 — built by Julian Knutsen and Chris Sells, announced April 2026 — deconstructs that application into composable packs. Two ideas from the announcement shape everything: full observability with git-versioned audit trails (Dolt) turning “dark factories” into light factories, and pool-first reliability — “any agent can go temporarily insane, at any time, and make a bad call.” The human's role shifts to “shepherds, tending flocks of agents which do the ground-level work.” [Welcome to Gas Town · Welcome to Gas City]
Used Gas Town? This guide assumes nothing from it — but section 10 maps every Town role, command, and habit onto Gas City primitives, and green notes like this one flag the ancestry as we go.
Vital statistics
| Fact | Value |
|---|---|
| What | “Composable orchestration infrastructure for multi-agent coding workflows” — an orchestration-builder platform, not a hosted product |
| Version covered | v1.4.0, released 2026-07-24 (this page pins every source link to that tag) |
| Builders | Julian Knutsen & Chris Sells; announced by Steve Yegge, April 2026 |
| Language | Go ≈93%, TypeScript ≈5%, Shell — repo gastownhall/gascity, MIT license |
| Required tools | tmux, git, jq, pgrep, lsof |
| Optional tools | dolt 2.1.0+ & bd 1.0.0+ + flock (the default bd beads store), gh (GitHub gates), herdr 0.7.1+ (optional backend), agent CLIs (claude, codex, gemini…) per provider |
| Session backends | tmux (default and required fallback), subprocess, exec, ACP, Kubernetes, herdr |
| Docs | docs/ user docs (Mintlify) · engdocs/ contributor architecture docs |
What changed in v1.4.0
If you learned Gas City on the 1.3 line, five things moved. Each gets its full treatment in the section named beside it.
| Change | Why it matters | Where |
|---|---|---|
| The vocabulary shifted — user docs now say platform and orchestrator where they said SDK and controller, and the canonical model is “the six primitives” | Agent · Bead · Formula · Rig · Pack · Event, with orchestrator, bead store, and event bus as the role-agnostic machinery under them. engdocs/ still says “controller” for the same daemon | §2 |
| A run-centered dashboard and typed API | Run detail unifies the formula stage ladder, structured transcripts, live token rate, and estimated burn rate; session and run reads moved to typed, paginated surfaces backed by warm projections. The SPA is now compiled into the gc binary and served by the supervisor | §3 |
| Command-usage metrics ship in release builds | Official releases record a canonical command ID, release, OS, and an anonymous installation ID after a one-time disclosure — never arguments, paths, or file contents. gc metrics off, DO_NOT_TRACK=1, or GC_DISABLE_USAGE_METRICS=1 opt out | §3 |
| Formulas v2 is the shape to learn | v1 and v2 are peers, not a version ladder — but v2 is what makes the orchestrator, rather than one agent, the engine. Production controls landed for retry, fan-out, drain, scope, artifacts, and finalization | §5 |
| New operator surface | gc costs (per-run usage from .gc/usage.jsonl), gc context (named remote cities), gc extmsg (external-conversation bindings), gc login/logout/whoami, gc metrics, gc runtime check/conformance/heartbeat. Nothing was removed | §4, §10 |
Upgrading an existing city? Run gc doctor --fix once, first. It
“converges pack imports, provider catalogs, project identity, retired hold labels, and managed
beads/Dolt metadata before the orchestrator starts.” The known 1.3→1.4 papercuts and their
one-command fixes are in §3.
[v1.4.0 release notes]
Sources: repo · README · Yegge announcement · Gas Town origin post
2 · The mental model
Three sentences carry the whole system. You declare what should exist — agents,
projects, methods, schedules — in city.toml and the packs it imports.
An orchestrator daemon makes reality match, reconciling running sessions against
that declaration on every tick and driving formula runs forward. All state lives in
beads — “everything is a bead: tasks, mail, molecules, convoys, and epics” — so work
survives any crash, and agents pull work instead of being pushed to.
[glossary · life-of-a-bead]
Since v1.4.0 the docs name a canonical model: six primitives, each answering one question, sitting on three pieces of role-agnostic machinery — the orchestrator (runs formulas, reconciles sessions), the bead store (durable work), and the event bus (fires activity outward). “None of this machinery knows what your agents do.” [how-gas-city-works]
| Primitive | Asks | Is |
|---|---|---|
| Agent | WHO | a configured worker — name, provider, prompt template, scope |
| Bead | WHAT | one unit of work — ID, title, status, type |
| Formula | HOW | a reusable, written-down method applied over work |
| Rig | WHERE | an external project (usually a git repo) registered with the city |
| Pack | CONFIGURES | the unit of configuration — declares agents, formulas, orders |
| Event | OBSERVE | an outbound notification fired by activity — fired, not polled |
Terminology, so the two doc trees don't confuse you. The v1.4.0 user docs call
the per-city daemon the orchestrator and Gas City a platform;
engdocs/ still calls the same daemon the controller and the same
thing an SDK. Same component, same code. This guide uses “orchestrator” for the concept and
“controller” where it names actual Go symbols like controllerLoop().
[how-gas-city-works · nine-concepts]
city.toml plus .gc/ runtime state. Crucially,
“the City is a pack”: the same configuration unit as everything else, just the root one.
[how-gas-city-works]
min_active_sessions and max_active_sessions — “each tick the
orchestrator runs the agent's scale_check query to measure demand and sizes the pool
to it,” retiring sessions that fall idle. On restart the orchestrator adopts the live
sessions it finds, creating a session bead for each, rather than respawning them. “The SDK
manages session lifecycle. The prompt defines agent behavior.”
[how-gas-city-works · nine-concepts]
open → in_progress → closed. The universal substrate: tasks, mail messages, sessions,
and convoys are all beads differentiated only by type. Stored durably (Dolt-backed bd
by default), which is why a crashed agent's work is resumable.
[how-gas-city-works]
pack.toml. Packs compose through named imports, so shared behavior (like the
entire Gas Town role set) ships as a reusable directory instead of copied files.
[how-gas-city-works]
The load-bearing trick is the bead. Because sessions, mail, and batches are all just typed beads in one durable store, every other feature — routing, messaging, monitoring, crash recovery — is a query. Keep that in mind and the rest of this guide is details.
Sources: how-gas-city-works · nine-concepts · engdocs glossary · dashboard
3 · Install & first city
Labs in this guide were written against gascity v1.4.0 docs (fetched 2026-08-09) on macOS
with Homebrew. Homebrew installs all runtime dependencies for you (tmux, jq, dolt, bd…); the
manual dependency table only matters for direct-download or source builds. Building from source
needs Go 1.26+ (pinned in go.mod as 1.26.4).
[installation]
brew install gascity
gc version
Expect: a version string. Homebrew pulls the runtime dependencies automatically.
Upgrades later: brew update && brew upgrade gascity && gc service restart.
[installation]
zsh users: Oh My Zsh's git plugin aliases gc to
git commit. Check with command gc version; fix permanently by adding
unalias gc 2>/dev/null to ~/.zshrc.
[installation]
gc init ~/bright-lights
cd ~/bright-lights
gc status
gc dashboard
Expect: gc init “bootstraps the city directory, registers it with
the supervisor, and starts the orchestrator” — the city is live immediately, no separate start
step. gc status shows the city-wide overview; gc dashboard opens the
supervisor's web UI (typically http://127.0.0.1:8372/) with agents, beads, mail,
formula runs, and store health.
Useful from anywhere: gc cities lists every city registered with the machine-wide
supervisor; gc stop stops all agent sessions in a city; gc start starts
a city under the supervisor again.
[quickstart · dashboard · CLI]
The dashboard, rebuilt around runs
v1.4.0 changed what gc dashboard is. The UI is “a single-page app compiled
into the gc binary and served by the supervisor on its own listener, so there is
nothing extra to install or run” — the old standalone static server (and its --port
flag) is gone. gc dashboard now resolves the supervisor URL, opens your browser, and
prints the URL; --no-open prints without launching (handy over SSH). If the
supervisor isn't running it tells you how to start it instead of opening a dead URL. One
supervisor serves every registered city — pick yours from the header switcher.
[dashboard · CLI]
The headline feature is run detail: the release notes describe it as unifying
“the formula stage ladder, structured transcripts, live token rate, and estimated burn rate,”
with session and run reads moved onto “typed, paginated API surfaces backed by warm projections
instead of ad hoc wire shapes.” A late fix (PR #4436) restored tokens/min and
burn/hr for long-lived pool sessions by sweeping model usage at each interval end —
so those fields populate even when agents self-drive after their initial claim.
[v1.4.0 release notes]
Security posture: the dashboard binds where the supervisor binds — loopback
(127.0.0.1) by default — and is “intended for local, single-operator use.” Without
allow_mutations it runs read-only and disables its mutating controls. Set
GC_SUPERVISOR_DASHBOARD=0 before starting the supervisor for a typed-API-only
supervisor with no embedded dashboard. Exposing it beyond loopback is a real deployment decision
— see the remote-hardened-city runbook
before you do.
[dashboard]
Command-usage metrics (new in v1.4.0)
Official release builds of gc now record privacy-scoped command-usage metrics. The
disclosure is shown before the first eligible interactive command is recorded, and that first
invocation is never recorded — collection begins with the next one. Unversioned, development,
test, and CI builds fail closed and cannot collect. Events carry “only a canonical command ID,
the gc release, operating system, and an anonymous installation ID — never
arguments, paths, file contents, or environment values.”
[release notes · CLI]
gc metrics status # redacted local state
gc metrics example # the exact request shape that would be sent
gc metrics off # durable opt-out: disables, purges queued data, removes the ID
gc metrics on # accept the disclosure (verified TTY)
# environment-level opt-outs, per process
export DO_NOT_TRACK=1
export GC_DISABLE_USAGE_METRICS=1
Note the asymmetry: DO_NOT_TRACK=0 or GC_DISABLE_USAGE_METRICS=0
“never forces collection on and never overrides a saved opt-out. There is no environment
force-enable.” Don't confuse this with [usage] and gc costs
(§4) — those are your own local cost accounting and go nowhere off the machine.
brew update && brew upgrade gascity
cd ~/bright-lights
gc doctor --fix # run this once per existing city, before anything else
gc start
Expect: gc doctor --fix “converges pack imports, provider catalogs,
project identity, retired hold labels, and managed beads/Dolt metadata before the orchestrator
starts.” The release notes report no 1.3→1.4 regressions; every known papercut comes from
existing machine state left by an older install:
| Symptom | Cause | Fix |
|---|---|---|
gc start keeps running an old binary, or fails with synthetic cache is invalid … missing bundled pack cache marker | Older gc/supervisor at a different path; a present-but-invalid bundled-pack cache does not self-heal (only an absent one does) | gc import install to reseed, then let gc start auto-restart the supervisor (Linux: systemctl --user restart gascity-supervisor) |
gc start <city> aborts over an unrelated city, hinting you to gc init the healthy one | A stale pre-1.3 registered city with un-migrated provider config fails the registry scan | gc doctor --fix in the offending stale city, or gc unregister <stale-city> |
| macOS: supervisor binary-drift auto-restart never completes | macOS can't resolve a direct (non-launchd) supervisor's executable | gc supervisor stop --wait, then gc start |
One config change is load-bearing: configure one store-scoped
control-dispatcher for every graph-owning scope. Formula control beads route
to the dispatcher whose Dir matches the city or rig store that owns the graph, and “a
rig-owned graph with no matching dispatcher fails before instantiation” rather than falling back
to a dispatcher that cannot read its work.
[v1.4.0 release notes · CLI]
Sources: installation · quickstart · dashboard · troubleshooting · v1.4.0 release notes
4 · city.toml anatomy
Everything section 2 called “desired state” is written here. Only [workspace] is
required; a useful minimal city is four blocks. Below, the documented minimal example with the
fields you'll touch first, annotated.
[config reference]
# ── required: city-level metadata ─────────────────────────────
[workspace]
name = "my-city"
# ── how to launch an agent CLI (a named preset) ───────────────
[providers.claude]
command = "claude"
args = ["chat", "--no-interactive", "--agent", "gascity"]
# ── a configured worker; behavior comes from its prompt ───────
[[agent]]
name = "polecat"
provider = "claude"
# scope = "rig" # where it may load: "city" | "rig"
# min_active_sessions = 1 # pool floor…
# max_active_sessions = 3 # …and ceiling
# wake_mode = "resume" # or "fresh" per wake
# drain_timeout = "5m" # default grace on shutdown
# ── an external project this city orchestrates ────────────────
[[rigs]]
name = "myrig"
path = "/path/to/rig"
# prefix = "mr" # bead-ID namespace for this rig
The rest of the schema, by neighborhood
| Section | What it configures |
|---|---|
[imports], [defaults], [patches] | Pack composition: named pack imports, city-level defaults, targeted post-merge patches (never recursive) |
[beads] | Work store backend: bd (Dolt, default), file, or exec:<script> |
[session] | Session provider: tmux (default), k8s, acp, subprocess, exec:<script> |
[mail], [events] | Mail backend (beadmail default; retention TTL for read messages) · event log (file-backed JSONL with rotation) |
[daemon] | Controller: patrol interval, restart thresholds, session circuit breaker, formula-v2 enablement |
[orders], [convergence] | Order skip list + timeout caps · convergence loop limits (max_per_agent 2, max_total 10) |
[api], [[webhook]], [webhooks], [[service]] | HTTP API (port 9443, mutation controls, auth keys) · inbound webhooks (default-closed) · new in 1.4: city-level webhook governance — public-exposure grants and the operator-owned rate limit · workspace-owned services |
[upstreams], [agent_defaults], [[pricing]] | Named model-serving endpoint presets, selectable per agent via upstream — the “Upstream axis”: who serves the model, rendered onto whatever env-var names each harness reads · fallback agent settings · per-model cost overrides |
[usage], [extmsg] | New in 1.4. Usage-fact sink — local (default, JSONL at .gc/usage.jsonl), exec:<script>, or discard; feeds gc costs · default routes for inbound external conversations that have no binding |
[github], [dolt], [doctor], [maintenance] | PR monitors with repair routing (repair_workflow now defaults to the standard polecat repair workflow) · Dolt server tuning · health thresholds · store maintenance cadence |
Two 1.4 knobs worth knowing before you need them. Under [beads],
conditional_writes selects the bead-write discipline — off (legacy),
auto (compare-and-swap where the store can, loud degrade otherwise), or
require (CAS or a typed refusal) — and guarded_release does the same for
ownership release (owner-blind unclaim vs fence-guarded verbs). Both default to off,
and any value outside the enum fails config load. Turn them up when concurrent claims start
racing.
[config reference]
If you ever bind the API off loopback, v1.4.0 added signed-grant admission
control: write_auth_verify_key / read_auth_verify_key require every
mutating (or reading) request to the typed per-city routes to carry a grant from a trusted
authority, with *_auth_required making a missing key a startup error instead of a
silently disabled gate. A non-loopback bind with allow_mutations and no verify key is
a fail-closed startup error unless you explicitly set
write_auth_allow_unverified. The bundled CLI and dashboard mint no grants, so
enabling the gates means fronting them with the authority that does.
[config reference · remote-hardened-city]
Never guess what your config resolved to. gc config show dumps
the fully resolved city configuration as TOML; gc config explain adds provenance —
which pack or layer each value came from. These two commands make section 7's layering rules
observable.
[CLI]
Templates: fields like work_dir, session_setup, on_boot,
and work_query accept placeholders — {{.Session}},
{{.Agent}}, {{.Rig}}, {{.RigRoot}},
{{.CityRoot}}, {{.WorkDir}} and friends — resolved per session.
[config reference]
New command surface in v1.4.0
Nothing was removed from the CLI in this release; these command families are additions. Most of them read or write config you now know where to find.
| Command | What it does |
|---|---|
gc costs | Aggregates recorded usage facts (model tokens and compute wall-seconds) by run, from .gc/usage.jsonl. Local only — under an exec: or discard [usage] provider it shows nothing. Cost is “a list-price estimate for decision support, not an authoritative charge”; unpriced invocations are flagged and excluded from the total |
gc context (+ --city-url, --city-name, --context) | A client-side registry of named remote cities in ~/.gc/contexts.toml — URL, remote city name, TLS options, and an optional credential- or grant-minting command. Precedence is git-like and dry-runnable with gc context current: explicit flag > explicit env > local city discovery > sticky default |
gc extmsg bind / handoff / unbind | Binds an external conversation (telegram, discord, …) to a session or a configured agent. An agent-name binding survives session restarts — inbound messages resolve a live session at delivery time, cold-waking one if none is live. handoff rebinds to another agent: the front-desk pattern. Requires the city API server; no local fallback |
gc login / gc logout / gc whoami | Authenticate to a hosted Gas City service and show the authenticated account |
gc metrics status/example/on/off | Local controls for command-usage metrics (§3) |
gc runtime check / conformance / heartbeat | Probe a runtime backend and check it against the provider contract |
gc import credential add/list/remove | Credentials for fetching private pack imports |
One flag change that will bite scripts: for gc beads list and
gc beads show, the bare --json flag “is reserved by the CLI's
JSON-contract layer and is not wired for this command” — use --format=json. As
before, API-path JSON includes _cache_age_s and fallback-path JSON omits it.
[CLI reference]
Sources: config reference (city.toml schema) · CLI reference · connected clients
5 · Work routing: beads, formulas, molecules, waits & mail
Gas City is a pull system. Nothing pushes a task into an agent's prompt; work is written into the bead store, and each agent's hook query finds what's addressed to it. The docs put the contract in capitals: “If you find work on your hook, YOU RUN IT” — the run-what-you-find rule. [life-of-a-bead]
The primitives, one by one
Sling is the everyday verb: gc sling <target> "<task>"
creates a bead and routes it — to a fixed agent by setting its assignee, to a pool by labeling it
pool:<name>, and with --formula it cooks and routes in one motion.
Pool agents then claim with bd update <id> --claim.
[quickstart · life-of-a-bead]
Formulas go through three stages: TOML file → in-memory recipe (flattened steps
plus dependency edges) → materialized beads, independent of the file. “Work persists, so whoever
picks it up next finds the same state.” Preview with gc formula show, materialize
without routing via gc formula cook, detect drift with
gc formula version-check.
[understanding-formulas]
formula = "feature-flow"
[vars]
feature = "the feature"
[[steps]]
id = "design"
title = "Design {{feature}}"
[[steps]]
id = "implement"
needs = ["design"] # readiness gate: runs when design closes
Molecules and wisps. A molecule is “a formula instantiated at runtime: one root
bead plus zero or more provider-managed step beads.” A wisp is “an ephemeral bead
produced by a v1 formula run” — created by gc sling or order dispatch, auto-closed
and garbage-collected once both wisp_gc_interval and wisp_ttl are set
(wisp GC is disabled unless both are). The core pack's cleanup order tidies all three outcomes
above: it reaps stale ephemeral runs and purges closed step records, with cleanup edges covering
v2 workflows too.
[glossary]
Convoys group related beads into a tracked batch (“a container bead that groups
related issues”), with blocking needs edges between members; when all children close,
the convoy auto-closes. Orders automate dispatch: a trigger — cooldown, cron,
condition, event, or manual — paired with a formula (gc order list,
gc order run).
[glossary · life-of-a-bead · CLI]
Mail is messaging built from beads: a message is a bead with
Type="message", sender in From, recipient as assignee; your inbox is
“the set of open, unread message beads assigned to a recipient.” Reading adds a
read label but keeps the bead open; archiving closes it. Threads are labels
(thread:<id>). Nudges are the opposite trade-off: text sent
straight into a session to wake or redirect it — fire-and-forget, no persistence, lost if the
session isn't running. Waits are durable session waits — a session blocks on a
condition and survives it (gc wait inspects them).
[messaging · session · CLI]
Two compiler contracts — and why v2 is the one to learn
This is the decision v1.4.0's docs put first, ahead of every pattern. “Both contracts are live and supported. They are peers, not a version ladder — each makes a different thing the engine.” Under v1 the engine is the agent you sling to; under v2 it is the orchestrator. [understanding-formulas]
| v1 (default) | v2 ([requires]) | |
|---|---|---|
| Engine | the agent you sling to | the orchestrator |
| Steps | resolved at apply, then inert | independently routable units |
| Control flow | none after apply | check/retry/drain/tally, scope checks, finalize |
| Routing | one agent, one session | many agents and pools (gc.run_target per step) |
| Shape | parent-child molecule tree | flat graph of blocking edges + appended finalize |
“For new work, choose v2. The opt-in is one table”:
[requires]
formula_compiler = ">=2.0.0"
Base constructs (steps, needs, children,
condition, loop, vars, extends) mean the same
in both. Graph-only constructs — check, retry, drain,
on_complete, tally, and reserved gc.* step metadata —
require the declaration; compiling without it fails telling you exactly which table to add. The
host-side [daemon] formula_v2 switch defaults to on, and the deprecated
contract = "graph.v2" key still parses with a gc doctor warning.
Two v1-only edges remain, “neither a reason to start on v1”: gc converge accepts only
v1 formulas (use a v2 check loop for iterate-until-it-passes instead), and container dependencies
have a v2 gap — the v2 compiler creates no parent-child edges yet, so a needs on a
parent gates only on that parent step; list the children explicitly until
#3451 lands.
Verb vs outcome: cook, sling, or order
Three verbs create formula instances, and the outcome follows from the contract rather
than from a separate choice. Cook (gc formula cook <name>)
creates without routing — nothing wakes up; use it to inspect beads first, route yourself, or
graft a sub-DAG onto existing work with --attach <bead-id>.
Sling (gc sling <target> <name> --formula) cooks and
routes in one motion. Orders are scheduled dispatch: a trigger plus a formula
(or a shell command — never both), instantiated and routed to the order's pool each time it fires.
[understanding-formulas]
| Outcome | From | Per-step beads | Root is visible work |
|---|---|---|---|
| Single-bead run (a wisp) | v1, no steps (phase = "vapor") | No — steps stay in the recipe | Yes — the root is the work |
| v1 run with steps (a molecule) | v1 with steps: container root + step children | Yes, as children | No — the root is a container |
| v2 workflow | v2 | Yes, independently routable | No — the root blocks on finalize |
Two refusals that will confuse you the first time. “A pool wakes only for
Ready-visible work, so slinging a v1 run at a pool is refused outright — convert to v2 first.”
And cook and sling in the store the worker reads: cook materializes into the
scope you run it from (--rig, else the enclosing rig directory, else the city), and
sling refuses a cross-store route with refusing cross-store route, telling you to
re-file the bead or pick a reachable target. City-scoped agents are the exception — they are
cross-store eligible and may serve work in any store.
[understanding-formulas]
# a toy project to orchestrate
mkdir ~/hello-world && cd ~/hello-world && git init && cd -
# register it with the city, then sling work at an agent
gc rig add ~/hello-world
cd ~/hello-world
gc sling claude "Create a script that prints hello world"
# watch the bead move open → in_progress → closed
bd show <bead-id> --watch
# and see the same activity as events
gc events
Expect: gc rig add gives the rig its own bead scope (an ID prefix
filtered out of the shared store), hooks, and routing context — and, since v1.3.2, materializes
the rig's skills immediately, including for rigs sourced outside the city tree.
gc sling prints the created bead's ID; a session starts and the task is delivered.
--watch live-updates the status line.
[quickstart]
Gas Town's convoys survive intact — “convoys stay bead-backed grouping and lineage; the
implementation boundary moved.” gc sling now creates convoy structure while routing.
[coming-from-gastown]
Sources: understanding-formulas · life-of-a-bead · messaging · glossary · CLI reference
6 · Runtime providers
Agents don't care where they run. The [session] block picks a backend city-wide (or
GC_SESSION for one process); every backend implements the same provider contract, so
the orchestrator's logic — and your config — stays identical from a laptop tmux pane to a
Kubernetes pod. One rule to remember: “tmux is the default session backend and the fallback,
so it stays required even if you run agents on another backend.” v1.4.0 pushed this further:
provider routing, ACP/automatic runtime selection, Herdr-backed sessions, and
Kubernetes/subprocess/tmux execution now “share one session lifecycle and worker boundary,” with
pool demand, wake, resume, drain, close, and orphan recovery all reasoning from persisted
session/work identity rather than provider-specific shortcuts.
[config reference · README]
| Backend | Mechanism | Reach for it when… |
|---|---|---|
| tmux | Sessions as tmux panes | Default. Local, interactive, attachable — and the required fallback for everything else |
| subprocess | Plain local processes | Non-interactive agents that need no terminal |
| exec:<script> | Your script implements the session contract | Custom environments — the escape hatch (contract in reference/exec-session-provider.md) |
| ACP | Agent Client Protocol; acp/auto/hybrid routing layers | Editor/protocol-native agents rather than terminal CLIs |
| k8s | Sessions in Kubernetes | Scaling beyond one machine |
| herdr | Shared herdr session-server per city, workspace/tab hierarchy; opt-in, selected city-wide only | Many agents, cleaner organization than raw tmux — purpose-built for AI agent fleets |
The backend is a whole-city choice — you cannot pin one agent back to tmux.
This trips people who expect Kubernetes-style per-workload scheduling. The per-agent
session field selects a transport, not a backend, and accepts only
acp, tmux, or omission. Under a herdr city:
session = "acp" genuinely moves that agent onto the ACP backend — “it is the one
per-agent lever that changes which backend an agent runs on” — but
“session = "tmux" does not keep an agent on tmux. The herdr provider does not
implement the transport-capability check, so the pin is neither honored nor rejected; the agent
falls back to the base provider and runs on herdr.” To put an agent on tmux, the whole city
(or process) must default to tmux.
[herdr-provider]
Piloting a new backend, the documented way: scope which city runs on
it, not which agents. Requires herdr 0.7.1+ on PATH — if the binary is
missing, sessions selected onto herdr fail to start.
[herdr-provider]
# 1 · per-process trial on a throwaway city — nothing committed
GC_SESSION=herdr gc start <scratch-city>
# 2 · promote to that scratch city's default, run it end to end
[session]
provider = "herdr"
# 3 · widen to the real city only after several stable work cycles
Apply a selector change with gc reload or a city restart; already-running sessions
keep their current backend until they next restart. Confirm with gc config show.
Layout under herdr mirrors the town: one workspace per rig plus one for the town, one tab per
agent.
Sources: config reference · herdr-provider · session architecture · README
7 · Multi-project: rigs, packs & overrides
One city, many codebases, shared behavior — this is where Gas City earns the “composable” in its tagline. Three mechanisms do the work: rigs isolate projects, packs share configuration, and overrides/patches specialize it per place.
Rigs: project isolation
Registering a repo (gc rig add) gives it its own bead scope, hook installation, and
routing context; the rig's prefix namespaces its bead IDs so queries never leak
across projects. Under the hood “the city and all its rigs share one underlying store, and reads
and writes are filtered to the current scope's prefix” — the isolation is the prefix, not a
separate database, but it is enforced hard enough that cross-scope routing is refused
(§5). Rig config can set its own formulas_dir,
formula_vars, session caps, and default sling targets —
default_sling_targets (plural, new in 1.4) takes a list and picks one at random per
targetless dispatch, taking precedence over the singular form.
[how-gas-city-works · config reference]
Packs: shared behavior via named imports
“A pack is a directory with a pack.toml file” — agents in agents/,
formulas in formulas/, plus prompts, orders, commands, skills. Imports are
named bindings, and the binding qualifies every imported agent name: import a pack as
gascity and you address its planner as gascity.planner, never bare
planner. Imports live at city level, or under a [[rigs]] entry — which
stamps the rig's identity onto the imported agents.
[understanding-packs]
Keep two identifiers apart, because only one is portable. A registry handle like
main:gascity finds a pack on this machine — main is your local
registry name and another machine could call it work. A durable
source like
https://github.com/gastownhall/gascity-packs/tree/main/gascity is what you commit,
“independent of any machine's registry name or cache.” Use the handle to search
(gc pack registry …); write the source into your TOML. New in 1.4:
gc import credential manages credentials for private pack sources.
[understanding-packs · CLI]
The layering order (memorize the last line)
city.toml + city pack
→ imported packs (+ pack-level patches)
→ city-level imports → city-level patches
→ rig-level imports (stamp rig agents) → rig overrides
→ pack globals
→ city agent defaults
“Later layers win for replacement-style fields. Defaults run last but only fill blanks,
so they never override an explicit value from an earlier layer.” Session-cap precedence runs
agent > rig > workspace; an agent's scope field (city,
rig, or omitted for both) says where a definition may load — “it does not
name a particular rig.”
[understanding-packs · config reference]
mkdir ~/second-project && cd ~/second-project && git init && cd -
gc rig add ~/second-project
gc rig # both rigs listed
gc config explain # every resolved value, with which layer set it
Expect: the new rig appears with its own prefix and beads store;
gc config explain shows provenance per value — the layering chain above, made
visible.
[quickstart · CLI]
The deepest habit to unlearn from Gas Town: “do not port code or prompts that assume
directory path implies who the agent is.” Identity is explicit config now — dir
is scope, not selfhood.
[coming-from-gastown]
Sources: understanding-packs · config reference · quickstart
8 · The reconcile loop
Kubernetes users will feel at home: the orchestrator is a declarative convergence
engine. Desired state comes from city.toml parsed with includes and
patches; observed state comes from Provider.ListRunning() plus session metadata;
reconcileSessionBeads() continuously repairs the divergence. The whole cycle lives
in controllerLoop() and runs every patrol_interval (default 30s).
[controller]
Where v2 orchestration actually executes. The reconcile loop keeps
sessions converged; a second mechanism drives work graphs. The engdocs are
blunt about it: v2 orchestration “is implemented by the control dispatcher in
internal/dispatch executing control beads (check, retry, fan-out, tally, drain,
scope-check, workflow-finalize).” Those control beads route to the dispatcher whose
Dir matches the store that owns the graph — which is why every graph-owning scope
needs its own control-dispatcher agent configured (§3). v1.4.0 also
made that routing retry transient config/include read failures instead of quarantining an
in-flight run, while still failing closed when a successfully loaded config genuinely lacks the
scoped dispatcher.
[nine-concepts · dispatch · release notes]
Failure handling
- Graceful termination is two-pass: “(1) send Interrupt (Ctrl-C) to all sessions, (2) wait shutdown_timeout, (3) force-kill survivors via Stop().”
- Crash loops hit a breaker: the crash tracker quarantines agents using a sliding window — “max_restarts within restart_window” — so a broken agent can't consume resources forever.
- Health patrol is just an order: “probe sessions… compare thresholds… publish stalls… restart with backoff” — remediation composed from the same primitives as any other automation.
- Pool capacity is fenced (new in 1.4): claim ownership, wake budgets, slot selection, and confirmed-dead cleanup are “fenced against stale or partial observations,” so pool sessions no longer lose or strand work while draining, restarting, or reusing capacity.
- Degraded is now said out loud: managed beads and Dolt paths — provider health, endpoint ownership, lock release, compaction, reindexing, stale data-dir cleanup, partial store reads — “preserve errors instead of silently reporting complete state.”
Above the loop: the supervisor
The machine-wide supervisor exposes the “typed HTTP + SSE control plane” (OpenAPI-documented)
that the dashboard and gc events consume — and since v1.4.0 it serves the dashboard
SPA itself, same-origin, out of the gc binary. Its API defaults to loopback with
mutation controls and auth keys in [api]; the event log is file-backed JSONL with
rotation (gc events rotate). v1.4.0 bounded these paths under load: “the CLI drains
paginated event windows, request paths avoid unbounded scans, tmux sessions keep their shared
server, and structured transcripts preserve tool and error frames.”
[reference index · config reference · CLI]
Sources: engdocs controller · nine-concepts · reference index
9 · Code tour
The repo splits cleanly: cmd/gc/ holds the CLI and the controller;
internal/ holds 106 top-level packages (150 Go package directories at v1.4.0, up
from 76/105 at v1.3.0) implementing the primitives; docs/
is the user manual and engdocs/ the contributor manual. Start reading with three
engdocs files: nine-concepts.md (the layering), life-of-a-bead.md
(the data flow), controller.md (the loop).
[internal/ · engdocs/architecture]
Details worth knowing before you patch anything
- Provider contract: lifecycle (
Start/Stop/Interrupt), observation (IsRunning,ListRunning,Peek,GetLastActivity…), interaction (Attach/Nudge/SendKeys), metadata, and staging (CopyTo/RunLive). Optional extensions:InteractionProvider,IdleWaitProvider,ImmediateNudgeProvider. Rules:Stopis idempotent;ProcessAlivereturns true on empty process lists. - Session identity:
agent.SessionNameFor()is “the single source of truth for runtime session naming”; beacons (startup ID strings) let restarted sessions be recognized and adopted after crashes. - Store implementations:
BdStore(Dolt-backed, production),FileStore,MemStore, and an exec store — all behind the same CRUD + parent-child + dependencies + labels + query contract. - The invariant that bites: “Only non-nil fields in UpdateOpts are applied. Labels append, never replace.”
- Where v1.4.0's new surface lives: 30 top-level packages appeared under
internal/. The ones that map to features in this guide areusageandproductmetrics(usage facts and command-usage metrics),eventfeedandtranscriptmeta(paginated feeds, structured transcripts),poolplan,warmupandorderdispatch(pool sizing and dispatch),clientauth/clientgrant/citywriteauth/clientcontext(remote cities and signed grants),webhookmatch/webhooksink/webhookverify, andrig,storeref,doltorphanfor store scoping and repair.engdocs/architecture/losttally-control.md(21 docs → 20).
[session · nine-concepts · life-of-a-bead]
Sources: internal/ tree · engdocs/architecture tree · session.md · controller.md
10 · Gas Town → Gas City
The migration doc's core instruction: don't import Gas Town's surface area — “re-express the intent in Gas City's primitives.” The deepest change is ownership: the orchestrator now “owns reconciling desired→running sessions, session scaling, order evaluation, health patrol” — duties Gas Town distributed across role agents. Roles didn't die; they became configuration, and ship today as the Gastown pack (mayor, polecat, witness, refinery, deacon, boot). v1.4.0's rewrite of that page adds the second half of the story: “the orchestrator can now run a formula as a graph across many agents, out of your session — decomposing a job into beads, fanning the ready ones out in parallel, gating each step on its dependencies, and retrying failures to completion. Your single-agent, in-session formulas still run (v1); this fleet orchestration (v2) is what's new.” Because it is a platform, a feature added to Gas City lifts every orchestrator built on it — Gas Town included. [coming-from-gastown · command map]
Roles & concepts
| Gas Town | Gas City | What changed |
|---|---|---|
| Mayor — “planner/coordinator; the human's point of contact” | “Configured agent + coordinating prompt” | A role is a prompt now, not platform code |
| Deacon (watchdog) | “Orchestrator health patrol + config thresholds” | The platform absorbed the watchdog duty |
| Witness (lifecycle observer) | “Events + waits, formulas, session scale config” | Observation decomposed into primitives |
| Refinery (post-processing) | “Configured agent + a formula or order post-processing step” | A pipeline step, not a standing role |
| Polecat (on-demand worker) | “Scalable/transient agent config (a pool)” | min/max_active_sessions replace role spawning |
| Crew (persistent workers) | “Persistent named agent config” | Same idea, declared in TOML |
| Dog (integration relay) | “Core-pack exec orders”, optional agent wrapper | Shell work no longer needs an agent session |
| Plugins | Orders — exec order (shell/orchestrator-side) or formula order (agent-driven) | One automation model: trigger + formula |
| Convoys | Convoys — still bead-backed grouping and lineage | “The implementation boundary moved”; gc sling creates convoy structure while routing |
| Beads | Beads | Unchanged substrate — now literally everything (mail, sessions) is one |
| ~/gt/… role homes, path-derived identity | City dir + city.toml + .gc/; agents in agents/<name>/ with explicit dir | “Directory path implies who the agent is” is gone — identity is explicit config |
Muscle-memory table (most-typed commands)
| gt | gc / bd |
|---|---|
gt install · gt up / gt down | gc init · gc start / gc stop |
gt daemon · gt status · gt dashboard | gc supervisor · gc status · gc dashboard |
gt sling · gt convoy · gt hook · gt ready | gc sling · gc convoy · gc hook · bd ready |
gt mol · gt formula | gc formula cook / bd mol … · gc formula list/show/cook, gc sling --formula |
gt gate · gt park · gt resume | gc wait / formula [steps.gate] · gc wait · gc wait ready, gc session wake, gc mail check |
gt mail · gt nudge · gt peek | gc mail · gc session nudge · gc session peek |
gt plugin | gc order |
gt session at mayor/ · gt send mayor "task" | gc session attach mayor · gc sling mayor "<description>" |
gt activity / gt feed / gt trail / gt log | gc events (+ gc session peek/logs, gc supervisor logs) |
gt bead / gt cat / gt show / gt close | mostly plain bd (bd close etc.) |
Dropped without direct equivalent — usually on purpose: gt broadcast,
gt notify, gt dnd, gt escalate, gt whoami,
gt checkpoint, gt patrol, gt seance,
gt mq (merge queue), gt callbacks, gt issue. Hardcoded role
types, path-derived config, and helper agents for shell work are explicitly gone.
[command map · coming-from-gastown]
Two entries the command map hasn't caught up with. It still lists
gt costs as having “no matching top-level cost accounting command today,” but
v1.4.0's CLI reference ships gc costs — per-run model tokens and
compute wall-seconds with a list-price estimate, read from .gc/usage.jsonl. Treat
that row as stale. gc whoami also now exists, but it is not the successor to
gt whoami: it shows the authenticated hosted-service account, whereas Town's
whoami answered “which agent am I” — still answered by config, session metadata, and
GC_* env, exactly as the map says.
[CLI reference · command map]
Documented migration mistakes: importing Town's surface area instead of re-expressing intent · inventing new hardcoded roles instead of configuring agents · duplicating state outside beads when metadata suffices · assuming directory layout enforces architecture. [coming-from-gastown]
Sources: coming-from-gastown · gastown-command-map
11 · Quiz, glossary & sources
Twenty-two questions, weighted toward the mental model and work routing, with the last four on what v1.4.0 changed. Reveal, then score yourself honestly — 17+ is mastery.
min_active_sessions and max_active_sessions. (§2)gc hook runs the agent's work_query (bd ready --assignee=…, or pool label + --claim). The rule: “If you find work on your hook, YOU RUN IT” — run-what-you-find. (§5)needs list — the step IDs that must complete first. The orchestrator gates each step on its dependencies and runs all ready steps in parallel. (§5)gc config show (fully resolved TOML) and gc config explain — which adds provenance: the pack/layer each value came from. (§4)gc init do, and what do you have afterwards?Provider.ListRunning() plus session metadata. reconcileSessionBeads() repairs the divergence. (§8)[requires] formula_compiler = ">=2.0.0". Choose v2 for new work. (§5)session field selects a transport and accepts only acp, tmux, or omission; session = "tmux" under a herdr city is “neither honored nor rejected” and the agent runs on herdr anyway. Only session = "acp" genuinely moves an agent to a different backend. To get tmux, the city must default to tmux. (§6)gc start aborts complaining about a city you weren't starting. What happened, and what's the first command you run on any 1.3→1.4 upgrade?gc init the healthy city. Fix the offending one (gc doctor --fix inside it) or gc unregister <stale-city>. And the first command on any upgrade is gc doctor --fix, which converges pack imports, provider catalogs, project identity, retired hold labels, and managed beads/Dolt metadata before the orchestrator starts. (§3)Glossary
| Term | Meaning | Gas Town ancestor |
|---|---|---|
| Bead | One unit of work — ID, title, status, type; open → in_progress → closed | Bead (unchanged) |
| City | The root pack at the deployment directory; city.toml + .gc/ state | The Town |
| Rig | External project registered with the city; own bead namespace + hooks | Rig |
| Agent | Worker a pack defines: prompt + scope + provider; config, not code | Role (mayor, crew…) |
| Session | A running agent — live process managed by a provider | — |
| Pool | Agent scaling between min/max active sessions | Polecats |
| Formula | Reusable TOML method: steps, vars, needs edges | gt formula |
| Molecule | A v1 formula instantiated at runtime: container root + step children | gt mol |
| Workflow | A v2 formula instantiated at runtime: flat graph of independently routable step beads, root blocks on finalize | — |
| Wisp | Ephemeral bead from a v1 formula run; auto-closes, garbage-collected on TTL | — |
| Control bead | Orchestration step the control dispatcher executes: check, retry, fan-out, tally, drain, scope-check, workflow-finalize | — |
| Control dispatcher | Store-scoped worker that executes control beads; one required per graph-owning scope | — |
| Convoy | Container bead grouping related work as a tracked batch | Convoy |
| Epic | Ordinary bead type for tracking; not a first-class container | — |
| Order | Trigger (cooldown/cron/condition/event/manual) + formula or script | Plugin |
| Event | Immutable append-only record with monotonic sequence; replayable | gt feed/activity |
| Message beads; inbox = open unread beads assigned to you | gt mail | |
| Nudge | Fire-and-forget text into a session to wake or redirect it | gt nudge |
| Wait | Durable session wait — block on a condition, survive it (gc wait) | gt gate / gt park |
| Pack | The unit of configuration: pack.toml + agents, formulas, orders | — |
| Sling | Create + route work in one motion (gc sling, --formula to cook too) | gt sling / gt send |
| Orchestrator | Per-city daemon driving the reconcile loop and formula runs — called the controller in engdocs and in code | (distributed across roles) |
| Supervisor | Machine-wide HTTP+SSE control plane; serves the API and hosts the dashboard SPA | gt daemon |
| Health patrol | Probe → thresholds → stalls → restart with backoff; just an order | Deacon |
| Upstream | Named model-serving endpoint preset, selected per agent; renders onto each harness's env-var names | — |
| Context | Named remote city in ~/.gc/contexts.toml; a local city always wins over the sticky default | — |
| Usage fact | Recorded model-token / compute-second record in .gc/usage.jsonl; what gc costs aggregates | gt costs |
Sources
Everything above traces to these. All sources fetched 2026-08-09 and pinned to the
v1.4.0 tag, so every quote on this page stays checkable even as main moves
on.
Official — user docs
- github.com/gastownhall/gascity — repo · README · v1.4.0 release notes
- Getting started: how-gas-city-works (the six primitives) · installation · quickstart · dashboard · troubleshooting · FAQ · coming-from-gastown
- Guides: understanding-formulas · understanding-packs · configuring-an-agent · harness-recipes · connected-clients
- Reference: config (city.toml schema) · cli · gastown-command-map · herdr-provider · formula-spec-v2 · reference index
- Runbooks: remote-hardened-city
Official — contributor architecture (engdocs)
- nine-concepts (retitled “Code-layering View (implements the six primitives)”) · controller · dispatch · life-of-a-bead · glossary · messaging · session
Lineage
- Yegge — “Welcome to Gas Town” (Jan 2026) · Yegge — “Welcome to Gas City” (Apr 2026)