Implementation guide for Gas Town's configuration system. Created: 2025-01-06
Gas Town uses a layered property system for configuration. Properties are looked up through multiple layers, with earlier layers overriding later ones. This enables both local control and global coordination.
┌─────────────────────────────────────────────────────────────┐
│ 1. WISP LAYER (transient, town-local) │
│ Location: <rig>/.beads-wisp/config/ │
│ Synced: Never │
│ Use: Temporary local overrides │
└─────────────────────────────┬───────────────────────────────┘
│ if missing
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. RIG BEAD LAYER (persistent, synced globally) │
│ Location: <rig>/.beads/ (rig identity bead labels) │
│ Synced: Via git (all clones see it) │
│ Use: Project-wide operational state │
└─────────────────────────────┬───────────────────────────────┘
│ if missing
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. TOWN DEFAULTS │
│ Location: ~/gt/config.json or ~/gt/.beads/ │
│ Synced: N/A (per-town) │
│ Use: Town-wide policies │
└─────────────────────────────┬───────────────────────────────┘
│ if missing
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. SYSTEM DEFAULTS (compiled in) │
│ Use: Fallback when nothing else specified │
└─────────────────────────────────────────────────────────────┘
For most properties, the first non-nil value wins:
func GetConfig(key string) interface{} {
if val := wisp.Get(key); val != nil {
if val == Blocked { return nil }
return val
}
if val := rigBead.GetLabel(key); val != nil {
return val
}
if val := townDefaults.Get(key); val != nil {
return val
}
return systemDefaults[key]
}For integer properties, values from wisp and bead layers add to the base:
func GetIntConfig(key string) int {
base := getBaseDefault(key) // Town or system default
beadAdj := rigBead.GetInt(key) // 0 if missing
wispAdj := wisp.GetInt(key) // 0 if missing
return base + beadAdj + wispAdj
}This enables temporary adjustments without changing the base value.
You can explicitly block a property from being inherited:
gt rig config set gastown auto_restart --blockThis creates a "blocked" marker in the wisp layer. Even if the rig bead
or defaults say auto_restart: true, the lookup returns nil.
Each rig has an identity bead for operational state:
id: gt-rig-gastown
type: rig
name: gastown
repo: git@github.com:steveyegge/gastown.git
prefix: gt
labels:
- status:operational
- priority:normalThese beads sync via git, so all clones of the rig see the same state.
gt rig park gastown # Stop services, daemon won't restart
gt rig unpark gastown # Allow services to run- Stored in wisp layer (
.beads-wisp/config/) - Only affects this town
- Disappears on cleanup
- Use: Local maintenance, debugging
gt rig dock gastown # Set status:docked label on rig bead
gt rig undock gastown # Remove label- Stored on rig identity bead
- Syncs to all clones via git
- Permanent until explicitly changed
- Use: Project-wide maintenance, coordinated downtime
The daemon checks both levels before auto-restarting:
func shouldAutoRestart(rig *Rig) bool {
status := rig.GetConfig("status")
if status == "parked" || status == "docked" {
return false
}
return true
}| Key | Type | Behavior | Description |
|---|---|---|---|
status |
string | Override | operational/parked/docked |
auto_restart |
bool | Override | Daemon auto-restart behavior |
max_polecats |
int | Override | Maximum concurrent polecats |
priority_adjustment |
int | Stack | Scheduling priority modifier |
maintenance_window |
string | Override | When maintenance allowed |
dnd |
bool | Override | Do not disturb mode |
gt rig config show gastown # Show effective config (all layers)
gt rig config show gastown --layer # Show which layer each value comes from# Set in wisp layer (local, ephemeral)
gt rig config set gastown key value
# Set in bead layer (global, permanent)
gt rig config set gastown key value --global
# Block inheritance
gt rig config set gastown key --block
# Clear from wisp layer
gt rig config unset gastown keygt rig park gastown # Local: stop + prevent restart
gt rig unpark gastown # Local: allow restart
gt rig dock gastown # Global: mark as offline
gt rig undock gastown # Global: mark as operational
gt rig status gastown # Show current state# Base priority: 0 (from defaults)
# Give this rig temporary priority boost for urgent work
gt rig config set gastown priority_adjustment 10
# Effective priority: 0 + 10 = 10
# When done, clear it:
gt rig config unset gastown priority_adjustment# I'm upgrading the local clone, don't restart services
gt rig park gastown
# ... do maintenance ...
gt rig unpark gastown# Major refactor in progress, all clones should pause
gt rig dock gastown
# Syncs via git - other towns see the rig as docked
bd sync
# When done:
gt rig undock gastown
bd sync# Rig bead says auto_restart: true
# But I'm debugging and don't want that here
gt rig config set gastown auto_restart --block
# Now auto_restart returns nil for this town onlyWisp config stored in .beads-wisp/config/<rig>.json:
{
"rig": "gastown",
"values": {
"status": "parked",
"priority_adjustment": 10
},
"blocked": ["auto_restart"]
}Rig operational state stored as labels on the rig identity bead:
bd label add gt-rig-gastown status:docked
bd label remove gt-rig-gastown status:dockedThe daemon's lifecycle manager checks config before starting services:
func (d *Daemon) maybeStartRigServices(rig string) {
r := d.getRig(rig)
status := r.GetConfig("status")
if status == "parked" || status == "docked" {
log.Info("Rig %s is offline, skipping auto-start", rig)
return
}
d.ensureWitness(rig)
d.ensureRefinery(rig)
}Operational state changes are tracked as event beads, providing an immutable audit trail. Labels cache the current state for fast queries.
| Event Type | Description | Payload |
|---|---|---|
patrol.muted |
Patrol cycle disabled | {reason, until?} |
patrol.unmuted |
Patrol cycle re-enabled | {reason?} |
agent.started |
Agent session began | {session_id?} |
agent.stopped |
Agent session ended | {reason, outcome?} |
mode.degraded |
System entered degraded mode | {reason} |
mode.normal |
System returned to normal | {} |
# Create operational event
bd create --type=event --event-type=patrol.muted \
--actor=human:overseer --target=agent:deacon \
--payload='{"reason":"fixing convoy deadlock","until":"gt-abc1"}'
# Query recent events for an agent
bd list --type=event --target=agent:deacon --limit=10
# Query current state via labels
bd list --type=role --label=patrol:mutedEvents capture the full history. Labels cache the current state:
patrol:muted/patrol:activemode:degraded/mode:normalstatus:idle/status:working
State change flow: create event bead (immutable), then update role bead labels (cache).
# Mute patrol
bd create --type=event --event-type=patrol.muted ...
bd update role-deacon --add-label=patrol:muted --remove-label=patrol:active| Type | Storage | Example |
|---|---|---|
| Static config | TOML files | Daemon tick interval |
| Role directives | Markdown files | Operator behavioral policy per role |
| Formula overlays | TOML files | Per-step formula modifications |
| Operational state | Beads (events + labels) | Patrol muted |
| Runtime flags | Marker files | .deacon-disabled |
Events are the source of truth. Labels are the cache.
For Boot triage and degraded mode details, see Watchdog Chain.
Directives and overlays extend the property layer model to agent behavior. They follow the same rig > town > system precedence as other config.
Per-role Markdown files that modify agent behavior at prime time:
SYSTEM LAYER: Embedded role template (compiled in)
│ if directive exists
▼
TOWN LAYER: ~/gt/directives/<role>.md
│ concatenated with
▼
RIG LAYER: ~/gt/<rig>/directives/<role>.md
Both town and rig directives concatenate. Rig content appears last and wins conflicts (same as CSS specificity — later rules override earlier ones).
Per-formula TOML files that modify individual steps:
SYSTEM LAYER: Embedded formula (compiled in)
│ if overlay exists
▼
TOWN LAYER: ~/gt/formula-overlays/<formula>.toml
│ rig replaces town entirely
▼
RIG LAYER: ~/gt/<rig>/formula-overlays/<formula>.toml
Unlike directives, overlays use full replacement at the rig level — if a rig overlay exists, the town overlay is ignored entirely. This prevents conflicting step modifications from merging unpredictably.
| Config Type | Town + Rig Interaction | Rationale |
|---|---|---|
| Rig properties | First non-nil wins (override) | Standard config lookup |
| Integer properties | Values stack (additive) | Allows adjustments |
| Role directives | Concatenate (rig last) | Additive policy; rig gets last word |
| Formula overlays | Rig replaces town | Step mods can conflict; full replacement is safer |
See directives-and-overlays.md for the full
reference with TOML format, examples, and gt doctor integration.
~/gt/docs/hop/PROPERTY-LAYERS.md- Strategic architecturewisp-architecture.md- Wisp system designagent-as-bead.md- Agent identity beads (similar pattern)- directives-and-overlays.md - Full reference