* fix(sling_test): update test for cook dir change
The cook command no longer needs database context and runs from cwd,
not the target rig directory. Update test to match this behavior
change from bd2a5ab5.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(tests): skip tests requiring missing binaries, handle --allow-stale
- Add skipIfAgentBinaryMissing helper to skip tests when codex/gemini
binaries aren't available in the test environment
- Update rig manager test stub to handle --allow-stale flag
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(config): remove BEADS_DIR from agent environment
Stop exporting BEADS_DIR in AgentEnv - agents should use beads redirect
mechanism instead of relying on environment variable. This prevents
prefix mismatches when agents operate across different beads databases.
Changes:
- Remove BeadsDir field from AgentEnvConfig
- Remove BEADS_DIR from env vars set on agent sessions
- Update doctor env_check to not expect BEADS_DIR
- Update all manager Start() calls to not pass BeadsDir
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(doctor): detect BEADS_DIR in tmux session environment
Add a doctor check that warns when BEADS_DIR is set in any Gas Town
tmux session. BEADS_DIR in the environment overrides prefix-based
routing and breaks multi-rig lookups - agents should use the beads
redirect mechanism instead.
The check:
- Iterates over all Gas Town tmux sessions (gt-* and hq-*)
- Checks if BEADS_DIR is set in the session environment
- Returns a warning with fix hint to restart sessions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: julianknutsen <julianknutsen@users.noreply.github>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
213 lines
5.8 KiB
Go
213 lines
5.8 KiB
Go
// Package config provides configuration loading and environment variable management.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// AgentEnvConfig specifies the configuration for generating agent environment variables.
|
|
// This is the single source of truth for all agent environment configuration.
|
|
type AgentEnvConfig struct {
|
|
// Role is the agent role: mayor, deacon, witness, refinery, crew, polecat, boot
|
|
Role string
|
|
|
|
// Rig is the rig name (empty for town-level agents like mayor/deacon)
|
|
Rig string
|
|
|
|
// AgentName is the specific agent name (empty for singletons like witness/refinery)
|
|
// For polecats, this is the polecat name. For crew, this is the crew member name.
|
|
AgentName string
|
|
|
|
// TownRoot is the root of the Gas Town workspace.
|
|
// Sets GT_ROOT environment variable.
|
|
TownRoot string
|
|
|
|
// RuntimeConfigDir is the optional CLAUDE_CONFIG_DIR path
|
|
RuntimeConfigDir string
|
|
|
|
// SessionIDEnv is the environment variable name that holds the session ID.
|
|
// Sets GT_SESSION_ID_ENV so the runtime knows where to find the session ID.
|
|
SessionIDEnv string
|
|
|
|
// BeadsNoDaemon sets BEADS_NO_DAEMON=1 if true
|
|
// Used for polecats that should bypass the beads daemon
|
|
BeadsNoDaemon bool
|
|
}
|
|
|
|
// AgentEnv returns all environment variables for an agent based on the config.
|
|
// This is the single source of truth for agent environment variables.
|
|
func AgentEnv(cfg AgentEnvConfig) map[string]string {
|
|
env := make(map[string]string)
|
|
|
|
env["GT_ROLE"] = cfg.Role
|
|
|
|
// Set role-specific variables
|
|
switch cfg.Role {
|
|
case "mayor":
|
|
env["BD_ACTOR"] = "mayor"
|
|
env["GIT_AUTHOR_NAME"] = "mayor"
|
|
|
|
case "deacon":
|
|
env["BD_ACTOR"] = "deacon"
|
|
env["GIT_AUTHOR_NAME"] = "deacon"
|
|
|
|
case "boot":
|
|
env["BD_ACTOR"] = "deacon-boot"
|
|
env["GIT_AUTHOR_NAME"] = "boot"
|
|
|
|
case "witness":
|
|
env["GT_RIG"] = cfg.Rig
|
|
env["BD_ACTOR"] = fmt.Sprintf("%s/witness", cfg.Rig)
|
|
env["GIT_AUTHOR_NAME"] = fmt.Sprintf("%s/witness", cfg.Rig)
|
|
|
|
case "refinery":
|
|
env["GT_RIG"] = cfg.Rig
|
|
env["BD_ACTOR"] = fmt.Sprintf("%s/refinery", cfg.Rig)
|
|
env["GIT_AUTHOR_NAME"] = fmt.Sprintf("%s/refinery", cfg.Rig)
|
|
|
|
case "polecat":
|
|
env["GT_RIG"] = cfg.Rig
|
|
env["GT_POLECAT"] = cfg.AgentName
|
|
env["BD_ACTOR"] = fmt.Sprintf("%s/polecats/%s", cfg.Rig, cfg.AgentName)
|
|
env["GIT_AUTHOR_NAME"] = cfg.AgentName
|
|
|
|
case "crew":
|
|
env["GT_RIG"] = cfg.Rig
|
|
env["GT_CREW"] = cfg.AgentName
|
|
env["BD_ACTOR"] = fmt.Sprintf("%s/crew/%s", cfg.Rig, cfg.AgentName)
|
|
env["GIT_AUTHOR_NAME"] = cfg.AgentName
|
|
}
|
|
|
|
// Only set GT_ROOT if provided
|
|
// Empty values would override tmux session environment
|
|
if cfg.TownRoot != "" {
|
|
env["GT_ROOT"] = cfg.TownRoot
|
|
}
|
|
|
|
// Set BEADS_AGENT_NAME for polecat/crew (uses same format as BD_ACTOR)
|
|
if cfg.Role == "polecat" || cfg.Role == "crew" {
|
|
env["BEADS_AGENT_NAME"] = fmt.Sprintf("%s/%s", cfg.Rig, cfg.AgentName)
|
|
}
|
|
|
|
if cfg.BeadsNoDaemon {
|
|
env["BEADS_NO_DAEMON"] = "1"
|
|
}
|
|
|
|
// Add optional runtime config directory
|
|
if cfg.RuntimeConfigDir != "" {
|
|
env["CLAUDE_CONFIG_DIR"] = cfg.RuntimeConfigDir
|
|
}
|
|
|
|
// Add session ID env var name if provided
|
|
if cfg.SessionIDEnv != "" {
|
|
env["GT_SESSION_ID_ENV"] = cfg.SessionIDEnv
|
|
}
|
|
|
|
return env
|
|
}
|
|
|
|
// AgentEnvSimple is a convenience function for simple role-based env var lookup.
|
|
// Use this when you only need role, rig, and agentName without advanced options.
|
|
func AgentEnvSimple(role, rig, agentName string) map[string]string {
|
|
return AgentEnv(AgentEnvConfig{
|
|
Role: role,
|
|
Rig: rig,
|
|
AgentName: agentName,
|
|
})
|
|
}
|
|
|
|
// ExportPrefix builds an export statement prefix for shell commands.
|
|
// Returns a string like "export GT_ROLE=mayor BD_ACTOR=mayor && "
|
|
// The keys are sorted for deterministic output.
|
|
func ExportPrefix(env map[string]string) string {
|
|
if len(env) == 0 {
|
|
return ""
|
|
}
|
|
|
|
// Sort keys for deterministic output
|
|
keys := make([]string, 0, len(env))
|
|
for k := range env {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
var parts []string
|
|
for _, k := range keys {
|
|
parts = append(parts, fmt.Sprintf("%s=%s", k, env[k]))
|
|
}
|
|
|
|
return "export " + strings.Join(parts, " ") + " && "
|
|
}
|
|
|
|
// BuildStartupCommandWithEnv builds a startup command with the given environment variables.
|
|
// This combines the export prefix with the agent command and optional prompt.
|
|
func BuildStartupCommandWithEnv(env map[string]string, agentCmd, prompt string) string {
|
|
prefix := ExportPrefix(env)
|
|
|
|
if prompt != "" {
|
|
// Include prompt as argument to agent command
|
|
return fmt.Sprintf("%s%s %q", prefix, agentCmd, prompt)
|
|
}
|
|
return prefix + agentCmd
|
|
}
|
|
|
|
// MergeEnv merges multiple environment maps, with later maps taking precedence.
|
|
func MergeEnv(maps ...map[string]string) map[string]string {
|
|
result := make(map[string]string)
|
|
for _, m := range maps {
|
|
for k, v := range m {
|
|
result[k] = v
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// FilterEnv returns a new map with only the specified keys.
|
|
func FilterEnv(env map[string]string, keys ...string) map[string]string {
|
|
result := make(map[string]string)
|
|
for _, k := range keys {
|
|
if v, ok := env[k]; ok {
|
|
result[k] = v
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// WithoutEnv returns a new map without the specified keys.
|
|
func WithoutEnv(env map[string]string, keys ...string) map[string]string {
|
|
result := make(map[string]string)
|
|
exclude := make(map[string]bool)
|
|
for _, k := range keys {
|
|
exclude[k] = true
|
|
}
|
|
for k, v := range env {
|
|
if !exclude[k] {
|
|
result[k] = v
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// EnvForExecCommand returns os.Environ() with the given env vars appended.
|
|
// This is useful for setting cmd.Env on exec.Command.
|
|
func EnvForExecCommand(env map[string]string) []string {
|
|
result := os.Environ()
|
|
for k, v := range env {
|
|
result = append(result, k+"="+v)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// EnvToSlice converts an env map to a slice of "K=V" strings.
|
|
// Useful for appending to os.Environ() manually.
|
|
func EnvToSlice(env map[string]string) []string {
|
|
result := make([]string, 0, len(env))
|
|
for k, v := range env {
|
|
result = append(result, k+"="+v)
|
|
}
|
|
return result
|
|
}
|