cd /news/ai-agents/running-gas-city-across-multiple-cit… Β· home β€Ί topics β€Ί ai-agents β€Ί article
[ARTICLE Β· art-53595] src=gist.github.com β†— pub= topic=ai-agents verified=true sentiment=Β· neutral

Running Gas City across multiple cities and providers

A developer released a multi-city, multi-provider Gas City configuration repository on GitHub. The setup uses a tier abstraction where agents refer to abstract tiers like 'fast', 'solid', and 'deep', which are bound to concrete model families per city, enabling portable and maintainable configs across different providers.

read9 min views1 publishedJul 9, 2026

Now available as a repo: https://github.com/thinkjones/gascity-cookbook

A worked example of a multi-city, multi-provider Gas City setup: one config monorepo that runs several "cities" (agent deployments), each on a different model provider, sharing one reusable pack of config.

The trick that makes it maintainable is a tier abstraction: your agents and workflows only ever refer to abstract tiers β€” fast

, solid

, deep

β€” and each city binds those tiers to a concrete model family (Gemini/Antigravity on one city, Claude on another). The same config is portable; only the binding changes per city.

Names in this guide are illustrative. The real concepts map 1:1 to a Gas City repo.

Piece What it is
Pack
A reusable directory of config (pack.toml + definitions). Meant to be imported.
City
A deployment: a city.toml (workspace provider + rigs) plus a pack.toml (its imports). The city is the root pack.
Rig
A project/repo attached to a city, where work actually happens.
Provider
A coding-agent CLI + model (claude , agy , …), declared under [providers.<name>] .
Agent / Role
A worker (or coordinator) with a prompt. Formulas route work to roles by name.
Formula
A workflow (e.g. build-basic ) whose steps route to roles.

Composition: at load time a city's pack.toml

imports the shared pack + remote role packs, then city.toml

picks the workspace provider and rigs. No single file holds the final config β€” trace it with gc config explain

.

gascity-config/
β”œβ”€β”€ AGENTS.md                      # agent-facing router (also read by non-Claude CLIs)
β”œβ”€β”€ README.md                      # human overview
β”œβ”€β”€ docs/                          # durable guides (this file lives here)
β”‚
β”œβ”€β”€ packs/                         # reusable packs (imported by cities)
β”‚   └── core-pack/
β”‚       β”œβ”€β”€ pack.toml              # provider family concretes, imports, mayor named_session
β”‚       β”œβ”€β”€ model-tiers.base.toml  # role -> tier map (DATA; single source of truth)
β”‚       β”œβ”€β”€ agents/
β”‚       β”‚   └── mayor/
β”‚       β”‚       β”œβ”€β”€ agent.toml     # provider, idle_timeout, wake_mode, append_fragments
β”‚       β”‚       └── prompt.template.md
β”‚       β”œβ”€β”€ template-fragments/
β”‚       β”‚   β”œβ”€β”€ routing.template.md      # {{ define "efficient-routing-rules" }}
β”‚       β”‚   └── mayor-rhythm.template.md  # {{ define "mayor-operating-rhythm" }}
β”‚       β”œβ”€β”€ commands/              # `gc cc <name>` custom CLI commands
β”‚       β”‚   β”œβ”€β”€ gen-model-tiers/{command.toml, run.sh}
β”‚       β”‚   └── clear-human-mail/{command.toml, run.sh}
β”‚       β”œβ”€β”€ orders/                # scheduled/event-driven dispatch
β”‚       β”‚   └── mayor-hourly-sweep.toml
β”‚       └── assets/scripts/        # helper scripts referenced by orders/commands
β”‚
└── cities/                        # runtime deployments
    β”œβ”€β”€ river-city/                # primary provider: Antigravity (Gemini)
    β”‚   β”œβ”€β”€ city.toml              # workspace provider, tier aliases, rigs, include=[...]
    β”‚   β”œβ”€β”€ pack.toml              # imports core-pack + remote role pack
    β”‚   └── model-tiers.toml       # GENERATED per-rig tier patches (included)
    └── mesa-city/                 # primary provider: Claude
        β”œβ”€β”€ city.toml
        β”œβ”€β”€ pack.toml
        └── model-tiers.toml       # GENERATED

Each pack stays clean (only its own config); cross-cutting planning lives at the repo root under docs/

.

This is the core idea. Abstract tier names are the stable interface; the concrete model is a swappable implementation.

packs/core-pack/pack.toml

:

[pack]
name   = "core-pack"
schema = 2

