Property Layers: Multi-Level Configuration
Implementation guide for Gas Town's configuration system. Created: 2025-01-06
Overview
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.
The Four Layers
┌─────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────┘
Lookup Behavior
Override Semantics (Default)
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]
}
Stacking Semantics (Integers)
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.
Blocking Inheritance
You can explicitly block a property from being inherited:
gt rig config set gastown auto_restart --block
This creates a "blocked" marker in the wisp layer. Even if the rig bead
or defaults say auto_restart: true, the lookup returns nil.
Rig Identity Beads
Each rig has an identity bead for operational state:
id: gt-rig-gastown
type: rig
name: gastown
repo: [email protected]:steveyegge/gastown.git
prefix: gt
labels:
- status:operational
- priority:normal
These beads sync via git, so all clones of the rig see the same state.
Two-Level Rig Control
Level 1: Park (Local, Ephemeral)
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
Level 2: Dock (Global, Persistent)
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
Daemon Behavior
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
}
Configuration Keys
| 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 |
Commands
View Configuration
gt rig config show gastown # Show effective config (all layers)
gt rig config show gastown --layer # Show which layer each value comes from
Set Configuration
# 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 key
Rig Lifecycle
gt 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
Examples
Temporary Priority Boost
# 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
Local Maintenance
# I'm upgrading the local clone, don't restart services
gt rig park gastown
# ... do maintenance ...
gt rig unpark gastown
Project-Wide Maintenance
# 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
Block Auto-Restart Locally
# 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 only
Implementation Notes
Wisp Storage
Wisp config stored in .beads-wisp/config/<rig>.json:
{
"rig": "gastown",
"values": {
"status": "parked",
"priority_adjustment": 10
},
"blocked": ["auto_restart"]
}
Rig Bead Labels
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:docked
Daemon Integration
The 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 Events
Operational state changes are tracked as event beads, providing an immutable audit trail. Labels cache the current state for fast queries.
Event Types
| 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 | {} |
Creating and Querying Events
# 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:muted
Labels-as-State Pattern
Events 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
Configuration vs State
| 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.
Role Directives and Formula Overlays
Directives and overlays extend the property layer model to agent behavior. They follow the same rig > town > system precedence as other config.
Directives (Behavioral Policy)
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).
Overlays (Formula Modifications)
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.
Precedence Summary
| 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.
Related Documents
~/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