Problem
Agent definitions are split across [[agent]] TOML tables and filesystem assets (prompts, overlays, scripts) scattered in separate directory trees. This creates six problems:
-
Scattered identity. There's no single place to understand what an agent is. Adding an agent means editing city.toml and creating files in multiple directories (prompts/, overlays/, scripts/).
-
Invisible prompt injection. Every .md file is secretly a Go template. Fragments get injected via global_fragments and inject_fragments without appearing in the prompt file itself. You can't read a prompt and know what the agent actually sees.
-
Provider files leak across providers. Overlay files (.claude/settings.json, CLAUDE.md) get copied into every agent's working directory regardless of which provider the agent uses. A Codex agent gets Claude's settings.
-
No home for skills or MCP servers. The Agent Skills standard is adopted by 30+ tools (Claude Code, Codex, Gemini, Cursor, Copilot, etc.), but Gas City has no convention for shipping skills with a pack. MCP server config is provider-specific JSON baked into overlay files with no abstraction.
-
Definition vs. modification conflated. There's no separation between "I'm defining my own agent" and "I'm tweaking an imported agent." Both use [[agent]] tables, and collision resolution depends on load order and fallback flags.
-
Ad hoc asset wiring. Overlays, prompts, and scripts each have their own mechanism (overlay_dir, prompt_template, scripts_dir). There's no consistent pattern.
Proposed change: agents as directories
Agents are defined by convention: a directory in agents/ with at least a prompt.md file. All additional assets live in the agent's directory, as does any configuration in an optional agent.toml file.
Minimal agent — just a prompt, inherits all defaults:
agents/polecat/
└── prompt.md
Agent with config overrides:
agents/mayor/
├── agent.toml # optional — overrides defaults
└── prompt.md # required — the system prompt
Fully configured agent with per-agent assets:
agents/mayor/
├── agent.toml # optional — overrides defaults
├── prompt.md # required — the system prompt
├── namepool.txt # optional — display names for pool sessions
├── overlay/ # optional — agent-specific overlay files
│ ├── AGENTS.md # provider-agnostic instructions (copied for all providers)
│ └── per-provider/
│ └── claude/
├── skills/ # optional — agent-specific skills
├── mcp/ # optional — agent-specific MCP servers
└── template-fragments/ # optional — agent-specific prompt fragments
Full city with city-wide assets and multiple agents:
my-city/
├── city.toml
├── agents/
│ ├── polecat/
│ │ └── prompt.md
│ └── mayor/
│ ├── agent.toml
│ └── prompt.md
├── overlays/ # city-wide overlays (all agents)
│ ├── per-provider/
│ │ ├── claude/
│ │ │ ├── .claude/
│ │ │ │ └── settings.json
│ │ │ └── CLAUDE.md
│ │ └── codex/
│ │ └── AGENTS.md
│ └── .editorconfig # provider-agnostic (all agents)
├── skills/ # city-wide skills (all agents)
├── mcp/ # city-wide MCP servers (all agents)
├── template-fragments/ # city-wide prompt template fragments
├── formulas/
├── scripts/
├── commands/
└── doctor/
city.toml: agent defaults
[[agent]] tables are replaced by an [agents] section for city-wide defaults:
# city.toml
[agents]
provider = "claude"
model = "claude-sonnet-4-20250514"
scope = "rig"
These apply to every agent in the city. Individual agents override in their own agent.toml:
# agents/mayor/agent.toml — only what differs from defaults
scope = "city"
max_active_sessions = 1
A minimal agent (directory with just prompt.md) inherits all defaults and needs no agent.toml.
Pool agents
Pool behavior is config, not structure. A pool agent is an agent that spawns multiple concurrent sessions from the same definition — useful when work arrives faster than a single session can handle. The controller scales sessions up and down based on demand, within the configured bounds:
# agents/polecat/agent.toml
min_active_sessions = 1
max_active_sessions = 3
If the agent's directory contains a namepool.txt file (one name per line), each session gets a name from it as a display alias — no TOML field needed, same convention-over-configuration approach as prompt.md. All instances share the same prompt, skills, MCP servers, and overlays — they differ only in their session identity and working directory.
Provider-aware overlays
Overlays are files materialized into the agent's working directory before it starts. Provider-specific files live in per-provider/ subdirectories so agents only get files for their provider.
Layering order (later wins on file collision):
- City-wide
overlays/ — universal files (everything outside per-provider/)
- City-wide
overlays/per-provider/<provider>/ — provider-matched
- Agent-specific
agents/<name>/overlay/ — universal files
- Agent-specific
agents/<name>/overlay/per-provider/<provider>/ — provider-matched
The <provider> name matches the Gas City provider name (claude, codex, cursor, etc.). Switching an agent's provider changes which overlay files apply — no manual cleanup.
This means a city can ship distinct CLAUDE.md and AGENTS.md files for different providers, and each agent only sees the one for its provider.
Skills
Skills use the Agent Skills open standard, adopted by 30+ providers including Claude Code, Codex, Gemini, Cursor, GitHub Copilot, JetBrains Junie, Goose, Roo Code, and many more.
A skill is a directory containing a SKILL.md file (YAML frontmatter + markdown instructions) with optional scripts/, references/, and assets/ subdirectories:
skills/code-review/
├── SKILL.md # required: metadata + instructions
├── scripts/ # optional: executable code
├── references/ # optional: documentation
└── assets/ # optional: templates, resources
# SKILL.md frontmatter
---
name: code-review
description: Reviews code changes for bugs, security issues, and style. Use when reviewing PRs or changed files.
---
Skills are portable across providers. The same SKILL.md works with Claude Code, Codex, Gemini, and any other compliant agent. Gas City materializes skills into the provider-expected location in the agent's working directory at startup (e.g., .claude/skills/ for Claude Code, .agents/skills/ for Codex).
Skills can be city-wide or per-agent:
my-city/
├── skills/ # city-wide — available to all agents
│ ├── code-review/
│ │ └── SKILL.md
│ └── test-runner/
│ ├── SKILL.md
│ └── scripts/
│ └── run-tests.sh
├── agents/
│ └── polecat/
│ └── skills/ # agent-specific — only this agent
│ └── polecat-workflow/
│ └── SKILL.md
An agent gets city-wide skills + its own skills. Agent-specific wins on name collision.
Skill promotion
When an agent creates a skill during a session (in the rig's working directory), it stays local to that rig. To bring it into the city definition:
gc skill promote code-review --to city # copies to city's skills/
gc skill promote code-review --to agent mayor # copies to agents/mayor/skills/
Promoting is an explicit human decision — skills don't automatically flow from rigs back to the city.
MCP servers
MCP (Model Context Protocol) servers provide tools, resources, and prompts to agents over a runtime protocol. Unlike skills (which have a portable file standard), MCP server configuration is provider-specific — each provider embeds it in its own settings file. Gas City abstracts this with a provider-agnostic TOML format.
Definition format
An MCP server is a named TOML file in mcp/:
# mcp/beads-health.toml
name = "beads-health"
description = "Query bead status and health metrics"
command = "scripts/mcp-beads-health.sh"
args = ["--city-root", "."]
[env]
BEADS_DB = ".beads"
For template expansion (dynamic paths, credentials), use .toml.tmpl:
# mcp/beads-health.toml.tmpl
name = "beads-health"
description = "Query bead status and health metrics"
command = "scripts/mcp-beads-health.sh"
args = ["--city-root", "{{.CityRoot}}"]
[env]
BEADS_DB = "{{.RigRoot}}/.beads"
Same .tmpl rule as prompts — plain .toml is static, .toml.tmpl goes through Go template expansion with PromptContext variables.
Remote MCP servers use url instead of command:
# mcp/sentry.toml.tmpl
name = "sentry"
description = "Sentry error tracking integration"
url = "https://mcp.sentry.io/sse"
[headers]
Authorization = "Bearer {{.SENTRY_TOKEN}}"
Field spec
| Field |
Required |
Description |
name |
Yes |
Server name (must match filename without extension) |
description |
Yes |
What this server provides |
command |
Yes* |
Command to launch local server (stdio transport) |
args |
No |
Arguments to the command |
url |
Yes* |
URL for remote server (HTTP transport) |
headers |
No |
HTTP headers for remote server |
[env] |
No |
Environment variables passed to local server |
*One of command or url is required.
What Gas City does at agent startup
- Collects all MCP server definitions for this agent (city-wide + agent-specific)
- Template-expands any
.toml.tmpl files
- Resolves
command paths to absolute paths (scripts are NOT copied to the rig)
- Injects into the provider's config format:
- Claude Code: merges into
.claude/settings.json mcpServers
- Cursor: merges into
.cursor/mcp.json mcpServers
- VS Code/Copilot: merges into VS Code settings
- Others: provider-specific mapping as supported
Each MCP server is a separate file, so multiple packs' MCP servers merge cleanly — no last-writer-wins on a single settings file.
Prompts and templates
.tmpl suffix required for template processing. prompt.md is plain markdown — no template engine runs. prompt.md.tmpl goes through Go text/template. No more "everything is secretly a template."
This applies to all file types, not just prompts. If a file needs template expansion, it has .tmpl. If it doesn't, it doesn't.
Template fragments
Fragments are reusable chunks of prompt content. They are named Go templates defined in .md.tmpl files:
{{ define "command-glossary" }}
Use `/gc-work`, `/gc-dispatch`, `/gc-agents`, `/gc-rigs`, `/gc-mail`,
or `/gc-city` to load command reference for any topic.
{{ end }}
Fragments live in template-fragments/ at city or pack level:
my-city/
├── template-fragments/
│ ├── command-glossary.md.tmpl
│ ├── operational-awareness.md.tmpl
│ └── tdd-discipline.md.tmpl
├── agents/
│ ├── mayor/
│ │ └── prompt.md.tmpl
│ └── polecat/
│ └── prompt.md
An agent whose prompt is .md.tmpl can pull in fragments explicitly:
# Mayor
You are the mayor of this city.
{{ template "operational-awareness" . }}
---
{{ template "command-glossary" . }}
An agent whose prompt is plain .md cannot use fragments — no template engine runs.
What this replaces:
| Current mechanism |
New model |
global_fragments in workspace config |
Gone — each prompt explicitly includes what it needs |
inject_fragments on agent config |
Gone — same reason |
inject_fragments_append on patches |
Gone — same reason |
prompts/shared/*.md.tmpl |
template-fragments/*.md.tmpl at city level |
All .md files run through Go templates |
Only .md.tmpl files run through Go templates |
The three-layer injection pipeline (inline templates → global_fragments → inject_fragments) collapses to one: explicit {{ template "name" . }} in the .md.tmpl file. The prompt file is the single source of truth for what the agent sees.
Auto-append (opt-in)
For migration and convenience, an agent can opt into auto-appending fragments via append_fragments in agent.toml:
# agents/polecat/agent.toml
append_fragments = ["operational-awareness", "command-glossary"]
City-wide defaults can set this for all agents:
# city.toml
[agents]
append_fragments = ["operational-awareness", "command-glossary"]
append_fragments only works on .md.tmpl prompts. Plain .md prompts are inert — nothing is injected, no template engine runs.
Implicit agents
Gas City provides a built-in agent for each configured provider (claude, codex, gemini, etc.) so that gc sling claude "do something" works immediately after gc init with no agent configuration.
Implicit agents follow the same directory convention. They are materialized from the gc binary into .gc/system/agents/:
.gc/system/agents/
├── claude/
│ └── prompt.md
├── codex/
│ └── prompt.md
└── gemini/
└── prompt.md
Shadowing: A user-defined agent with the same name wins over the system implicit. Priority chain (lowest to highest):
- System implicit (
.gc/system/agents/) — bare minimum, always exists
- Pack-defined (
agents/claude/ in a pack) — overrides system
- City-defined (
agents/claude/ in the city) — overrides packs
Agent patches
Patches modify imported agents without defining new ones. They are distinct from agent definitions — agents/<name>/ always creates YOUR agent; patches modify SOMEONE ELSE's agent.
Config-only patch — override agent.toml fields by qualified name:
# city.toml
[[patches.agent]]
name = "gastown.mayor"
model = "claude-opus-4-20250514"
max_active_sessions = 2
[patches.agent.env]
REVIEW_MODE = "strict"
Prompt replacement — redirect to a file in your city's patches/ directory:
[[patches.agent]]
name = "gastown.mayor"
prompt = "gastown-mayor-prompt.md" # relative to patches/
my-city/
├── city.toml
├── agents/ # YOUR agents only
└── patches/ # all patch-related files
└── gastown-mayor-prompt.md
Key design decisions:
agents/<name>/ = new agent. [[patches.agent]] = modify imported agent. Never conflated.
- Patches target by qualified name (
gastown.mayor). Bare names work when unambiguous.
- File-level: prompt replacement only for now. Skills, MCP, overlays deferred.
Rig patches
Rig patches are agent patches scoped to one rig. They live in city.toml alongside the rig declaration:
# city.toml
[[rigs]]
name = "api-server"
# polecat in api-server gets 2 sessions; other rigs unaffected
[[rigs.patches]]
agent = "gastown.polecat"
max_active_sessions = 2
Same fields as agent patches, same qualified naming, same semantics. The only difference is scope:
| Mechanism |
Where |
Scope |
| Agent patches |
[[patches.agent]] in city.toml |
All rigs |
| Rig patches |
[[rigs.patches]] in city.toml |
One rig only |
Application order (later wins):
- Agent definition (from
agents/ directory)
- Pack-level agent patches (from pack's
[[patches.agent]])
- City-level agent patches (from city.toml
[[patches.agent]])
- Rig patches (from city.toml
[[rigs.patches]])
A rig patch can undo a city-level patch for that one rig.
Alternatives considered
- Keep
[[agent]] tables, add asset conventions alongside. Doesn't solve scattered identity — two parallel declaration mechanisms is worse than one.
- Provider-specific overlay via separate
overlay_dir fields per provider. Doesn't compose when multiple packs contribute overlays.
- Ship MCP config as raw provider JSON in overlays. Current approach. Doesn't compose across packs (last-writer-wins on settings.json), duplicates across providers.
- Build a custom skills system. Agent Skills is already adopted by 30+ tools. Building our own creates a walled garden.
Scope and impact
- Breaking:
[[agent]] tables move to agents/ directories. Migration tooling needed.
- Config: city.toml gains
[agents] defaults section, loses [[agent]] tables. agent.toml is new per-agent.
- Prompts:
.tmpl suffix becomes required for template processing. Existing .md prompts using {{ need renaming.
- New features: Skills, MCP TOML abstraction,
per-provider/ overlays, template-fragments/ convention, patches/ directory.
- Naming: Current
[[rigs.overrides]] renamed to [[rigs.patches]] for consistency with [[patches.agent]].
- Docs: Tutorials and reference docs need updates.
Open questions
- Skill lifecycle: Should agent-created skills auto-promote, stay local to the rig, or require explicit
gc skill promote? Current design says explicit.
- Provider-named agents: Must
agents/claude/ use provider = "claude", or is naming just convention?
- Suppressing implicit agents: How does a city say "I configure claude as a provider but don't want an implicit
claude agent"?
- Patch directory structure: Flat
patches/ or namespaced by target pack?
- Patches vs. overrides naming: This proposal unifies on "patches" everywhere. Alternative: unify on "overrides" everywhere. The key property is that the mechanism is the same regardless of scope.
Problem
Agent definitions are split across
[[agent]]TOML tables and filesystem assets (prompts, overlays, scripts) scattered in separate directory trees. This creates six problems:Scattered identity. There's no single place to understand what an agent is. Adding an agent means editing city.toml and creating files in multiple directories (
prompts/,overlays/,scripts/).Invisible prompt injection. Every
.mdfile is secretly a Go template. Fragments get injected viaglobal_fragmentsandinject_fragmentswithout appearing in the prompt file itself. You can't read a prompt and know what the agent actually sees.Provider files leak across providers. Overlay files (
.claude/settings.json,CLAUDE.md) get copied into every agent's working directory regardless of which provider the agent uses. A Codex agent gets Claude's settings.No home for skills or MCP servers. The Agent Skills standard is adopted by 30+ tools (Claude Code, Codex, Gemini, Cursor, Copilot, etc.), but Gas City has no convention for shipping skills with a pack. MCP server config is provider-specific JSON baked into overlay files with no abstraction.
Definition vs. modification conflated. There's no separation between "I'm defining my own agent" and "I'm tweaking an imported agent." Both use
[[agent]]tables, and collision resolution depends on load order andfallbackflags.Ad hoc asset wiring. Overlays, prompts, and scripts each have their own mechanism (
overlay_dir,prompt_template,scripts_dir). There's no consistent pattern.Proposed change: agents as directories
Agents are defined by convention: a directory in
agents/with at least aprompt.mdfile. All additional assets live in the agent's directory, as does any configuration in an optionalagent.tomlfile.Minimal agent — just a prompt, inherits all defaults:
Agent with config overrides:
Fully configured agent with per-agent assets:
Full city with city-wide assets and multiple agents:
city.toml: agent defaults
[[agent]]tables are replaced by an[agents]section for city-wide defaults:These apply to every agent in the city. Individual agents override in their own
agent.toml:A minimal agent (directory with just
prompt.md) inherits all defaults and needs noagent.toml.Pool agents
Pool behavior is config, not structure. A pool agent is an agent that spawns multiple concurrent sessions from the same definition — useful when work arrives faster than a single session can handle. The controller scales sessions up and down based on demand, within the configured bounds:
If the agent's directory contains a
namepool.txtfile (one name per line), each session gets a name from it as a display alias — no TOML field needed, same convention-over-configuration approach asprompt.md. All instances share the same prompt, skills, MCP servers, and overlays — they differ only in their session identity and working directory.Provider-aware overlays
Overlays are files materialized into the agent's working directory before it starts. Provider-specific files live in
per-provider/subdirectories so agents only get files for their provider.Layering order (later wins on file collision):
overlays/— universal files (everything outsideper-provider/)overlays/per-provider/<provider>/— provider-matchedagents/<name>/overlay/— universal filesagents/<name>/overlay/per-provider/<provider>/— provider-matchedThe
<provider>name matches the Gas City provider name (claude,codex,cursor, etc.). Switching an agent's provider changes which overlay files apply — no manual cleanup.This means a city can ship distinct
CLAUDE.mdandAGENTS.mdfiles for different providers, and each agent only sees the one for its provider.Skills
Skills use the Agent Skills open standard, adopted by 30+ providers including Claude Code, Codex, Gemini, Cursor, GitHub Copilot, JetBrains Junie, Goose, Roo Code, and many more.
A skill is a directory containing a
SKILL.mdfile (YAML frontmatter + markdown instructions) with optionalscripts/,references/, andassets/subdirectories:Skills are portable across providers. The same SKILL.md works with Claude Code, Codex, Gemini, and any other compliant agent. Gas City materializes skills into the provider-expected location in the agent's working directory at startup (e.g.,
.claude/skills/for Claude Code,.agents/skills/for Codex).Skills can be city-wide or per-agent:
An agent gets city-wide skills + its own skills. Agent-specific wins on name collision.
Skill promotion
When an agent creates a skill during a session (in the rig's working directory), it stays local to that rig. To bring it into the city definition:
Promoting is an explicit human decision — skills don't automatically flow from rigs back to the city.
MCP servers
MCP (Model Context Protocol) servers provide tools, resources, and prompts to agents over a runtime protocol. Unlike skills (which have a portable file standard), MCP server configuration is provider-specific — each provider embeds it in its own settings file. Gas City abstracts this with a provider-agnostic TOML format.
Definition format
An MCP server is a named TOML file in
mcp/:For template expansion (dynamic paths, credentials), use
.toml.tmpl:Same
.tmplrule as prompts — plain.tomlis static,.toml.tmplgoes through Go template expansion withPromptContextvariables.Remote MCP servers use
urlinstead ofcommand:Field spec
namedescriptioncommandargsurlheaders[env]*One of
commandorurlis required.What Gas City does at agent startup
.toml.tmplfilescommandpaths to absolute paths (scripts are NOT copied to the rig).claude/settings.jsonmcpServers.cursor/mcp.jsonmcpServersEach MCP server is a separate file, so multiple packs' MCP servers merge cleanly — no last-writer-wins on a single settings file.
Prompts and templates
.tmplsuffix required for template processing.prompt.mdis plain markdown — no template engine runs.prompt.md.tmplgoes through Gotext/template. No more "everything is secretly a template."This applies to all file types, not just prompts. If a file needs template expansion, it has
.tmpl. If it doesn't, it doesn't.Template fragments
Fragments are reusable chunks of prompt content. They are named Go templates defined in
.md.tmplfiles:{{ define "command-glossary" }} Use `/gc-work`, `/gc-dispatch`, `/gc-agents`, `/gc-rigs`, `/gc-mail`, or `/gc-city` to load command reference for any topic. {{ end }}Fragments live in
template-fragments/at city or pack level:An agent whose prompt is
.md.tmplcan pull in fragments explicitly:An agent whose prompt is plain
.mdcannot use fragments — no template engine runs.What this replaces:
global_fragmentsin workspace configinject_fragmentson agent configinject_fragments_appendon patchesprompts/shared/*.md.tmpltemplate-fragments/*.md.tmplat city level.mdfiles run through Go templates.md.tmplfiles run through Go templatesThe three-layer injection pipeline (inline templates → global_fragments → inject_fragments) collapses to one: explicit
{{ template "name" . }}in the.md.tmplfile. The prompt file is the single source of truth for what the agent sees.Auto-append (opt-in)
For migration and convenience, an agent can opt into auto-appending fragments via
append_fragmentsin agent.toml:City-wide defaults can set this for all agents:
append_fragmentsonly works on.md.tmplprompts. Plain.mdprompts are inert — nothing is injected, no template engine runs.Implicit agents
Gas City provides a built-in agent for each configured provider (claude, codex, gemini, etc.) so that
gc sling claude "do something"works immediately aftergc initwith no agent configuration.Implicit agents follow the same directory convention. They are materialized from the
gcbinary into.gc/system/agents/:Shadowing: A user-defined agent with the same name wins over the system implicit. Priority chain (lowest to highest):
.gc/system/agents/) — bare minimum, always existsagents/claude/in a pack) — overrides systemagents/claude/in the city) — overrides packsAgent patches
Patches modify imported agents without defining new ones. They are distinct from agent definitions —
agents/<name>/always creates YOUR agent; patches modify SOMEONE ELSE's agent.Config-only patch — override agent.toml fields by qualified name:
Prompt replacement — redirect to a file in your city's
patches/directory:Key design decisions:
agents/<name>/= new agent.[[patches.agent]]= modify imported agent. Never conflated.gastown.mayor). Bare names work when unambiguous.Rig patches
Rig patches are agent patches scoped to one rig. They live in city.toml alongside the rig declaration:
Same fields as agent patches, same qualified naming, same semantics. The only difference is scope:
[[patches.agent]]in city.toml[[rigs.patches]]in city.tomlApplication order (later wins):
agents/directory)[[patches.agent]])[[patches.agent]])[[rigs.patches]])A rig patch can undo a city-level patch for that one rig.
Alternatives considered
[[agent]]tables, add asset conventions alongside. Doesn't solve scattered identity — two parallel declaration mechanisms is worse than one.overlay_dirfields per provider. Doesn't compose when multiple packs contribute overlays.Scope and impact
[[agent]]tables move toagents/directories. Migration tooling needed.[agents]defaults section, loses[[agent]]tables.agent.tomlis new per-agent..tmplsuffix becomes required for template processing. Existing.mdprompts using{{need renaming.per-provider/overlays,template-fragments/convention,patches/directory.[[rigs.overrides]]renamed to[[rigs.patches]]for consistency with[[patches.agent]].Open questions
gc skill promote? Current design says explicit.agents/claude/useprovider = "claude", or is naming just convention?claudeagent"?patches/or namespaced by target pack?