[providers]
[providers.antigravity-fast]   base = "builtin:antigravity"  args = ["--model", "Gemini 3.5 Flash (Low)"]
[providers.antigravity-solid]  base = "builtin:antigravity"  args = ["--model", "Gemini 3.1 Pro (Low)"]
[providers.antigravity-deep]   base = "builtin:antigravity"  args = ["--model", "Gemini 3.1 Pro (High)"]

[providers.claude-fast]   base = "builtin:claude"  option_defaults = { model = "haiku" }
[providers.claude-solid]  base = "builtin:claude"  option_defaults = { model = "sonnet" }
[providers.claude-deep]   base = "builtin:claude"  option_defaults = { model = "opus" }

A provider's base

can point at another provider, not just a builtin. So each city binds the three generic tier names to a family. City-level providers override imported ones ("pack is base, city wins").

cities/river-city/city.toml

(Antigravity):

[workspace]
provider = "antigravity"          # the default for anything without its own provider

[providers.fast]   { base = "antigravity-fast" }
[providers.solid]  { base = "antigravity-solid" }
[providers.deep]   { base = "antigravity-deep" }

cities/mesa-city/city.toml

(Claude):

[workspace]
provider = "claude"

[providers.fast]   { base = "claude-fast" }
[providers.solid]  { base = "claude-solid" }
[providers.deep]   { base = "claude-deep" }

Now fast

/ solid

/ deep

mean Gemini Flash Low / Pro Low / Pro High in river-city, and Haiku / Sonnet / Opus in mesa-city. Everything downstream (agents, routing, formulas) references only the generic names β€” so it's identical across cities.

Portability note:a provider'scommand

/path_check

arenotvariable-expanded β€” use abare name on PATH(command = "kimi"

), not$KIMI_PATH

or an absolute path. Gas City broadens PATH to include~/.local/bin

, Homebrew, cargo, nvm/asdf, etc., so a bare name resolves on every machine. Env-blockvalues(secrets)doexpand$VAR

.

A formula like build-basic

routes each step to a role (requirements-planner

, implementation-worker

, publisher

, …). The model a step runs on is that role agent's provider. So "use the right model per step" = "set each role's provider to a tier".

Roles are rig-scoped (one copy per rig), so this would be N roles Γ— M rigs of nearly identical [[patches.agent]]

blocks. Instead: keep the data + generator in the shared pack and generate the expansion per city.

Data β€” packs/core-pack/model-tiers.base.toml

:

[tiers]
requirements-planner  = "deep"
design-author         = "deep"
task-decomposer       = "deep"
implementation-worker = "solid"
implementation-reviewer = "solid"
run-operator          = "fast"
publisher             = "fast"

Generator β€” a shared command that reads the base map, enumerates the current city's project rigs, and emits [[patches.agent]]

blocks:

cd cities/river-city
gc cc gen-model-tiers > model-tiers.toml     # cc = the core-pack import binding

Produces (one block per rig Γ— role):

[[patches.agent]]
dir = "checkout-service"        # the rig
name = "requirements-planner"   # UNQUALIFIED role name (no binding/rig prefix)
provider = "deep"

[[patches.agent]]
dir = "marketing-site"
name = "requirements-planner"
provider = "deep"

Wire it in β€” cities/river-city/city.toml

(top-level, before any table):

include = ["model-tiers.toml"]

Retune tiers once in model-tiers.base.toml

, regenerate each city. Add a rig β†’ regenerate; it's picked up automatically. Same base map drives every city β€” Claude cities resolve deep

to Opus, Antigravity cities to Pro (High).

Patch-targeting gotcha:a patch matches the agent'sstoredfields β€”name

is theunqualifiedrole (run-operator

, notgc.run-operator

) anddir

is the rig. Thebinding.

/rig/

prefixes only appear indisplaynames (gc rig list

), never in patches.

build-basic

is a formula. Each step carries gc.run_target

metadata naming a role:

formula step  ──gc.run_target──▢  role agent  ──provider──▢  model tier  ──resolves to──▢  concrete model
"requirements"                    requirements-planner        deep                          Opus / Gemini Pro High
"implement"                       implementation-worker       solid                         Sonnet / Gemini Pro Low
"publish"                         publisher                   fast                          Haiku / Gemini Flash Low

The controller sees an unassigned workflow bead tagged gc.run_target=<role>

, spawns that role's on-demand pool session, which claims the bead and runs on its provider. The formula never mentions a model β€” the model is 100% the target role's provider. That's why tiering = setting role providers, and why it's portable across families.

