Files
gastown/internal/rig/config.go
slit 805ac7c17a feat(rig): implement property layer lookup (gt-emh1c)
Implements unified config lookup across all layers:
1. Wisp layer (transient, town-local)
2. Rig identity bead labels
3. Town defaults
4. System defaults (compiled-in)

Two lookup modes:
- Override: First non-nil value wins (default)
- Stacking: Integer values sum (for priority_adjustment)

API on Rig:
- GetConfig(key) interface{}
- GetIntConfig(key) int (stacking for priority_adjustment)
- GetBoolConfig(key) bool
- GetStringConfig(key) string
- GetConfigWithSource(key) (value, source)

Includes cherry-picked dependencies:
- Wisp config storage layer (nux, gt-3w685)
- Rig identity bead schema (furiosa, gt-zmznh)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 20:40:14 -08:00

219 lines
5.6 KiB
Go

// Package rig provides rig management functionality.
// This file implements the property layer lookup API for unified config access.
package rig
import (
"path/filepath"
"strconv"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/wisp"
)
// ConfigSource identifies which layer a config value came from.
type ConfigSource string
const (
SourceWisp ConfigSource = "wisp" // Local wisp layer (.beads-wisp/config/)
SourceBead ConfigSource = "bead" // Rig identity bead labels
SourceTown ConfigSource = "town" // Town defaults (~/gt/settings/config.json)
SourceSystem ConfigSource = "system" // Compiled-in system defaults
SourceBlocked ConfigSource = "blocked" // Explicitly blocked at wisp layer
SourceNone ConfigSource = "none" // No value found
)
// ConfigResult holds a config lookup result with its source.
type ConfigResult struct {
Value interface{}
Source ConfigSource
}
// SystemDefaults contains compiled-in default values.
// These are the fallback when no other layer provides a value.
var SystemDefaults = map[string]interface{}{
"status": "operational",
"auto_restart": true,
"max_polecats": 10,
"priority_adjustment": 0,
"dnd": false,
}
// StackingKeys defines which keys use stacking semantics (values add up).
// All other keys use override semantics (first non-nil wins).
var StackingKeys = map[string]bool{
"priority_adjustment": true,
}
// GetConfig looks up a config value through all layers.
// Override semantics: first non-nil value wins.
// Layers are checked in order: wisp -> bead -> town -> system
func (r *Rig) GetConfig(key string) interface{} {
result := r.GetConfigWithSource(key)
return result.Value
}
// GetConfigWithSource looks up a config value and returns which layer it came from.
func (r *Rig) GetConfigWithSource(key string) ConfigResult {
townRoot := filepath.Dir(r.Path)
// Layer 1: Wisp (transient, local)
wispCfg := wisp.NewConfig(townRoot, r.Name)
if wispCfg.IsBlocked(key) {
return ConfigResult{Value: nil, Source: SourceBlocked}
}
if val := wispCfg.Get(key); val != nil {
return ConfigResult{Value: val, Source: SourceWisp}
}
// Layer 2: Rig identity bead labels
if val := r.getBeadLabel(key); val != nil {
return ConfigResult{Value: val, Source: SourceBead}
}
// Layer 3: Town defaults
// Note: Town defaults for operational state would typically be in
// ~/gt/settings/config.json. For now, we skip directly to system defaults.
// Future: load from config.TownSettings
// Layer 4: System defaults
if val, ok := SystemDefaults[key]; ok {
return ConfigResult{Value: val, Source: SourceSystem}
}
return ConfigResult{Value: nil, Source: SourceNone}
}
// GetBoolConfig looks up a boolean config value.
// Returns false if not set, not a bool, or blocked.
func (r *Rig) GetBoolConfig(key string) bool {
result := r.GetConfig(key)
if result == nil {
return false
}
switch v := result.(type) {
case bool:
return v
case string:
// Handle string booleans from bead labels
return v == "true" || v == "1" || v == "yes"
default:
return false
}
}
// GetIntConfig looks up an integer config value with stacking semantics.
// For stacking keys, values from wisp and bead layers ADD to the base.
// For non-stacking keys, uses override semantics.
func (r *Rig) GetIntConfig(key string) int {
townRoot := filepath.Dir(r.Path)
// Check if this key uses stacking semantics
if !StackingKeys[key] {
// Override semantics: return first non-nil
result := r.GetConfig(key)
return toInt(result)
}
// Stacking semantics: sum up adjustments from all layers
// Get base value (town or system default)
base := 0
if val, ok := SystemDefaults[key]; ok {
base = toInt(val)
}
// Check wisp layer for blocked
wispCfg := wisp.NewConfig(townRoot, r.Name)
if wispCfg.IsBlocked(key) {
return 0 // Blocked returns zero
}
// Add bead adjustment
beadAdj := 0
if val := r.getBeadLabel(key); val != nil {
beadAdj = toInt(val)
}
// Add wisp adjustment
wispAdj := 0
if val := wispCfg.Get(key); val != nil {
wispAdj = toInt(val)
}
return base + beadAdj + wispAdj
}
// GetStringConfig looks up a string config value.
// Returns empty string if not set or blocked.
func (r *Rig) GetStringConfig(key string) string {
result := r.GetConfig(key)
if result == nil {
return ""
}
switch v := result.(type) {
case string:
return v
default:
return ""
}
}
// getBeadLabel reads a label value from the rig identity bead.
// Returns nil if the rig bead doesn't exist or the label is not set.
func (r *Rig) getBeadLabel(key string) interface{} {
townRoot := filepath.Dir(r.Path)
// Get the rig's beads prefix
prefix := "gt" // default
if r.Config != nil && r.Config.Prefix != "" {
prefix = r.Config.Prefix
}
// Construct rig identity bead ID
rigBeadID := beads.RigBeadIDWithPrefix(prefix, r.Name)
// Load the bead
beadsDir := beads.ResolveBeadsDir(r.Path)
bd := beads.NewWithBeadsDir(townRoot, beadsDir)
issue, err := bd.Show(rigBeadID)
if err != nil {
return nil
}
// Parse labels for key:value format
for _, label := range issue.Labels {
// Labels are in format "key:value"
if len(label) > len(key)+1 && label[:len(key)+1] == key+":" {
return label[len(key)+1:]
}
}
return nil
}
// toInt converts a value to int, returning 0 for unconvertible types.
func toInt(v interface{}) int {
if v == nil {
return 0
}
switch val := v.(type) {
case int:
return val
case int64:
return int(val)
case float64:
return int(val)
case string:
if i, err := strconv.Atoi(val); err == nil {
return i
}
return 0
default:
return 0
}
}