Skip to main content

Understanding Packs

Every reusable capability in Gas City comes from a pack. A pack tells Gas City what can be loaded: agents, named sessions, formulas, skills, commands, MCP configuration, defaults, and the files those definitions need while running. This guide has two parts:
  • The pack model: what a pack is, where its definitions live, and how imports become city behavior.
  • The registry workflow: how to find a pack with gc, inspect it, write the import, and validate the result.
The pack specification is the public source of truth for the exact format. This guide explains the same model in a more practical style.

The Pack Model

A pack is a directory with a pack.toml file. Only pack.toml is required, but most useful packs also include agents, prompt templates, formulas, commands, doctor checks, skills, MCP configuration, or support files. Here is a small pack:
review-pack/
  pack.toml
  agents/
    reviewer/
      agent.toml
      prompt.md
  formulas/
    review.toml
The pack.toml file names the pack:
[pack]
name = "review-pack"
schema = 2
version = "1.0.0"
The agent definition lives in agents/reviewer/agent.toml:
scope = "city"
provider = "codex"
default_sling_formula = "review"
The agent directory gives the agent its local name, reviewer. Because the directory contains prompt.md, the loader discovers that prompt by convention. If another city imports this pack, it does not need to copy prompt.md; the file still belongs to the pack that declared it. Every city also has a city pack: the pack rooted at the city directory, next to city.toml. In loader and spec language, this is also called the root pack. The city pack is where the city keeps reusable definitions, imports, and local pack metadata. If the loader cannot understand a pack’s schema, it stops and reports an error. That is deliberate: it is better to reject a pack whose format is not understood than to load only part of it.

Why Import A Pack?

Import a pack when you want to reuse behavior defined somewhere else without copying its files into your city. For example, a review pack might provide:
  • a reviewer agent
  • review formulas
  • prompt fragments
  • setup scripts
  • doctor checks
When the city pack imports the review pack, those definitions become available to the city according to the imported pack’s scope rules.
city pack
  pack.toml
    [imports.review] ──► review pack
                         agents/reviewer/
                         formulas/review.toml

Registries, Handles, And Sources

Registries are catalogs for reusable packs. A registry record tells gc the pack name, summary, version metadata, and source. A registry handle is a short command argument for a pack record. In main:gascity, main is the local registry name on this machine and gascity is the pack name inside that registry. main is not a keyword in pack.toml; another machine could call the same registry first-party or work. A source is the durable location written into checked-in TOML. Durable means the import does not depend on this machine’s registry name or cache layout. The registry helps you find the source, but the committed source is what another machine uses later. The distinction looks like this:
ValueExampleUsed in
Registry handlemain:gascitygc pack registry commands, such as search and show.
Durable sourcehttps://github.com/gastownhall/gascity-packs/tree/main/gascityChecked-in import TOML.
For a GitHub-hosted pack inside a repository, use a browser-dereferenceable tree URL:
[imports.gascity]
source = "https://github.com/gastownhall/gascity-packs/tree/main/gascity"
That source tells gc to clone the gastownhall/gascity-packs repository and use the gascity directory on the main branch as the pack root. The same URL also opens the pack directory in a browser.

City Imports And Rig Imports

A city-level import belongs to the city pack and appears at the top level of the city pack’s pack.toml:
[imports.gascity]
source = "https://github.com/gastownhall/gascity-packs/tree/main/gascity"
version = "^0.1"
If that pack defines a city-scoped agent named planner, the runtime agent is named:
planner
A rig-level import appears under the [[rigs]] table that needs it:
[[rigs]]
name = "checkout-service"
path = "../checkout-service"

[rigs.imports.gascity]
source = "https://github.com/gastownhall/gascity-packs/tree/main/gascity"
version = "^0.1"
If that same pack defines a rig-scoped agent named planner, the runtime agent is stamped with the rig name:
checkout-service/planner
The rig name becomes the identity prefix. The rig path is the filesystem location of the project. These are different pieces of information.

Agent Scope

An agent in a pack can say where it is allowed to load.
scope = "city"
provider = "codex"
prompt_template = "prompt.md"
The scope field has three useful states.
ScopeMeaning
omittedThe agent is eligible for city-level and rig-level loading.
cityThe agent loads only when the pack is imported at the city level.
rigThe agent loads only when the pack is imported for a rig.
The scope says where the definition is available. It does not name a particular rig. A rig-scoped agent becomes part of a rig only when that rig imports the pack.

Names

Agent names are local names. Import bindings do not become part of runtime agent names. If a city imports this dependency:
[imports.review_tools]
source = "../packs/review"
and the imported pack defines agents/reviewer/agent.toml, the runtime name is:
reviewer
Gas City uses the binding to find and order dependencies while loading config. It does not use the binding as a runtime namespace. If two packs define city-level agents with the same name, config load fails after fallback rules are applied. The same rule applies inside a single rig. Give one of the agents a different public name, or avoid importing both definitions onto the same surface.

Defaults And Patches