Launch one:

gc bd create "Add a --json flag to the export command"
gc sling run-operator <bead-id> --on build-basic

A single always-on named session that plans, dispatches, and monitors β€” one per city.

packs/core-pack/agents/mayor/agent.toml

:

name = "mayor"
provider = "deep"                                   # coordinator gets the strong tier
append_fragments = ["mayor-operating-rhythm", "efficient-routing-rules"]
idle_timeout = "1h"                                 # idle instead of busy-looping
wake_mode = "fresh"                                 # re-prime clean each wake β€” no context pile-up
max_active_sessions = 1

Declared as always-on in packs/core-pack/pack.toml

:

[[named_session]]
template = "mayor"
mode = "always"
scope = "city"

gives it aprompt.template.md

coordinatorprompt (plan/dispatch/monitor). Without a prompt it falls back to a generic worker loop and busy-polls.attach reusable prompt chunks (see next section) β€” here an "operating rhythm" (idle-when-empty, periodic checks) and the routing rules.append_fragments

  • Because it's deep

, the coordinator runs on Opus / Gemini Pro (High) automatically per city.

Reusable prompt blocks in template-fragments/*.template.md

, defined as Go-template blocks and attached by bare define-name:

packs/core-pack/template-fragments/routing.template.md

:

{{ define "efficient-routing-rules" }}
### Task Routing Rules
Route by how much reasoning the task needs, prefixing the target rig:

    gc.routed_to=<rig>/<tier>

- Trivial / mechanical  β†’ fast
- Standard implementation, review β†’ solid
- Non-trivial: planning, design, architecture β†’ deep

When asked to plan anything non-trivial, delegate it to a `deep` subagent β€”
do not plan it yourself.
{{ end }}

Two rules that bite everyone:

Reference by the bare define name(efficient-routing-rules

), not<file>.<name>

.The file must end in(or legacy.template.md

.md.tmpl

). A plain.md

fragment is silently ignored β†’ "template not found".

Custom commands live under a pack's commands/

and are invoked as gc <binding> <name>

(the import binding β€” cc

for core-pack here):

gc cc gen-model-tiers > model-tiers.toml   # the tier generator above
gc cc clear-human-mail                     # flush a recipient's mail inbox

Orders pair a trigger with an action, evaluated by the controller each tick. packs/core-pack/orders/mayor-hourly-sweep.toml

:

[order]
description = "Wake the mayor once an hour to sweep status and check running convoys."
trigger  = "cooldown"
interval = "1h"
exec     = "bash $GC_PACK_DIR/assets/scripts/mayor-hourly-sweep.sh"
timeout  = "30s"

The script finds the active mayor session and nudges it (gc session nudge <id> "..." --delivery queue

) β€” pure plumbing; all judgment stays in the mayor's prompt.

gc config show                                   # fully-composed config
gc config explain --json --provider deep | jq .  # trace a tier to its concrete model
gc lint <pack>                                   # validate before merge
gc reload                                         # apply config to a running city
gc status                                         # city health
gc session list / peek <name> / reset <name>     # observe / restart a session
gc prime <agent> --strict                        # render an agent's prompt (catch typos/fallbacks)
gc sling <role> <bead> --on build-basic          # launch the build factory

Config change loop: edit β†’ gc lint

β†’ gc config explain

(confirm the resolved value) β†’ gc reload

.

Tier the interface, bind per city. Agents/formulas referencefast

/solid

/deep

; cities bind them. One config, N providers.One source of truth for model IDs. Family concretes in the shared pack; cities only say which family. Bumping a model is a one-line edit.Generate the repetitive patches. RoleΓ—rig tier patches are derived data β€” keep the base map + generator in the pack,include

the generated fragment in the city.Bare name on PATH for portability;command

isn't var-expanded; env values are.$VAR

only for secret env values.Fragments need and are referenced by bare define-name..template.md

A named session's display name is(e.g.<binding>.<name>

cc.mayor

); to show it unprefixed, declare the[[named_session]]

in thecity's ownpack with an explicitname

andtemplate = "<binding>.<agent>"

.Only some providers need Claude auto-installs hooks; others (e.g. Antigravity) must be listed to get their managed prime/nudge/mail hooks.install_agent_hooks

.Give the coordinator a real prompt or it falls back to a worker loop and busy-polls.

── more in #ai-agents 4 stories Β· sorted by recency
── more on @gas city 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/running-gas-city-acr…] indexed:0 read:9min 2026-07-09 Β· β€”