{"slug": "running-gas-city-across-multiple-cities-and-providers", "title": "Running Gas City across multiple cities and providers", "summary": "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.", "body_md": "**Now available as a repo: https://github.com/thinkjones/gascity-cookbook**\n\nA worked example of a **multi-city, multi-provider Gas City**\nsetup: one config monorepo that runs several \"cities\" (agent deployments), each on a\ndifferent model provider, sharing one reusable pack of config.\n\nThe trick that makes it maintainable is a **tier abstraction**: your agents and workflows\nonly ever refer to abstract tiers — `fast`\n\n, `solid`\n\n, `deep`\n\n— and each city binds those\ntiers to a concrete model family (Gemini/Antigravity on one city, Claude on another). The\nsame config is portable; only the binding changes per city.\n\nNames in this guide are illustrative. The real concepts map 1:1 to a Gas City repo.\n\n| Piece | What it is |\n|---|---|\nPack |\nA reusable directory of config (`pack.toml` + definitions). Meant to be imported. |\nCity |\nA deployment: a `city.toml` (workspace provider + rigs) plus a `pack.toml` (its imports). The city is the root pack. |\nRig |\nA project/repo attached to a city, where work actually happens. |\nProvider |\nA coding-agent CLI + model (`claude` , `agy` , …), declared under `[providers.<name>]` . |\nAgent / Role |\nA worker (or coordinator) with a prompt. Formulas route work to roles by name. |\nFormula |\nA workflow (e.g. `build-basic` ) whose steps route to roles. |\n\n**Composition:** at load time a city's `pack.toml`\n\nimports the shared pack + remote role\npacks, then `city.toml`\n\npicks the workspace provider and rigs. No single file holds the\nfinal config — trace it with `gc config explain`\n\n.\n\n```\ngascity-config/\n├── AGENTS.md                      # agent-facing router (also read by non-Claude CLIs)\n├── README.md                      # human overview\n├── docs/                          # durable guides (this file lives here)\n│\n├── packs/                         # reusable packs (imported by cities)\n│   └── core-pack/\n│       ├── pack.toml              # provider family concretes, imports, mayor named_session\n│       ├── model-tiers.base.toml  # role -> tier map (DATA; single source of truth)\n│       ├── agents/\n│       │   └── mayor/\n│       │       ├── agent.toml     # provider, idle_timeout, wake_mode, append_fragments\n│       │       └── prompt.template.md\n│       ├── template-fragments/\n│       │   ├── routing.template.md      # {{ define \"efficient-routing-rules\" }}\n│       │   └── mayor-rhythm.template.md  # {{ define \"mayor-operating-rhythm\" }}\n│       ├── commands/              # `gc cc <name>` custom CLI commands\n│       │   ├── gen-model-tiers/{command.toml, run.sh}\n│       │   └── clear-human-mail/{command.toml, run.sh}\n│       ├── orders/                # scheduled/event-driven dispatch\n│       │   └── mayor-hourly-sweep.toml\n│       └── assets/scripts/        # helper scripts referenced by orders/commands\n│\n└── cities/                        # runtime deployments\n    ├── river-city/                # primary provider: Antigravity (Gemini)\n    │   ├── city.toml              # workspace provider, tier aliases, rigs, include=[...]\n    │   ├── pack.toml              # imports core-pack + remote role pack\n    │   └── model-tiers.toml       # GENERATED per-rig tier patches (included)\n    └── mesa-city/                 # primary provider: Claude\n        ├── city.toml\n        ├── pack.toml\n        └── model-tiers.toml       # GENERATED\n```\n\nEach pack stays clean (only its own config); cross-cutting planning lives at the repo root\nunder `docs/`\n\n.\n\nThis is the core idea. **Abstract tier names are the stable interface; the concrete model\nis a swappable implementation.**\n\n`packs/core-pack/pack.toml`\n\n:\n\n```\n[pack]\nname   = \"core-pack\"\nschema = 2\n\n[providers]\n# --- family concretes: the ONE place model IDs live ---\n[providers.antigravity-fast]   base = \"builtin:antigravity\"  args = [\"--model\", \"Gemini 3.5 Flash (Low)\"]\n[providers.antigravity-solid]  base = \"builtin:antigravity\"  args = [\"--model\", \"Gemini 3.1 Pro (Low)\"]\n[providers.antigravity-deep]   base = \"builtin:antigravity\"  args = [\"--model\", \"Gemini 3.1 Pro (High)\"]\n\n[providers.claude-fast]   base = \"builtin:claude\"  option_defaults = { model = \"haiku\" }\n[providers.claude-solid]  base = \"builtin:claude\"  option_defaults = { model = \"sonnet\" }\n[providers.claude-deep]   base = \"builtin:claude\"  option_defaults = { model = \"opus\" }\n```\n\nA provider's `base`\n\ncan point at *another provider*, not just a builtin. So each city binds\nthe three generic tier names to a family. City-level providers override imported ones\n(\"pack is base, city wins\").\n\n`cities/river-city/city.toml`\n\n(Antigravity):\n\n```\n[workspace]\nprovider = \"antigravity\"          # the default for anything without its own provider\n\n[providers.fast]   { base = \"antigravity-fast\" }\n[providers.solid]  { base = \"antigravity-solid\" }\n[providers.deep]   { base = \"antigravity-deep\" }\n```\n\n`cities/mesa-city/city.toml`\n\n(Claude):\n\n```\n[workspace]\nprovider = \"claude\"\n\n[providers.fast]   { base = \"claude-fast\" }\n[providers.solid]  { base = \"claude-solid\" }\n[providers.deep]   { base = \"claude-deep\" }\n```\n\nNow `fast`\n\n/ `solid`\n\n/ `deep`\n\nmean **Gemini Flash Low / Pro Low / Pro High** in river-city,\nand **Haiku / Sonnet / Opus** in mesa-city. Everything downstream (agents, routing, formulas)\nreferences only the generic names — so it's identical across cities.\n\nPortability note:a provider's`command`\n\n/`path_check`\n\narenotvariable-expanded — use abare name on PATH(`command = \"kimi\"`\n\n), not`$KIMI_PATH`\n\nor an absolute path. Gas City broadens PATH to include`~/.local/bin`\n\n, Homebrew, cargo, nvm/asdf, etc., so a bare name resolves on every machine. Env-blockvalues(secrets)doexpand`$VAR`\n\n.\n\nA formula like `build-basic`\n\nroutes each step to a **role** (`requirements-planner`\n\n,\n`implementation-worker`\n\n, `publisher`\n\n, …). The model a step runs on is **that role agent's\nprovider**. So \"use the right model per step\" = \"set each role's provider to a tier\".\n\nRoles are **rig-scoped** (one copy per rig), so this would be N roles × M rigs of nearly\nidentical `[[patches.agent]]`\n\nblocks. Instead: keep the data + generator in the shared pack\nand generate the expansion per city.\n\n**Data** — `packs/core-pack/model-tiers.base.toml`\n\n:\n\n```\n[tiers]\nrequirements-planner  = \"deep\"\ndesign-author         = \"deep\"\ntask-decomposer       = \"deep\"\nimplementation-worker = \"solid\"\nimplementation-reviewer = \"solid\"\nrun-operator          = \"fast\"\npublisher             = \"fast\"\n```\n\n**Generator** — a shared command that reads the base map, enumerates the *current city's*\nproject rigs, and emits `[[patches.agent]]`\n\nblocks:\n\n```\ncd cities/river-city\ngc cc gen-model-tiers > model-tiers.toml     # cc = the core-pack import binding\n```\n\nProduces (one block per rig × role):\n\n```\n# GENERATED — do not edit by hand\n[[patches.agent]]\ndir = \"checkout-service\"        # the rig\nname = \"requirements-planner\"   # UNQUALIFIED role name (no binding/rig prefix)\nprovider = \"deep\"\n\n[[patches.agent]]\ndir = \"marketing-site\"\nname = \"requirements-planner\"\nprovider = \"deep\"\n# ...etc\n```\n\n**Wire it in** — `cities/river-city/city.toml`\n\n(top-level, before any table):\n\n```\ninclude = [\"model-tiers.toml\"]\n```\n\nRetune tiers once in `model-tiers.base.toml`\n\n, regenerate each city. Add a rig → regenerate;\nit's picked up automatically. Same base map drives every city — Claude cities resolve `deep`\n\nto Opus, Antigravity cities to Pro (High).\n\nPatch-targeting gotcha:a patch matches the agent'sstoredfields —`name`\n\nis theunqualifiedrole (`run-operator`\n\n, not`gc.run-operator`\n\n) and`dir`\n\nis the rig. The`binding.`\n\n/`rig/`\n\nprefixes only appear indisplaynames (`gc rig list`\n\n), never in patches.\n\n`build-basic`\n\nis a formula. Each step carries `gc.run_target`\n\nmetadata naming a role:\n\n```\nformula step  ──gc.run_target──▶  role agent  ──provider──▶  model tier  ──resolves to──▶  concrete model\n\"requirements\"                    requirements-planner        deep                          Opus / Gemini Pro High\n\"implement\"                       implementation-worker       solid                         Sonnet / Gemini Pro Low\n\"publish\"                         publisher                   fast                          Haiku / Gemini Flash Low\n```\n\nThe controller sees an unassigned workflow bead tagged `gc.run_target=<role>`\n\n, spawns that\nrole's on-demand pool session, which claims the bead and runs on **its** provider. The\nformula never mentions a model — the model is 100% the target role's provider. That's why\ntiering = setting role providers, and why it's portable across families.\n\nLaunch one:\n\n```\ngc bd create \"Add a --json flag to the export command\"\ngc sling run-operator <bead-id> --on build-basic\n```\n\nA single always-on **named session** that plans, dispatches, and monitors — one per city.\n\n`packs/core-pack/agents/mayor/agent.toml`\n\n:\n\n```\nname = \"mayor\"\nprovider = \"deep\"                                   # coordinator gets the strong tier\nappend_fragments = [\"mayor-operating-rhythm\", \"efficient-routing-rules\"]\nidle_timeout = \"1h\"                                 # idle instead of busy-looping\nwake_mode = \"fresh\"                                 # re-prime clean each wake — no context pile-up\nmax_active_sessions = 1\n```\n\nDeclared as always-on in `packs/core-pack/pack.toml`\n\n:\n\n```\n[[named_session]]\ntemplate = \"mayor\"\nmode = \"always\"\nscope = \"city\"\n```\n\ngives it a`prompt.template.md`\n\n*coordinator*prompt (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`\n\n- Because it's\n`deep`\n\n, the coordinator runs on Opus / Gemini Pro (High) automatically per city.\n\nReusable prompt blocks in `template-fragments/*.template.md`\n\n, defined as Go-template blocks\nand attached by **bare define-name**:\n\n`packs/core-pack/template-fragments/routing.template.md`\n\n:\n\n```\n{{ define \"efficient-routing-rules\" }}\n### Task Routing Rules\nRoute by how much reasoning the task needs, prefixing the target rig:\n\n    gc.routed_to=<rig>/<tier>\n\n- Trivial / mechanical  → fast\n- Standard implementation, review → solid\n- Non-trivial: planning, design, architecture → deep\n\nWhen asked to plan anything non-trivial, delegate it to a `deep` subagent —\ndo not plan it yourself.\n{{ end }}\n```\n\nTwo rules that bite everyone:\n\n**Reference by the bare define name**(`efficient-routing-rules`\n\n), not`<file>.<name>`\n\n.**The file must end in**(or legacy`.template.md`\n\n`.md.tmpl`\n\n). A plain`.md`\n\nfragment is silently ignored → \"template not found\".\n\n**Custom commands** live under a pack's `commands/`\n\nand are invoked as `gc <binding> <name>`\n\n(the import binding — `cc`\n\nfor core-pack here):\n\n```\ngc cc gen-model-tiers > model-tiers.toml   # the tier generator above\ngc cc clear-human-mail                     # flush a recipient's mail inbox\n```\n\n**Orders** pair a trigger with an action, evaluated by the controller each tick.\n`packs/core-pack/orders/mayor-hourly-sweep.toml`\n\n:\n\n```\n[order]\ndescription = \"Wake the mayor once an hour to sweep status and check running convoys.\"\ntrigger  = \"cooldown\"\ninterval = \"1h\"\nexec     = \"bash $GC_PACK_DIR/assets/scripts/mayor-hourly-sweep.sh\"\ntimeout  = \"30s\"\n```\n\nThe script finds the active mayor session and nudges it (`gc session nudge <id> \"...\" --delivery queue`\n\n) — pure plumbing; all judgment stays in the mayor's prompt.\n\n```\ngc config show                                   # fully-composed config\ngc config explain --json --provider deep | jq .  # trace a tier to its concrete model\ngc lint <pack>                                   # validate before merge\ngc reload                                         # apply config to a running city\ngc status                                         # city health\ngc session list / peek <name> / reset <name>     # observe / restart a session\ngc prime <agent> --strict                        # render an agent's prompt (catch typos/fallbacks)\ngc sling <role> <bead> --on build-basic          # launch the build factory\n```\n\n**Config change loop:** edit → `gc lint`\n\n→ `gc config explain`\n\n(confirm the resolved value)\n→ `gc reload`\n\n.\n\n**Tier the interface, bind per city.** Agents/formulas reference`fast`\n\n/`solid`\n\n/`deep`\n\n; 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`\n\nthe generated fragment in the city.Bare name on PATH for portability;`command`\n\nisn't var-expanded; env values are.`$VAR`\n\nonly for secret env values.**Fragments need** and are referenced by bare define-name.`.template.md`\n\n**A named session's display name is**(e.g.`<binding>.<name>`\n\n`cc.mayor`\n\n); to show it unprefixed, declare the`[[named_session]]`\n\nin the*city's own*pack with an explicit`name`\n\nand`template = \"<binding>.<agent>\"`\n\n.**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`\n\n.**Give the coordinator a real prompt** or it falls back to a worker loop and busy-polls.", "url": "https://wpnews.pro/news/running-gas-city-across-multiple-cities-and-providers", "canonical_source": "https://gist.github.com/thinkjones/4a6d9cb771668c89407843821816c3ba", "published_at": "2026-07-09 17:11:27+00:00", "updated_at": "2026-07-10 03:35:34.846263+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Gas City", "Gemini", "Claude", "Antigravity"], "alternates": {"html": "https://wpnews.pro/news/running-gas-city-across-multiple-cities-and-providers", "markdown": "https://wpnews.pro/news/running-gas-city-across-multiple-cities-and-providers.md", "text": "https://wpnews.pro/news/running-gas-city-across-multiple-cities-and-providers.txt", "jsonld": "https://wpnews.pro/news/running-gas-city-across-multiple-cities-and-providers.jsonld"}}