Superseded by #360 — Pack/City Model v.next, which integrates the definition/deployment/site-binding separation proposed here into the full pack/city redesign.
Edited 2026-03-30 21:13 PDT to reflect feedback.
Note: This draft has been significantly revised. The migration of some fields or constructs from city.toml into separate artifacts has been factored out and will be covered in a subsequent issue.
Problem
Gas City does not currently draw a clear enough boundary between three different kinds of state:
- City definition: The shareable, versioned definition of a city that can be deployed and bound to multiple operating environments. This includes shared rigs, agents, formulas, packs, prompts, and policy.
- Local bindings: The local data that binds a city definition to a particular operating environment, such as a directory layout or port assignments.
- Runtime state: Mutable state produced and maintained by Gas City while doing its work. This includes
.gc/, .beads/, caches, logs, sockets, staged formulas, and work databases.
Those categories are not reflected consistently in the file and directory structure of a city. In particular, city.toml tends to mix city definition with local bindings, while runtime directories such as .gc/ and .beads/ are adjacent to configuration but conceptually distinct from it.
This makes it harder for humans to learn, understand, and reason about the system.
It also makes it hard to share a city definition across multiple team members or operating environments via a shared repository so that all team members can work in a consistent way.
Goals
-
Have the file and directory structure of a city cleanly reflect the separation of the city's shared definition, local bindings, and runtime state.
-
Preserve a model in which shared city content can be versioned and exchanged cleanly across multiple humans without forcing them to share local filesystem topology.
-
Make the resulting model simple to explain and reason about for users who are trying to understand what belongs in shared config, what belongs in local bindings, and what should be treated as runtime state.
Non-Goals
-
Redesigning .gc/, .beads/, or other runtime directories except where needed to distinguish runtime state clearly from shared definition or local bindings.
-
Enumerating every future local override use case in advance.
The goal of this note is to establish a clean model and identify some common examples, not to exhaustively specify every possible local customization.
Design
This proposal attempts to achieve clean state separation between the three kinds of state by making the file structure of a city's directory more aligned with those three categories. The primary offender is city.toml, which contains both definition and binding information.
State-separating city.toml
The current city.toml design uses a single monolithic file to hold both the definition and local bindings of the city. Some examples of local binding information that is contained in city.toml are:
- rig path bindings
- host-local ports, sockets, and bind addresses
- other values that attach the city definition to a specific local environment
This proposal introduces a peer file city.local.toml that will contain only local binding information.
city.local.toml lives at the root of the city, alongside city.toml.
Example:
my-city/
├── city.toml
├── city.local.toml
├── prompts/
├── formulas/
├── scripts/
├── packs/
└── .gc/
This keeps the local bindings layer close to the city definition while still making the distinction between shared definition and local bindings explicit.
The relationship between the two files is:
city.toml becomes the sole city definition file with no local bindings
city.local.toml is an optional overlay applied on top of city.toml for the current machine
- when both files define the same field,
city.local.toml wins
- for array-of-table sections such as
[[rigs]], entries are merged by identity key, such as name
city.local.toml may bind or override objects declared in city.toml, but it should not introduce new city-definition objects on its own
For example, a rig may be defined in city.toml and then bound to a local directory in city.local.toml:
# city.toml
[[rigs]]
name = "demo"
includes = ["packs/gastown"]
default_sling_target = "demo/polecat"
# city.local.toml
[[rigs]]
name = "demo"
path = "/Users/alice/src/demo"
This should be understood as an overlay mechanism, not as a second independent source of city definition.
In other words, city.local.toml does not redefine what the city is. It binds that city definition to one local environment.
Determining what goes in city.toml vs. city.local.toml
Although city.local.toml may technically override fields from city.toml, not all such overrides are equally healthy.
Conceptually, fields should be understood as belonging to one of three classes:
| Class |
Meaning |
Normal Home |
definition-native |
The field is part of the city definition itself and should normally be shared. |
city.toml |
binding-native |
The field primarily binds the city definition to one operating environment. |
city.local.toml |
escapable |
The field belongs to the city definition by default, but may be overridden locally as an explicit exception. |
city.toml, with optional override in city.local.toml |
Not every field can be classified with equal confidence today. The category is still useful because it describes the intended shape of the model, but only some fields are clear enough at this stage to support a high-confidence classification.
Definition-native fields are part of the city definition itself and should normally live only in city.toml.
# city.toml
[workspace]
name = "my-city" # definition-native - defines the identity of the city
Binding-native fields primarily attach the city definition to one operating environment. When both files are shown, the city-side snippet shows the shared context and the local-side snippet shows the actual binding.
# city.toml
[[rigs]]
name = "demo" # definition-native - declares the name and existence of a rig
includes = ["packs/gastown"] # definition-native - determines which shared pack behavior is applied to the rig
# city.local.toml
[[rigs]]
name = "demo"
path = "/Users/alice/src/demo" # binding-only - binds the rig to a local directory
Escapable fields belong to the city definition by default, but some teams may choose to override them locally as an explicit exception.
# city.toml
[api]
port = 9443 # escapable - specifies a default port that will work in some or many deployments of this city
# city.local.toml
[api]
port = 19443 # the override of the escapable field
This [api].port example illustrates the intended shape of an escapable field: a city may want a shared default, while still permitting a local override when a machine-specific conflict exists. This note treats that pattern as real even though the appendix remains conservative about which fields are already clear enough to classify with high confidence.
Which fields are definition-native, binding-native, or escapable?
The Gas City implementation may allow broad overlay behavior, but the user should still be intentional when putting fields in city.toml, city.local.toml, or both.
At a high level, fields should be considered definition-native when they primarily describe what the city is, binding-native when they primarily attach that city definition to a particular operating environment, and escapable when they are definition-owned by default but can be overridden locally without immediately making the model incoherent.
There are additional criteria that should guide which class a given field is:
- Identity and structural integrity. If allowing local divergence would make two users feel like they are no longer using the same city, the field should remain definition-native.
- Fragility. Fields that affect routing, durable naming, interoperability, or externally visible endpoints deserve extra caution because silent divergence there is especially confusing.
- Predictability. If localizing a field would create surprising differences between teammates who believe they are sharing one city definition, it should not be casually treated as a local binding.
- Supportability. If varying a field from machine to machine would make the system materially harder to explain, debug, reproduce, or support, it should tend toward definition-native.
The hardest cases are not the obviously shared ones or the obviously local ones, but the fields that sit near the boundary between behavior and attachment. Those are the cases where these principles matter most, especially when asking whether local divergence preserves city identity and structural integrity and whether allowing that divergence makes the system more fragile.
Ultimately, it sometimes comes down to judgement. To that end, the exact detection and enforcement model is out of scope for this note. Appendix A lists some fairly obvious characterizations, but at this stage in the design, we are not tackling either a normative characterization of every field, nor a mechanism for enforcement (e.g., lint-like analysis, runtime enforcement)
Beyond city.toml
The state-separation problem is not limited to fields embedded in city.toml. A city root already contains other files and directories that behave as shared definition, local bindings, or runtime state, and the model needs to describe them consistently.
At a high level:
city.toml is the shared city definition file.
city.local.toml is the local bindings overlay.
- directories such as
prompts/, formulas/, orders/, scripts/, and packs/ are shared definition assets that live alongside city.toml.
.gc/ and .beads/ are runtime state, not city definition or local bindings.
The current and intended roles of the city-root artifacts can be summarized more completely like this.
Top-level city-root artifacts
| Path |
Current Role |
Intended Role In This Proposal |
Notes |
city.toml |
Mixed definition and local bindings |
Shared city definition |
The main file being cleaned up by this proposal. |
city.local.toml |
New |
Local bindings |
Optional local overlay applied on top of city.toml. |
prompts/ |
Shared definition asset |
Shared definition asset |
Prompt content is part of the shared city definition, not a local binding. |
formulas/ |
Shared definition asset |
Shared definition asset |
Formula files remain shared city definition. |
orders/ |
Shared definition asset |
Shared definition asset |
Order definitions remain part of the shared city definition. |
scripts/ |
Shared definition asset |
Shared definition asset |
Shared helper scripts remain part of city definition. |
packs/ |
Shared definition asset |
Shared definition asset |
Pack definitions remain shared and versioned. |
hooks/ |
Generated city-adjacent config |
Local bindings |
Under the current model, hooks/ is treated as machine-local generated hook configuration. If Gas City later grows a notion of shared, authored hook definitions, that should be handled separately rather than reusing this generated directory implicitly. |
.gc/ |
Runtime state root |
Runtime state root |
Hidden root for mutable city-owned runtime state. |
.beads/ |
Runtime state root |
Runtime state root |
Hidden root for city-adjacent work-store state. |
Given this, a reasonable .gitignore file for the city's root directory would be:
# Local bindings
city.local.toml
hooks/
# Runtime state
.gc/
.beads/
Appendix A: High-Confidence Candidate Field Classifications Within city.toml (non-normative)
If we intend to implement any enforcement mechanisms to make sure fields go into the right file, we would need to begin by bootstrapping the list of fields that would warrant enforcement.
Here is an initial conservative attempt at classification, but should be considered non-normative. It includes only fields whose likely classification is relatively clear at this stage of the design. It was a specific non-goal to fully enumerate every field here, so a complete classification would likely need to be a separate proposal. This note is focused purely on establishing the model.
| Field |
Likely Classification |
Confidence |
Notes |
[workspace].name |
definition-native |
High |
Defines the identity of the city itself and should not vary per machine. |
[[rigs]].name |
definition-native |
High |
Declares the identity and existence of a rig within the city. |
[[rigs]].includes |
definition-native |
High |
Determines which shared pack behavior is applied to a rig and therefore changes the shared city definition. |
[[rigs]].path |
binding-native |
High |
The clearest local binding in the current model. |
[[rigs]].formulas_dir |
maybe binding-native, but not definition-native |
Medium |
Likely local when it points into repo-local or machine-specific layout; shared when it points to city-owned formula assets. |
[session].socket |
binding-native |
High |
Socket names attach to local tmux/runtime state on one machine. |
[api].bind |
binding-native |
High |
Bind address attaches to one host's network interfaces. |
[api].port |
maybe binding-native or escapable, but not definition-native |
Medium |
Often host-local, but some teams may want a stable shared port convention. |
[dolt].host |
binding-native |
High |
Hostname is fundamentally local attachment information. |
[dolt].port |
maybe binding-native or escapable, but not definition-native |
Medium |
Similar to API port: often host-local, sometimes treated as shared convention. |
[daemon].observe_paths |
binding-native |
High |
Explicitly machine filesystem paths. |
[workspace].provider |
maybe binding-native or escapable, but not definition-native |
Medium |
Shared when it expresses team default behavior; local when different humans use different default providers. |
[workspace].start_command |
maybe binding-native or escapable, but not definition-native |
Medium |
Shared when it defines team-standard startup; local when it wraps host-specific binaries or auth setup. |
No high-confidence escapable fields are listed yet. The fields that seem closest to escapable, such as [api].port, [dolt].port, [workspace].provider, and [workspace].start_command, still look context-dependent enough that they are left at medium confidence for now.
Subsequent Work
- Reconciling the broader city/pack model is handled separately in a subsequent issue (in flight).
Edited 2026-03-30 21:13 PDT to reflect feedback.
Note: This draft has been significantly revised. The migration of some fields or constructs from
city.tomlinto separate artifacts has been factored out and will be covered in a subsequent issue.Problem
Gas City does not currently draw a clear enough boundary between three different kinds of state:
.gc/,.beads/, caches, logs, sockets, staged formulas, and work databases.Those categories are not reflected consistently in the file and directory structure of a city. In particular,
city.tomltends to mix city definition with local bindings, while runtime directories such as.gc/and.beads/are adjacent to configuration but conceptually distinct from it.This makes it harder for humans to learn, understand, and reason about the system.
It also makes it hard to share a city definition across multiple team members or operating environments via a shared repository so that all team members can work in a consistent way.
Goals
Have the file and directory structure of a city cleanly reflect the separation of the city's shared definition, local bindings, and runtime state.
Preserve a model in which shared city content can be versioned and exchanged cleanly across multiple humans without forcing them to share local filesystem topology.
Make the resulting model simple to explain and reason about for users who are trying to understand what belongs in shared config, what belongs in local bindings, and what should be treated as runtime state.
Non-Goals
Redesigning
.gc/,.beads/, or other runtime directories except where needed to distinguish runtime state clearly from shared definition or local bindings.Enumerating every future local override use case in advance.
The goal of this note is to establish a clean model and identify some common examples, not to exhaustively specify every possible local customization.
Design
This proposal attempts to achieve clean state separation between the three kinds of state by making the file structure of a city's directory more aligned with those three categories. The primary offender is
city.toml, which contains both definition and binding information.State-separating
city.tomlThe current
city.tomldesign uses a single monolithic file to hold both the definition and local bindings of the city. Some examples of local binding information that is contained incity.tomlare:This proposal introduces a peer file
city.local.tomlthat will contain only local binding information.city.local.tomllives at the root of the city, alongsidecity.toml.Example:
This keeps the local bindings layer close to the city definition while still making the distinction between shared definition and local bindings explicit.
The relationship between the two files is:
city.tomlbecomes the sole city definition file with no local bindingscity.local.tomlis an optional overlay applied on top ofcity.tomlfor the current machinecity.local.tomlwins[[rigs]], entries are merged by identity key, such asnamecity.local.tomlmay bind or override objects declared incity.toml, but it should not introduce new city-definition objects on its ownFor example, a rig may be defined in
city.tomland then bound to a local directory incity.local.toml:This should be understood as an overlay mechanism, not as a second independent source of city definition.
In other words,
city.local.tomldoes not redefine what the city is. It binds that city definition to one local environment.Determining what goes in
city.tomlvs.city.local.tomlAlthough
city.local.tomlmay technically override fields fromcity.toml, not all such overrides are equally healthy.Conceptually, fields should be understood as belonging to one of three classes:
definition-nativecity.tomlbinding-nativecity.local.tomlescapablecity.toml, with optional override incity.local.tomlNot every field can be classified with equal confidence today. The category is still useful because it describes the intended shape of the model, but only some fields are clear enough at this stage to support a high-confidence classification.
Definition-native fields are part of the city definition itself and should normally live only in
city.toml.Binding-native fields primarily attach the city definition to one operating environment. When both files are shown, the city-side snippet shows the shared context and the local-side snippet shows the actual binding.
Escapable fields belong to the city definition by default, but some teams may choose to override them locally as an explicit exception.
This
[api].portexample illustrates the intended shape of anescapablefield: a city may want a shared default, while still permitting a local override when a machine-specific conflict exists. This note treats that pattern as real even though the appendix remains conservative about which fields are already clear enough to classify with high confidence.Which fields are definition-native, binding-native, or escapable?
The Gas City implementation may allow broad overlay behavior, but the user should still be intentional when putting fields in
city.toml,city.local.toml, or both.At a high level, fields should be considered definition-native when they primarily describe what the city is, binding-native when they primarily attach that city definition to a particular operating environment, and escapable when they are definition-owned by default but can be overridden locally without immediately making the model incoherent.
There are additional criteria that should guide which class a given field is:
The hardest cases are not the obviously shared ones or the obviously local ones, but the fields that sit near the boundary between behavior and attachment. Those are the cases where these principles matter most, especially when asking whether local divergence preserves city identity and structural integrity and whether allowing that divergence makes the system more fragile.
Ultimately, it sometimes comes down to judgement. To that end, the exact detection and enforcement model is out of scope for this note. Appendix A lists some fairly obvious characterizations, but at this stage in the design, we are not tackling either a normative characterization of every field, nor a mechanism for enforcement (e.g., lint-like analysis, runtime enforcement)
Beyond
city.tomlThe state-separation problem is not limited to fields embedded in
city.toml. A city root already contains other files and directories that behave as shared definition, local bindings, or runtime state, and the model needs to describe them consistently.At a high level:
city.tomlis the shared city definition file.city.local.tomlis the local bindings overlay.prompts/,formulas/,orders/,scripts/, andpacks/are shared definition assets that live alongsidecity.toml..gc/and.beads/are runtime state, not city definition or local bindings.The current and intended roles of the city-root artifacts can be summarized more completely like this.
Top-level city-root artifacts
city.tomlcity.local.tomlcity.toml.prompts/formulas/orders/scripts/packs/hooks/hooks/is treated as machine-local generated hook configuration. If Gas City later grows a notion of shared, authored hook definitions, that should be handled separately rather than reusing this generated directory implicitly..gc/.beads/Given this, a reasonable
.gitignorefile for the city's root directory would be:Appendix A: High-Confidence Candidate Field Classifications Within
city.toml(non-normative)If we intend to implement any enforcement mechanisms to make sure fields go into the right file, we would need to begin by bootstrapping the list of fields that would warrant enforcement.
Here is an initial conservative attempt at classification, but should be considered non-normative. It includes only fields whose likely classification is relatively clear at this stage of the design. It was a specific non-goal to fully enumerate every field here, so a complete classification would likely need to be a separate proposal. This note is focused purely on establishing the model.
[workspace].namedefinition-native[[rigs]].namedefinition-native[[rigs]].includesdefinition-native[[rigs]].pathbinding-native[[rigs]].formulas_dirbinding-native, but notdefinition-native[session].socketbinding-native[api].bindbinding-native[api].portbinding-nativeorescapable, but notdefinition-native[dolt].hostbinding-native[dolt].portbinding-nativeorescapable, but notdefinition-native[daemon].observe_pathsbinding-native[workspace].providerbinding-nativeorescapable, but notdefinition-native[workspace].start_commandbinding-nativeorescapable, but notdefinition-nativeNo high-confidence
escapablefields are listed yet. The fields that seem closest toescapable, such as[api].port,[dolt].port,[workspace].provider, and[workspace].start_command, still look context-dependent enough that they are left at medium confidence for now.Subsequent Work