From e999ceb1c14a21ca5606e2f5a034be2fe5f7e650 Mon Sep 17 00:00:00 2001 From: julianknutsen Date: Fri, 9 Jan 2026 09:20:33 -0800 Subject: [PATCH] refactor: consolidate agent env vars into config.AgentEnv Create centralized AgentEnv function as single source of truth for all agent environment variables. All agents now consistently receive: - GT_ROLE, BD_ACTOR, GIT_AUTHOR_NAME (role identity) - GT_ROOT, BEADS_DIR (workspace paths) - GT_RIG, GT_POLECAT/GT_CREW (rig-specific identity) - BEADS_AGENT_NAME, BEADS_NO_DAEMON (beads config) - CLAUDE_CONFIG_DIR (optional account selection) Remove RoleEnvVars in favor of AgentEnvSimple wrapper. Remove IncludeBeadsEnv flag - beads env vars always included. Update all manager and cmd call sites to use AgentEnv. Co-Authored-By: Claude Opus 4.5 --- internal/boot/boot.go | 13 +- internal/cmd/crew_at.go | 19 ++- internal/cmd/deacon.go | 11 +- internal/cmd/role.go | 2 +- internal/config/env.go | 204 ++++++++++++++++++++++++++++ internal/config/loader.go | 47 +------ internal/crew/manager.go | 19 ++- internal/daemon/daemon.go | 4 +- internal/daemon/lifecycle.go | 33 +++-- internal/deacon/manager.go | 12 +- internal/doctor/env_check.go | 2 +- internal/doctor/env_check_test.go | 20 +-- internal/mayor/manager.go | 9 +- internal/polecat/session_manager.go | 26 ++-- internal/refinery/manager.go | 18 +-- internal/witness/manager.go | 11 +- 16 files changed, 336 insertions(+), 114 deletions(-) create mode 100644 internal/config/env.go diff --git a/internal/boot/boot.go b/internal/boot/boot.go index 2b1a42f0..b2ce10f3 100644 --- a/internal/boot/boot.go +++ b/internal/boot/boot.go @@ -11,6 +11,7 @@ import ( "path/filepath" "time" + "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/tmux" ) @@ -190,9 +191,15 @@ func (b *Boot) spawnTmux() error { return fmt.Errorf("creating boot session: %w", err) } - // Set environment - _ = b.tmux.SetEnvironment(SessionName, "GT_ROLE", "boot") - _ = b.tmux.SetEnvironment(SessionName, "BD_ACTOR", "deacon-boot") + // Set environment using centralized AgentEnv for consistency + envVars := config.AgentEnv(config.AgentEnvConfig{ + Role: "boot", + TownRoot: b.townRoot, + BeadsDir: beads.ResolveBeadsDir(b.townRoot), + }) + for k, v := range envVars { + _ = b.tmux.SetEnvironment(SessionName, k, v) + } // Launch Claude with environment exported inline and initial triage prompt // The "gt boot triage" prompt tells Boot to immediately start triage (GUPP principle) diff --git a/internal/cmd/crew_at.go b/internal/cmd/crew_at.go index 04d30773..f8e30f45 100644 --- a/internal/cmd/crew_at.go +++ b/internal/cmd/crew_at.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/spf13/cobra" + "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/crew" @@ -138,13 +139,17 @@ func runCrewAt(cmd *cobra.Command, args []string) error { } // Set environment (non-fatal: session works without these) - _ = t.SetEnvironment(sessionID, "GT_ROLE", "crew") - _ = t.SetEnvironment(sessionID, "GT_RIG", r.Name) - _ = t.SetEnvironment(sessionID, "GT_CREW", name) - - // Set runtime config dir for account selection (non-fatal) - if runtimeConfig.Session != nil && runtimeConfig.Session.ConfigDirEnv != "" && claudeConfigDir != "" { - _ = t.SetEnvironment(sessionID, runtimeConfig.Session.ConfigDirEnv, claudeConfigDir) + // Use centralized AgentEnv for consistency across all role startup paths + envVars := config.AgentEnv(config.AgentEnvConfig{ + Role: "crew", + Rig: r.Name, + AgentName: name, + TownRoot: townRoot, + BeadsDir: beads.ResolveBeadsDir(r.Path), + RuntimeConfigDir: claudeConfigDir, + }) + for k, v := range envVars { + _ = t.SetEnvironment(sessionID, k, v) } // Apply rig-based theming (non-fatal: theming failure doesn't affect operation) diff --git a/internal/cmd/deacon.go b/internal/cmd/deacon.go index 00bfcc01..399d15a3 100644 --- a/internal/cmd/deacon.go +++ b/internal/cmd/deacon.go @@ -358,8 +358,15 @@ func startDeaconSession(t *tmux.Tmux, sessionName, agentOverride string) error { } // Set environment (non-fatal: session works without these) - _ = t.SetEnvironment(sessionName, "GT_ROLE", "deacon") - _ = t.SetEnvironment(sessionName, "BD_ACTOR", "deacon") + // Use centralized AgentEnv for consistency across all role startup paths + envVars := config.AgentEnv(config.AgentEnvConfig{ + Role: "deacon", + TownRoot: townRoot, + BeadsDir: beads.ResolveBeadsDir(townRoot), + }) + for k, v := range envVars { + _ = t.SetEnvironment(sessionName, k, v) + } // Apply Deacon theme (non-fatal: theming failure doesn't affect operation) // Note: ConfigureGasTownSession includes cycle bindings diff --git a/internal/cmd/role.go b/internal/cmd/role.go index 70871a0f..86249891 100644 --- a/internal/cmd/role.go +++ b/internal/cmd/role.go @@ -506,7 +506,7 @@ func runRoleEnv(cmd *cobra.Command, args []string) error { } // Get canonical env vars from shared source of truth - envVars := config.RoleEnvVars(string(info.Role), info.Rig, info.Polecat) + envVars := config.AgentEnvSimple(string(info.Role), info.Rig, info.Polecat) envVars[EnvGTRoleHome] = home // Output in sorted order for consistent output diff --git a/internal/config/env.go b/internal/config/env.go new file mode 100644 index 00000000..9fec04e8 --- /dev/null +++ b/internal/config/env.go @@ -0,0 +1,204 @@ +// 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 + + // BeadsDir is the resolved BEADS_DIR path. + // Callers should use beads.ResolveBeadsDir() to compute this. + BeadsDir string + + // RuntimeConfigDir is the optional CLAUDE_CONFIG_DIR path + RuntimeConfigDir 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 + } + + env["GT_ROOT"] = cfg.TownRoot + env["BEADS_DIR"] = cfg.BeadsDir + + // 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 + } + + 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 +} diff --git a/internal/config/loader.go b/internal/config/loader.go index 62a062e7..f9fe0147 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -1170,45 +1170,6 @@ func BuildStartupCommandWithAgentOverride(envVars map[string]string, rigPath, pr return cmd, nil } -// RoleEnvVars returns the canonical environment variables for a role. -// This is the single source of truth for role identity env vars. -// The role parameter should be one of: "mayor", "deacon", "witness", "refinery", "polecat", "crew". -// For rig-specific roles, rig must be provided. -// For polecat/crew, polecatOrCrew must be the polecat or crew member name. -func RoleEnvVars(role, rig, polecatOrCrew string) map[string]string { - envVars := map[string]string{ - "GT_ROLE": role, - } - - switch role { - case "mayor": - envVars["BD_ACTOR"] = "mayor" - envVars["GIT_AUTHOR_NAME"] = "mayor" - case "deacon": - envVars["BD_ACTOR"] = "deacon" - envVars["GIT_AUTHOR_NAME"] = "deacon" - case "witness": - envVars["GT_RIG"] = rig - envVars["BD_ACTOR"] = fmt.Sprintf("%s/witness", rig) - envVars["GIT_AUTHOR_NAME"] = fmt.Sprintf("%s/witness", rig) - case "refinery": - envVars["GT_RIG"] = rig - envVars["BD_ACTOR"] = fmt.Sprintf("%s/refinery", rig) - envVars["GIT_AUTHOR_NAME"] = fmt.Sprintf("%s/refinery", rig) - case "polecat": - envVars["GT_RIG"] = rig - envVars["GT_POLECAT"] = polecatOrCrew - envVars["BD_ACTOR"] = fmt.Sprintf("%s/polecats/%s", rig, polecatOrCrew) - envVars["GIT_AUTHOR_NAME"] = polecatOrCrew - case "crew": - envVars["GT_RIG"] = rig - envVars["GT_CREW"] = polecatOrCrew - envVars["BD_ACTOR"] = fmt.Sprintf("%s/crew/%s", rig, polecatOrCrew) - envVars["GIT_AUTHOR_NAME"] = polecatOrCrew - } - - return envVars -} // BuildAgentStartupCommand is a convenience function for starting agent sessions. // It sets standard environment variables (GT_ROLE, BD_ACTOR, GIT_AUTHOR_NAME) @@ -1235,23 +1196,23 @@ func BuildAgentStartupCommandWithAgentOverride(role, bdActor, rigPath, prompt, a // BuildPolecatStartupCommand builds the startup command for a polecat. // Sets GT_ROLE, GT_RIG, GT_POLECAT, BD_ACTOR, and GIT_AUTHOR_NAME. func BuildPolecatStartupCommand(rigName, polecatName, rigPath, prompt string) string { - return BuildStartupCommand(RoleEnvVars("polecat", rigName, polecatName), rigPath, prompt) + return BuildStartupCommand(AgentEnvSimple("polecat", rigName, polecatName), rigPath, prompt) } // BuildPolecatStartupCommandWithAgentOverride is like BuildPolecatStartupCommand, but uses agentOverride if non-empty. func BuildPolecatStartupCommandWithAgentOverride(rigName, polecatName, rigPath, prompt, agentOverride string) (string, error) { - return BuildStartupCommandWithAgentOverride(RoleEnvVars("polecat", rigName, polecatName), rigPath, prompt, agentOverride) + return BuildStartupCommandWithAgentOverride(AgentEnvSimple("polecat", rigName, polecatName), rigPath, prompt, agentOverride) } // BuildCrewStartupCommand builds the startup command for a crew member. // Sets GT_ROLE, GT_RIG, GT_CREW, BD_ACTOR, and GIT_AUTHOR_NAME. func BuildCrewStartupCommand(rigName, crewName, rigPath, prompt string) string { - return BuildStartupCommand(RoleEnvVars("crew", rigName, crewName), rigPath, prompt) + return BuildStartupCommand(AgentEnvSimple("crew", rigName, crewName), rigPath, prompt) } // BuildCrewStartupCommandWithAgentOverride is like BuildCrewStartupCommand, but uses agentOverride if non-empty. func BuildCrewStartupCommandWithAgentOverride(rigName, crewName, rigPath, prompt, agentOverride string) (string, error) { - return BuildStartupCommandWithAgentOverride(RoleEnvVars("crew", rigName, crewName), rigPath, prompt, agentOverride) + return BuildStartupCommandWithAgentOverride(AgentEnvSimple("crew", rigName, crewName), rigPath, prompt, agentOverride) } // ExpectedPaneCommands returns tmux pane command names that indicate the runtime is running. diff --git a/internal/crew/manager.go b/internal/crew/manager.go index 8270ec35..de97bc3f 100644 --- a/internal/crew/manager.go +++ b/internal/crew/manager.go @@ -514,13 +514,18 @@ func (m *Manager) Start(name string, opts StartOptions) error { } // Set environment variables (non-fatal: session works without these) - _ = t.SetEnvironment(sessionID, "GT_RIG", m.rig.Name) - _ = t.SetEnvironment(sessionID, "GT_CREW", name) - _ = t.SetEnvironment(sessionID, "GT_ROLE", "crew") - - // Set CLAUDE_CONFIG_DIR for account selection (non-fatal) - if opts.ClaudeConfigDir != "" { - _ = t.SetEnvironment(sessionID, "CLAUDE_CONFIG_DIR", opts.ClaudeConfigDir) + // Use centralized AgentEnv for consistency across all role startup paths + townRoot := filepath.Dir(m.rig.Path) + envVars := config.AgentEnv(config.AgentEnvConfig{ + Role: "crew", + Rig: m.rig.Name, + AgentName: name, + TownRoot: townRoot, + BeadsDir: beads.ResolveBeadsDir(m.rig.Path), + RuntimeConfigDir: opts.ClaudeConfigDir, + }) + for k, v := range envVars { + _ = t.SetEnvironment(sessionID, k, v) } // Apply rig-based theming (non-fatal: theming failure doesn't affect operation) diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 3b4c3489..cf1107b4 100755 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -847,8 +847,8 @@ func (d *Daemon) restartPolecatSession(rigName, polecatName, sessionName string) } // Set environment variables - // Use shared RoleEnvVars for consistency across all role startup paths - envVars := config.RoleEnvVars("polecat", rigName, polecatName) + // Use centralized AgentEnvSimple for consistency across all role startup paths + envVars := config.AgentEnvSimple("polecat", rigName, polecatName) // Add polecat-specific beads configuration // Use ResolveBeadsDir to follow redirects for repos with tracked beads diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go index 23cff651..9b0c317f 100644 --- a/internal/daemon/lifecycle.go +++ b/internal/daemon/lifecycle.go @@ -487,18 +487,31 @@ func (d *Daemon) getStartCommand(roleConfig *beads.RoleConfig, parsed *ParsedIde } // setSessionEnvironment sets environment variables for the tmux session. -// Uses role bead config if available, falls back to hardcoded defaults. -func (d *Daemon) setSessionEnvironment(sessionName, identity string, config *beads.RoleConfig, parsed *ParsedIdentity) { - // Always set GT_ROLE - _ = d.tmux.SetEnvironment(sessionName, "GT_ROLE", identity) +// Uses centralized AgentEnv for consistency, plus role bead custom env vars if available. +func (d *Daemon) setSessionEnvironment(sessionName, identity string, roleConfig *beads.RoleConfig, parsed *ParsedIdentity) { + // Determine beads dir based on role type + var beadsPath string + if parsed.RigName != "" { + beadsPath = filepath.Join(d.config.TownRoot, parsed.RigName) + } else { + beadsPath = d.config.TownRoot + } - // BD_ACTOR uses slashes instead of dashes for path-like identity - bdActor := identityToBDActor(identity) - _ = d.tmux.SetEnvironment(sessionName, "BD_ACTOR", bdActor) + // Use centralized AgentEnv for base environment variables + envVars := config.AgentEnv(config.AgentEnvConfig{ + Role: parsed.RoleType, + Rig: parsed.RigName, + AgentName: parsed.AgentName, + TownRoot: d.config.TownRoot, + BeadsDir: beads.ResolveBeadsDir(beadsPath), + }) + for k, v := range envVars { + _ = d.tmux.SetEnvironment(sessionName, k, v) + } - // Set any custom env vars from role config - if config != nil { - for k, v := range config.EnvVars { + // Set any custom env vars from role config (bead-defined overrides) + if roleConfig != nil { + for k, v := range roleConfig.EnvVars { expanded := beads.ExpandRolePattern(v, d.config.TownRoot, parsed.RigName, parsed.AgentName, parsed.RoleType) _ = d.tmux.SetEnvironment(sessionName, k, expanded) } diff --git a/internal/deacon/manager.go b/internal/deacon/manager.go index d1d77d81..3696bb11 100644 --- a/internal/deacon/manager.go +++ b/internal/deacon/manager.go @@ -7,6 +7,7 @@ import ( "path/filepath" "time" + "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/claude" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/constants" @@ -93,8 +94,15 @@ func (m *Manager) Start(agentOverride string) error { } // Set environment variables (non-fatal: session works without these) - _ = t.SetEnvironment(sessionID, "GT_ROLE", "deacon") - _ = t.SetEnvironment(sessionID, "BD_ACTOR", "deacon") + // Use centralized AgentEnv for consistency across all role startup paths + envVars := config.AgentEnv(config.AgentEnvConfig{ + Role: "deacon", + TownRoot: m.townRoot, + BeadsDir: beads.ResolveBeadsDir(m.townRoot), + }) + for k, v := range envVars { + _ = t.SetEnvironment(sessionID, k, v) + } // Apply Deacon theming (non-fatal: theming failure doesn't affect operation) theme := tmux.DeaconTheme() diff --git a/internal/doctor/env_check.go b/internal/doctor/env_check.go index dbf829b1..6cd5d145 100644 --- a/internal/doctor/env_check.go +++ b/internal/doctor/env_check.go @@ -101,7 +101,7 @@ func (c *EnvVarsCheck) Run(ctx *CheckContext) *CheckResult { } // Get expected env vars based on role - expected := config.RoleEnvVars(string(identity.Role), identity.Rig, identity.Name) + expected := config.AgentEnvSimple(string(identity.Role), identity.Rig, identity.Name) // Get actual tmux env vars actual, err := reader.GetAllEnvironment(sess) diff --git a/internal/doctor/env_check_test.go b/internal/doctor/env_check_test.go index df365c2b..e6eb6918 100644 --- a/internal/doctor/env_check_test.go +++ b/internal/doctor/env_check_test.go @@ -83,7 +83,7 @@ func TestEnvVarsCheck_NonGasTownSessions(t *testing.T) { } func TestEnvVarsCheck_MayorCorrect(t *testing.T) { - expected := config.RoleEnvVars("mayor", "", "") + expected := config.AgentEnvSimple("mayor", "", "") reader := &mockEnvReader{ sessions: []string{"hq-mayor"}, sessionEnvs: map[string]map[string]string{ @@ -114,7 +114,7 @@ func TestEnvVarsCheck_MayorMissing(t *testing.T) { } func TestEnvVarsCheck_WitnessCorrect(t *testing.T) { - expected := config.RoleEnvVars("witness", "myrig", "") + expected := config.AgentEnvSimple("witness", "myrig", "") reader := &mockEnvReader{ sessions: []string{"gt-myrig-witness"}, sessionEnvs: map[string]map[string]string{ @@ -148,7 +148,7 @@ func TestEnvVarsCheck_WitnessMismatch(t *testing.T) { } func TestEnvVarsCheck_RefineryCorrect(t *testing.T) { - expected := config.RoleEnvVars("refinery", "myrig", "") + expected := config.AgentEnvSimple("refinery", "myrig", "") reader := &mockEnvReader{ sessions: []string{"gt-myrig-refinery"}, sessionEnvs: map[string]map[string]string{ @@ -164,7 +164,7 @@ func TestEnvVarsCheck_RefineryCorrect(t *testing.T) { } func TestEnvVarsCheck_PolecatCorrect(t *testing.T) { - expected := config.RoleEnvVars("polecat", "myrig", "Toast") + expected := config.AgentEnvSimple("polecat", "myrig", "Toast") reader := &mockEnvReader{ sessions: []string{"gt-myrig-Toast"}, sessionEnvs: map[string]map[string]string{ @@ -198,7 +198,7 @@ func TestEnvVarsCheck_PolecatMissing(t *testing.T) { } func TestEnvVarsCheck_CrewCorrect(t *testing.T) { - expected := config.RoleEnvVars("crew", "myrig", "worker1") + expected := config.AgentEnvSimple("crew", "myrig", "worker1") reader := &mockEnvReader{ sessions: []string{"gt-myrig-crew-worker1"}, sessionEnvs: map[string]map[string]string{ @@ -214,9 +214,9 @@ func TestEnvVarsCheck_CrewCorrect(t *testing.T) { } func TestEnvVarsCheck_MultipleSessions(t *testing.T) { - mayorEnv := config.RoleEnvVars("mayor", "", "") - witnessEnv := config.RoleEnvVars("witness", "rig1", "") - polecatEnv := config.RoleEnvVars("polecat", "rig1", "Toast") + mayorEnv := config.AgentEnvSimple("mayor", "", "") + witnessEnv := config.AgentEnvSimple("witness", "rig1", "") + polecatEnv := config.AgentEnvSimple("polecat", "rig1", "Toast") reader := &mockEnvReader{ sessions: []string{"hq-mayor", "gt-rig1-witness", "gt-rig1-Toast"}, @@ -238,7 +238,7 @@ func TestEnvVarsCheck_MultipleSessions(t *testing.T) { } func TestEnvVarsCheck_MixedCorrectAndMismatch(t *testing.T) { - mayorEnv := config.RoleEnvVars("mayor", "", "") + mayorEnv := config.AgentEnvSimple("mayor", "", "") reader := &mockEnvReader{ sessions: []string{"hq-mayor", "gt-rig1-witness"}, @@ -297,7 +297,7 @@ func TestEnvVarsCheck_GetEnvError(t *testing.T) { func TestEnvVarsCheck_HyphenatedRig(t *testing.T) { // Test rig name with hyphens: "foo-bar" - expected := config.RoleEnvVars("witness", "foo-bar", "") + expected := config.AgentEnvSimple("witness", "foo-bar", "") reader := &mockEnvReader{ sessions: []string{"gt-foo-bar-witness"}, sessionEnvs: map[string]map[string]string{ diff --git a/internal/mayor/manager.go b/internal/mayor/manager.go index a2ec31fa..896ffcab 100644 --- a/internal/mayor/manager.go +++ b/internal/mayor/manager.go @@ -7,6 +7,7 @@ import ( "path/filepath" "time" + "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/claude" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/constants" @@ -94,8 +95,12 @@ func (m *Manager) Start(agentOverride string) error { } // Set environment variables (non-fatal: session works without these) - // Use shared RoleEnvVars for consistency across all role startup paths - envVars := config.RoleEnvVars("mayor", "", "") + // Use centralized AgentEnv for consistency across all role startup paths + envVars := config.AgentEnv(config.AgentEnvConfig{ + Role: "mayor", + TownRoot: m.townRoot, + BeadsDir: beads.ResolveBeadsDir(m.townRoot), + }) for k, v := range envVars { _ = t.SetEnvironment(sessionID, k, v) } diff --git a/internal/polecat/session_manager.go b/internal/polecat/session_manager.go index 77d0cc99..23acc3a7 100644 --- a/internal/polecat/session_manager.go +++ b/internal/polecat/session_manager.go @@ -186,21 +186,21 @@ func (m *SessionManager) Start(polecat string, opts SessionStartOptions) error { } // Set environment (non-fatal: session works without these) - debugSession("SetEnvironment GT_RIG", m.tmux.SetEnvironment(sessionID, "GT_RIG", m.rig.Name)) - debugSession("SetEnvironment GT_POLECAT", m.tmux.SetEnvironment(sessionID, "GT_POLECAT", polecat)) - - // Set runtime config dir for account selection (non-fatal) - if runtimeConfig.Session != nil && runtimeConfig.Session.ConfigDirEnv != "" && opts.RuntimeConfigDir != "" { - debugSession("SetEnvironment "+runtimeConfig.Session.ConfigDirEnv, m.tmux.SetEnvironment(sessionID, runtimeConfig.Session.ConfigDirEnv, opts.RuntimeConfigDir)) + // Use centralized AgentEnv for consistency across all role startup paths + townRoot := filepath.Dir(m.rig.Path) + envVars := config.AgentEnv(config.AgentEnvConfig{ + Role: "polecat", + Rig: m.rig.Name, + AgentName: polecat, + TownRoot: townRoot, + BeadsDir: beads.ResolveBeadsDir(m.rig.Path), + RuntimeConfigDir: opts.RuntimeConfigDir, + BeadsNoDaemon: true, + }) + for k, v := range envVars { + debugSession("SetEnvironment "+k, m.tmux.SetEnvironment(sessionID, k, v)) } - // Set beads environment for worktree polecats (non-fatal) - // Use ResolveBeadsDir to follow redirects for repos with tracked beads - beadsDir := beads.ResolveBeadsDir(m.rig.Path) - debugSession("SetEnvironment BEADS_DIR", m.tmux.SetEnvironment(sessionID, "BEADS_DIR", beadsDir)) - debugSession("SetEnvironment BEADS_NO_DAEMON", m.tmux.SetEnvironment(sessionID, "BEADS_NO_DAEMON", "1")) - debugSession("SetEnvironment BEADS_AGENT_NAME", m.tmux.SetEnvironment(sessionID, "BEADS_AGENT_NAME", fmt.Sprintf("%s/%s", m.rig.Name, polecat))) - // Hook the issue to the polecat if provided via --issue flag if opts.Issue != "" { agentID := fmt.Sprintf("%s/polecats/%s", m.rig.Name, polecat) diff --git a/internal/refinery/manager.go b/internal/refinery/manager.go index 1fbe16b2..e1e02d81 100644 --- a/internal/refinery/manager.go +++ b/internal/refinery/manager.go @@ -186,19 +186,19 @@ func (m *Manager) Start(foreground bool) error { } // Set environment variables (non-fatal: session works without these) - // Use shared RoleEnvVars for consistency across all role startup paths - envVars := config.RoleEnvVars("refinery", m.rig.Name, "") + // Use centralized AgentEnv for consistency across all role startup paths + townRoot := filepath.Dir(m.rig.Path) + envVars := config.AgentEnv(config.AgentEnvConfig{ + Role: "refinery", + Rig: m.rig.Name, + TownRoot: townRoot, + BeadsDir: beads.ResolveBeadsDir(m.rig.Path), + BeadsNoDaemon: true, + }) // Add refinery-specific flag envVars["GT_REFINERY"] = "1" - // Add beads environment - refinery uses rig-level beads - // Use ResolveBeadsDir to handle both tracked (mayor/rig) and local beads - beadsDir := beads.ResolveBeadsDir(m.rig.Path) - envVars["BEADS_DIR"] = beadsDir - envVars["BEADS_NO_DAEMON"] = "1" - envVars["BEADS_AGENT_NAME"] = envVars["BD_ACTOR"] - // Set all env vars in tmux session (for debugging) and they'll also be exported to Claude for k, v := range envVars { _ = t.SetEnvironment(sessionID, k, v) diff --git a/internal/witness/manager.go b/internal/witness/manager.go index c2181de9..91acd055 100644 --- a/internal/witness/manager.go +++ b/internal/witness/manager.go @@ -8,6 +8,7 @@ import ( "time" "github.com/steveyegge/gastown/internal/agent" + "github.com/steveyegge/gastown/internal/beads" "github.com/steveyegge/gastown/internal/claude" "github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/constants" @@ -164,8 +165,14 @@ func (m *Manager) Start(foreground bool) error { } // Set environment variables (non-fatal: session works without these) - // Use shared RoleEnvVars for consistency across all role startup paths - envVars := config.RoleEnvVars("witness", m.rig.Name, "") + // Use centralized AgentEnv for consistency across all role startup paths + townRoot := filepath.Dir(m.rig.Path) + envVars := config.AgentEnv(config.AgentEnvConfig{ + Role: "witness", + Rig: m.rig.Name, + TownRoot: townRoot, + BeadsDir: beads.ResolveBeadsDir(m.rig.Path), + }) for k, v := range envVars { _ = t.SetEnvironment(sessionID, k, v) }