Defaults fill in blanks after packs have loaded. Root city defaults belong in city.toml, and pack-scoped defaults can be declared in pack.toml for agents loaded from that pack. Pack-scoped defaults follow the precedence rules in the pack spec: explicit agent fields win, bound imports preserve inherited pack defaults, and unbound legacy includes yield to root city defaults.
[agent_defaults]
default_sling_formula = "review"
This default applies only to agents whose default_sling_formula is still blank. If a pack explicitly sets the field on an agent, the explicit value wins. A patch changes an agent that already exists. It does not create a new agent.
[[patches.agent]]
name = "reviewer"
provider = "codex"
session_setup_append = ["tmux set status-left '[review]'"]
For a rig-scoped agent, use dir to select the rig identity prefix:
[[patches.agent]]
dir = "checkout-service"
name = "reviewer"
provider = "codex"
Here, dir is the rig name, not the rig path.

Loading Order

The loader applies packs, patches, and defaults in a deterministic order. The details matter when two layers set the same field. In simplified form, loading works like this:
1. Read `city.toml` and the city pack.
2. Load imported packs.
3. Apply pack-level agent patches inside each pack load.
4. Load city-level imports.
5. Apply city-level patches.
6. Load rig-level imports and stamp rig agents.
7. Apply rig overrides.
8. Apply pack globals.
9. Apply city agent defaults to fields that are still blank.
The later operation wins for replacement-style fields. Defaults are last, but they only fill blanks, so they do not override explicit values from earlier layers.

Choosing Where To Put A Change

When you customize a pack, choose the narrowest place that expresses what you mean.
If you want to…Put it here
Reuse another pack[imports.<binding>] with source and optional version.
Make a city-wide local policycity.toml defaults or patches.
Change one city-level imported agentcity.toml [[patches.agent]].
Change one rig-level imported agentThe rig’s [[rigs.overrides]] or a targeted city patch with dir.
Ship reusable behaviorThe pack’s own definitions and support files.
Pin an exact resolved dependencyThe lockfile, not the authored import.

Pack CLI

Find A Pack

Use registry search when you know the kind of capability you want but not the exact pack name.
$ gc pack registry search gascity
Example output:
Registry  Name     Latest  Description
main      gascity  0.1.0   Gas City planning and implementation workflow pack
The registry name and pack name form a registry handle for gc pack registry commands. To inspect that record:
$ gc pack registry show main:gascity
Example output:
Pack:        main:gascity
Description: Gas City planning and implementation workflow pack
Source:      https://github.com/gastownhall/gascity-packs/tree/main/gascity
Source kind: git
Latest:      0.1.0
Import commands:
  This version or later: gc import add https://github.com/gastownhall/gascity-packs/tree/main/gascity --name gascity --version '>=0.1.0'
  Exactly this version:  gc import add https://github.com/gastownhall/gascity-packs/tree/main/gascity --name gascity --version 0.1.0
Releases:
  0.1.0 v0.1.0 d3617d1319a
The Import commands lines are ready to paste. The first command accepts the shown release or any newer release that matches the constraint. The second command pins exactly the shown release. Both commands write durable import TOML using the Source line and the selected version; the registry handle stays out of the file.

Install Or Check Imports

After changing remote imports, install or repair the imported pack cache:
$ gc import install
That command resolves the declared imports, writes or repairs packs.lock, and materializes the imported packs in the local cache. Use gc import check when you want a read-only validation pass:
$ gc import check
gc import check reports missing, stale, or uncached import state and points back to gc import install when repair is needed. Registry commands remain discovery commands; they do not install or sync the authored import graph. After install/check succeeds, validate the composed configuration.
$ gc config show --validate
Then inspect the part of the city you expect the pack to provide. For example:
$ gc config show | rg 'planner'

Versioning And Locking

The [pack].version field is pack metadata. Import version selection is controlled by the importing file and by the lockfile, not by comparing [pack].version directly during load. With no version field, the import says “use this source” and leaves the exact selected revision to the installer and lockfile:
[imports.gascity]
source = "https://github.com/gastownhall/gascity-packs/tree/main/gascity"
A semver-style constraint says which compatible releases are acceptable:
[imports.gascity]
source = "https://github.com/gastownhall/gascity-packs/tree/main/gascity"
version = "^0.1"
An exact SHA pin says which revision must be used:
[imports.gascity]
source = "https://github.com/gastownhall/gascity-packs/tree/main/gascity"
version = "sha:d3617d1319a1206ac85f69ba024ec395c49c6f4b"
The authored import expresses the source and optional constraint. The lockfile records the exact resolved dependency state. Once the cache and lockfile are current, normal city loading uses the local resolved pack instead of re-fetching the remote source on every load.

Registry State Is Local

Registry commands manage local discovery state. Pack imports manage shared city state.
TaskCommand or file
See configured catalogsgc pack registry list
Refresh cached catalog recordsgc pack registry refresh
Search for a reusable packgc pack registry search
Inspect a registry recordgc pack registry show
Share a chosen dependency with the team[imports.<binding>] in checked-in TOML
Install or repair authored importsgc import install
Check installed import state without mutatinggc import check
Validate the composed citygc config show --validate
This separation keeps local discovery flexible without making shared config depend on the names or cache layout of one machine.
Last modified on June 3, 2026