// 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 } }