From c61b67eb03a73795e04e8b818df2fc01c8b15054 Mon Sep 17 00:00:00 2001 From: Subhrajit Makur Date: Wed, 14 Jan 2026 03:04:22 +0530 Subject: [PATCH] fix(config): implement role_agents support in BuildStartupCommand (#19) (#456) * fix(config): implement role_agents support in BuildStartupCommand The role_agents field in TownSettings and RigSettings existed but was not being used by the startup command builders. All services fell back to the default agent instead of using role-specific agent assignments. Changes: - BuildStartupCommand now extracts GT_ROLE from envVars and uses ResolveRoleAgentConfig() for role-based agent selection - BuildStartupCommandWithAgentOverride follows the same pattern when no explicit override is provided - refinery/manager.go uses ResolveRoleAgentConfig with constants - cmd/start.go uses ResolveRoleAgentConfig with constants - Updated comments from hardcoded agent name to generic "agent" - Added ValidateAgentConfig() to check agent exists and binary is in PATH - Added lookupAgentConfigIfExists() helper for validation - ResolveRoleAgentConfig now warns to stderr and falls back to default if configured agent is invalid or binary is missing Resolution priority (now working): 1. Explicit --agent override 2. Rig's role_agents[role] (validated) 3. Town's role_agents[role] (validated) 4. Rig's agent setting 5. Town's default_agent 6. Hardcoded default fallback Adds tests for: - TestBuildStartupCommand_UsesRoleAgentsFromTownSettings - TestBuildStartupCommand_RigRoleAgentsOverridesTownRoleAgents - TestBuildAgentStartupCommand_UsesRoleAgents - TestValidateAgentConfig - TestResolveRoleAgentConfig_FallsBackOnInvalidAgent Fixes: role_agents configuration not being applied to services * fix(config): add GT_ROOT to BuildStartupCommandWithAgentOverride - Fixes missing GT_ROOT and GT_SESSION_ID_ENV exports in BuildStartupCommandWithAgentOverride, matching BuildStartupCommand behavior - Adds test for override priority over role_agents - Adds test verifying GT_ROOT is included in command This addresses the Greptile review comment about agents started with an override not having access to town-level resources. Co-authored-by: Steve Yegge --- internal/cmd/start.go | 12 +- internal/config/loader.go | 140 ++++++++++++++++-- internal/config/loader_test.go | 259 ++++++++++++++++++++++++++++++++- internal/refinery/manager.go | 12 +- 4 files changed, 393 insertions(+), 30 deletions(-) diff --git a/internal/cmd/start.go b/internal/cmd/start.go index de6271ad..20d71f26 100644 --- a/internal/cmd/start.go +++ b/internal/cmd/start.go @@ -381,10 +381,10 @@ func startConfiguredCrew(t *tmux.Tmux, rigs []*rig.Rig, townRoot string, mu *syn func startOrRestartCrewMember(t *tmux.Tmux, r *rig.Rig, crewName, townRoot string) (msg string, started bool) { sessionID := crewSessionName(r.Name, crewName) if running, _ := t.HasSession(sessionID); running { - // Session exists - check if Claude is still running - agentCfg := config.ResolveAgentConfig(townRoot, r.Path) + // Session exists - check if agent is still running + agentCfg := config.ResolveRoleAgentConfig(constants.RoleCrew, townRoot, r.Path) if !t.IsAgentRunning(sessionID, config.ExpectedPaneCommands(agentCfg)...) { - // Claude has exited, restart it + // Agent has exited, restart it // Build startup beacon for predecessor discovery via /resume address := fmt.Sprintf("%s/crew/%s", r.Name, crewName) beacon := session.FormatStartupNudge(session.StartupNudgeConfig{ @@ -392,11 +392,11 @@ func startOrRestartCrewMember(t *tmux.Tmux, r *rig.Rig, crewName, townRoot strin Sender: "human", Topic: "restart", }) - claudeCmd := config.BuildCrewStartupCommand(r.Name, crewName, r.Path, beacon) - if err := t.SendKeys(sessionID, claudeCmd); err != nil { + agentCmd := config.BuildCrewStartupCommand(r.Name, crewName, r.Path, beacon) + if err := t.SendKeys(sessionID, agentCmd); err != nil { return fmt.Sprintf(" %s %s/%s restart failed: %v\n", style.Dim.Render("○"), r.Name, crewName, err), false } - return fmt.Sprintf(" %s %s/%s Claude restarted\n", style.Bold.Render("✓"), r.Name, crewName), true + return fmt.Sprintf(" %s %s/%s agent restarted\n", style.Bold.Render("✓"), r.Name, crewName), true } return fmt.Sprintf(" %s %s/%s already running\n", style.Dim.Render("○"), r.Name, crewName), false } diff --git a/internal/config/loader.go b/internal/config/loader.go index 509ecb45..73d0b048 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "os/exec" "path/filepath" "sort" "strings" @@ -898,6 +899,48 @@ func ResolveAgentConfigWithOverride(townRoot, rigPath, agentOverride string) (*R return lookupAgentConfig(agentName, townSettings, rigSettings), agentName, nil } +// ValidateAgentConfig checks if an agent configuration is valid and the binary exists. +// Returns an error describing the issue, or nil if valid. +func ValidateAgentConfig(agentName string, townSettings *TownSettings, rigSettings *RigSettings) error { + // Check if agent exists in config + rc := lookupAgentConfigIfExists(agentName, townSettings, rigSettings) + if rc == nil { + return fmt.Errorf("agent %q not found in config or built-in presets", agentName) + } + + // Check if binary exists on system + if _, err := exec.LookPath(rc.Command); err != nil { + return fmt.Errorf("agent %q binary %q not found in PATH", agentName, rc.Command) + } + + return nil +} + +// lookupAgentConfigIfExists looks up an agent by name but returns nil if not found +// (instead of falling back to default). Used for validation. +func lookupAgentConfigIfExists(name string, townSettings *TownSettings, rigSettings *RigSettings) *RuntimeConfig { + // Check rig's custom agents + if rigSettings != nil && rigSettings.Agents != nil { + if custom, ok := rigSettings.Agents[name]; ok && custom != nil { + return fillRuntimeDefaults(custom) + } + } + + // Check town's custom agents + if townSettings != nil && townSettings.Agents != nil { + if custom, ok := townSettings.Agents[name]; ok && custom != nil { + return fillRuntimeDefaults(custom) + } + } + + // Check built-in presets + if preset := GetAgentPresetByName(name); preset != nil { + return RuntimeConfigFromPreset(AgentPreset(name)) + } + + return nil +} + // ResolveRoleAgentConfig resolves the agent configuration for a specific role. // It checks role-specific agent assignments before falling back to the default agent. // @@ -906,6 +949,9 @@ func ResolveAgentConfigWithOverride(townRoot, rigPath, agentOverride string) (*R // 2. Town's RoleAgents[role] - if set, look up that agent // 3. Fall back to ResolveAgentConfig (rig's Agent → town's DefaultAgent → "claude") // +// If a configured agent is not found or its binary doesn't exist, a warning is +// printed to stderr and it falls back to the default agent. +// // role is one of: "mayor", "deacon", "witness", "refinery", "polecat", "crew". // townRoot is the path to the town directory (e.g., ~/gt). // rigPath is the path to the rig directory (e.g., ~/gt/gastown), or empty for town-level roles. @@ -935,14 +981,22 @@ func ResolveRoleAgentConfig(role, townRoot, rigPath string) *RuntimeConfig { // Check rig's RoleAgents first if rigSettings != nil && rigSettings.RoleAgents != nil { if agentName, ok := rigSettings.RoleAgents[role]; ok && agentName != "" { - return lookupAgentConfig(agentName, townSettings, rigSettings) + if err := ValidateAgentConfig(agentName, townSettings, rigSettings); err != nil { + fmt.Fprintf(os.Stderr, "warning: role_agents[%s]=%s - %v, falling back to default\n", role, agentName, err) + } else { + return lookupAgentConfig(agentName, townSettings, rigSettings) + } } } // Check town's RoleAgents if townSettings.RoleAgents != nil { if agentName, ok := townSettings.RoleAgents[role]; ok && agentName != "" { - return lookupAgentConfig(agentName, townSettings, rigSettings) + if err := ValidateAgentConfig(agentName, townSettings, rigSettings); err != nil { + fmt.Fprintf(os.Stderr, "warning: role_agents[%s]=%s - %v, falling back to default\n", role, agentName, err) + } else { + return lookupAgentConfig(agentName, townSettings, rigSettings) + } } } @@ -1150,13 +1204,26 @@ func findTownRootFromCwd() (string, error) { // envVars is a map of environment variable names to values. // rigPath is optional - if empty, tries to detect town root from cwd. // prompt is optional - if provided, appended as the initial prompt. +// +// If envVars contains GT_ROLE, the function uses role-based agent resolution +// (ResolveRoleAgentConfig) to select the appropriate agent for the role. +// This enables per-role model selection via role_agents in settings. func BuildStartupCommand(envVars map[string]string, rigPath, prompt string) string { var rc *RuntimeConfig var townRoot string + + // Extract role from envVars for role-based agent resolution + role := envVars["GT_ROLE"] + if rigPath != "" { // Derive town root from rig path townRoot = filepath.Dir(rigPath) - rc = ResolveAgentConfig(townRoot, rigPath) + if role != "" { + // Use role-based agent resolution for per-role model selection + rc = ResolveRoleAgentConfig(role, townRoot, rigPath) + } else { + rc = ResolveAgentConfig(townRoot, rigPath) + } } else { // Try to detect town root from cwd for town-level agents (mayor, deacon) var err error @@ -1164,7 +1231,12 @@ func BuildStartupCommand(envVars map[string]string, rigPath, prompt string) stri if err != nil { rc = DefaultRuntimeConfig() } else { - rc = ResolveAgentConfig(townRoot, "") + if role != "" { + // Use role-based agent resolution for per-role model selection + rc = ResolveRoleAgentConfig(role, townRoot, "") + } else { + rc = ResolveAgentConfig(townRoot, "") + } } } @@ -1222,32 +1294,69 @@ func PrependEnv(command string, envVars map[string]string) string { // BuildStartupCommandWithAgentOverride builds a startup command like BuildStartupCommand, // but uses agentOverride if non-empty. +// +// Resolution priority: +// 1. agentOverride (explicit override) +// 2. role_agents[GT_ROLE] (if GT_ROLE is in envVars) +// 3. Default agent resolution (rig's Agent → town's DefaultAgent → "claude") func BuildStartupCommandWithAgentOverride(envVars map[string]string, rigPath, prompt, agentOverride string) (string, error) { var rc *RuntimeConfig + var townRoot string + + // Extract role from envVars for role-based agent resolution (when no override) + role := envVars["GT_ROLE"] if rigPath != "" { - townRoot := filepath.Dir(rigPath) - var err error - rc, _, err = ResolveAgentConfigWithOverride(townRoot, rigPath, agentOverride) - if err != nil { - return "", err + townRoot = filepath.Dir(rigPath) + if agentOverride != "" { + var err error + rc, _, err = ResolveAgentConfigWithOverride(townRoot, rigPath, agentOverride) + if err != nil { + return "", err + } + } else if role != "" { + // No override, use role-based agent resolution + rc = ResolveRoleAgentConfig(role, townRoot, rigPath) + } else { + rc = ResolveAgentConfig(townRoot, rigPath) } } else { - townRoot, err := findTownRootFromCwd() + var err error + townRoot, err = findTownRootFromCwd() if err != nil { rc = DefaultRuntimeConfig() } else { - var resolveErr error - rc, _, resolveErr = ResolveAgentConfigWithOverride(townRoot, "", agentOverride) - if resolveErr != nil { - return "", resolveErr + if agentOverride != "" { + var resolveErr error + rc, _, resolveErr = ResolveAgentConfigWithOverride(townRoot, "", agentOverride) + if resolveErr != nil { + return "", resolveErr + } + } else if role != "" { + // No override, use role-based agent resolution + rc = ResolveRoleAgentConfig(role, townRoot, "") + } else { + rc = ResolveAgentConfig(townRoot, "") } } } + // Copy env vars to avoid mutating caller map + resolvedEnv := make(map[string]string, len(envVars)+2) + for k, v := range envVars { + resolvedEnv[k] = v + } + // Add GT_ROOT so agents can find town-level resources (formulas, etc.) + if townRoot != "" { + resolvedEnv["GT_ROOT"] = townRoot + } + if rc.Session != nil && rc.Session.SessionIDEnv != "" { + resolvedEnv["GT_SESSION_ID_ENV"] = rc.Session.SessionIDEnv + } + // Build environment export prefix var exports []string - for k, v := range envVars { + for k, v := range resolvedEnv { exports = append(exports, fmt.Sprintf("%s=%s", k, v)) } sort.Strings(exports) @@ -1266,7 +1375,6 @@ func BuildStartupCommandWithAgentOverride(envVars map[string]string, rigPath, pr return cmd, nil } - // BuildAgentStartupCommand is a convenience function for starting agent sessions. // It sets standard environment variables (GT_ROLE, BD_ACTOR, GIT_AUTHOR_NAME) // and builds the full startup command. diff --git a/internal/config/loader_test.go b/internal/config/loader_test.go index 4e361ce8..345cf534 100644 --- a/internal/config/loader_test.go +++ b/internal/config/loader_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" "time" + + "github.com/steveyegge/gastown/internal/constants" ) func TestTownConfigRoundTrip(t *testing.T) { @@ -1193,6 +1195,189 @@ func TestBuildStartupCommand_UsesRigAgentWhenRigPathProvided(t *testing.T) { } } +func TestBuildStartupCommand_UsesRoleAgentsFromTownSettings(t *testing.T) { + t.Parallel() + townRoot := t.TempDir() + rigPath := filepath.Join(townRoot, "testrig") + + // Configure town settings with role_agents + townSettings := NewTownSettings() + townSettings.DefaultAgent = "claude" + townSettings.RoleAgents = map[string]string{ + constants.RoleRefinery: "gemini", + constants.RoleWitness: "codex", + } + if err := SaveTownSettings(TownSettingsPath(townRoot), townSettings); err != nil { + t.Fatalf("SaveTownSettings: %v", err) + } + + // Create empty rig settings (no agent override) + rigSettings := NewRigSettings() + if err := SaveRigSettings(RigSettingsPath(rigPath), rigSettings); err != nil { + t.Fatalf("SaveRigSettings: %v", err) + } + + t.Run("refinery role gets gemini from role_agents", func(t *testing.T) { + cmd := BuildStartupCommand(map[string]string{"GT_ROLE": constants.RoleRefinery}, rigPath, "") + if !strings.Contains(cmd, "gemini") { + t.Fatalf("expected gemini for refinery role, got: %q", cmd) + } + }) + + t.Run("witness role gets codex from role_agents", func(t *testing.T) { + cmd := BuildStartupCommand(map[string]string{"GT_ROLE": constants.RoleWitness}, rigPath, "") + if !strings.Contains(cmd, "codex") { + t.Fatalf("expected codex for witness role, got: %q", cmd) + } + }) + + t.Run("crew role falls back to default_agent (not in role_agents)", func(t *testing.T) { + cmd := BuildStartupCommand(map[string]string{"GT_ROLE": constants.RoleCrew}, rigPath, "") + if !strings.Contains(cmd, "claude") { + t.Fatalf("expected claude fallback for crew role, got: %q", cmd) + } + }) + + t.Run("no role falls back to default resolution", func(t *testing.T) { + cmd := BuildStartupCommand(map[string]string{}, rigPath, "") + if !strings.Contains(cmd, "claude") { + t.Fatalf("expected claude for no role, got: %q", cmd) + } + }) +} + +func TestBuildStartupCommand_RigRoleAgentsOverridesTownRoleAgents(t *testing.T) { + t.Parallel() + townRoot := t.TempDir() + rigPath := filepath.Join(townRoot, "testrig") + + // Town settings has witness = gemini + townSettings := NewTownSettings() + townSettings.DefaultAgent = "claude" + townSettings.RoleAgents = map[string]string{ + constants.RoleWitness: "gemini", + } + if err := SaveTownSettings(TownSettingsPath(townRoot), townSettings); err != nil { + t.Fatalf("SaveTownSettings: %v", err) + } + + // Rig settings overrides witness to codex + rigSettings := NewRigSettings() + rigSettings.RoleAgents = map[string]string{ + constants.RoleWitness: "codex", + } + if err := SaveRigSettings(RigSettingsPath(rigPath), rigSettings); err != nil { + t.Fatalf("SaveRigSettings: %v", err) + } + + cmd := BuildStartupCommand(map[string]string{"GT_ROLE": constants.RoleWitness}, rigPath, "") + if !strings.Contains(cmd, "codex") { + t.Fatalf("expected codex from rig role_agents override, got: %q", cmd) + } + if strings.Contains(cmd, "gemini") { + t.Fatalf("did not expect town role_agents (gemini) in command: %q", cmd) + } +} + +func TestBuildAgentStartupCommand_UsesRoleAgents(t *testing.T) { + t.Parallel() + townRoot := t.TempDir() + rigPath := filepath.Join(townRoot, "testrig") + + // Configure town settings with role_agents + townSettings := NewTownSettings() + townSettings.DefaultAgent = "claude" + townSettings.RoleAgents = map[string]string{ + constants.RoleRefinery: "codex", + } + if err := SaveTownSettings(TownSettingsPath(townRoot), townSettings); err != nil { + t.Fatalf("SaveTownSettings: %v", err) + } + + // Create empty rig settings + rigSettings := NewRigSettings() + if err := SaveRigSettings(RigSettingsPath(rigPath), rigSettings); err != nil { + t.Fatalf("SaveRigSettings: %v", err) + } + + // BuildAgentStartupCommand passes role via GT_ROLE env var + cmd := BuildAgentStartupCommand(constants.RoleRefinery, "testrig/refinery", rigPath, "") + if !strings.Contains(cmd, "codex") { + t.Fatalf("expected codex for refinery role, got: %q", cmd) + } + if !strings.Contains(cmd, "GT_ROLE="+constants.RoleRefinery) { + t.Fatalf("expected GT_ROLE=%s in command: %q", constants.RoleRefinery, cmd) + } +} + +func TestValidateAgentConfig(t *testing.T) { + t.Parallel() + + t.Run("valid built-in agent", func(t *testing.T) { + // claude is a built-in preset and binary should exist + err := ValidateAgentConfig("claude", nil, nil) + // Note: This may fail if claude binary is not installed, which is expected + if err != nil && !strings.Contains(err.Error(), "not found in PATH") { + t.Errorf("unexpected error for claude: %v", err) + } + }) + + t.Run("invalid agent name", func(t *testing.T) { + err := ValidateAgentConfig("nonexistent-agent-xyz", nil, nil) + if err == nil { + t.Error("expected error for nonexistent agent") + } + if !strings.Contains(err.Error(), "not found in config or built-in presets") { + t.Errorf("unexpected error message: %v", err) + } + }) + + t.Run("custom agent with missing binary", func(t *testing.T) { + townSettings := NewTownSettings() + townSettings.Agents = map[string]*RuntimeConfig{ + "my-custom-agent": { + Command: "nonexistent-binary-xyz123", + Args: []string{"--some-flag"}, + }, + } + err := ValidateAgentConfig("my-custom-agent", townSettings, nil) + if err == nil { + t.Error("expected error for missing binary") + } + if !strings.Contains(err.Error(), "not found in PATH") { + t.Errorf("unexpected error message: %v", err) + } + }) +} + +func TestResolveRoleAgentConfig_FallsBackOnInvalidAgent(t *testing.T) { + t.Parallel() + townRoot := t.TempDir() + rigPath := filepath.Join(townRoot, "testrig") + + // Configure town settings with an invalid agent for refinery + townSettings := NewTownSettings() + townSettings.DefaultAgent = "claude" + townSettings.RoleAgents = map[string]string{ + constants.RoleRefinery: "nonexistent-agent-xyz", // Invalid agent + } + if err := SaveTownSettings(TownSettingsPath(townRoot), townSettings); err != nil { + t.Fatalf("SaveTownSettings: %v", err) + } + + // Create empty rig settings + rigSettings := NewRigSettings() + if err := SaveRigSettings(RigSettingsPath(rigPath), rigSettings); err != nil { + t.Fatalf("SaveRigSettings: %v", err) + } + + // Should fall back to default (claude) when agent is invalid + rc := ResolveRoleAgentConfig(constants.RoleRefinery, townRoot, rigPath) + if rc.Command != "claude" { + t.Errorf("expected fallback to claude, got: %s", rc.Command) + } +} + func TestGetRuntimeCommand_UsesRigAgentWhenRigPathProvided(t *testing.T) { t.Parallel() townRoot := t.TempDir() @@ -2204,8 +2389,8 @@ func TestEscalationConfigGetRouteForSeverity(t *testing.T) { }{ {SeverityLow, []string{"bead"}}, {SeverityMedium, []string{"bead", "mail:mayor"}}, - {SeverityHigh, []string{"bead", "mail:mayor"}}, // fallback for missing - {SeverityCritical, []string{"bead", "mail:mayor"}}, // fallback for missing + {SeverityHigh, []string{"bead", "mail:mayor"}}, // fallback for missing + {SeverityCritical, []string{"bead", "mail:mayor"}}, // fallback for missing } for _, tt := range tests { @@ -2321,3 +2506,73 @@ func TestEscalationConfigPath(t *testing.T) { t.Errorf("EscalationConfigPath = %q, want %q", path, expected) } } + +func TestBuildStartupCommandWithAgentOverride_PriorityOverRoleAgents(t *testing.T) { + t.Parallel() + townRoot := t.TempDir() + rigPath := filepath.Join(townRoot, "testrig") + + // Configure town settings with role_agents: refinery = codex + townSettings := NewTownSettings() + townSettings.DefaultAgent = "claude" + townSettings.RoleAgents = map[string]string{ + constants.RoleRefinery: "codex", + } + if err := SaveTownSettings(TownSettingsPath(townRoot), townSettings); err != nil { + t.Fatalf("SaveTownSettings: %v", err) + } + + // Create empty rig settings + rigSettings := NewRigSettings() + if err := SaveRigSettings(RigSettingsPath(rigPath), rigSettings); err != nil { + t.Fatalf("SaveRigSettings: %v", err) + } + + // agentOverride = "gemini" should take priority over role_agents[refinery] = "codex" + cmd, err := BuildStartupCommandWithAgentOverride( + map[string]string{"GT_ROLE": constants.RoleRefinery}, + rigPath, + "", + "gemini", // explicit override + ) + if err != nil { + t.Fatalf("BuildStartupCommandWithAgentOverride: %v", err) + } + + if !strings.Contains(cmd, "gemini") { + t.Errorf("expected gemini (override) in command, got: %q", cmd) + } + if strings.Contains(cmd, "codex") { + t.Errorf("did not expect codex (role_agents) when override is set: %q", cmd) + } +} + +func TestBuildStartupCommandWithAgentOverride_IncludesGTRoot(t *testing.T) { + t.Parallel() + townRoot := t.TempDir() + rigPath := filepath.Join(townRoot, "testrig") + + // Create necessary config files + townSettings := NewTownSettings() + if err := SaveTownSettings(TownSettingsPath(townRoot), townSettings); err != nil { + t.Fatalf("SaveTownSettings: %v", err) + } + if err := SaveRigSettings(RigSettingsPath(rigPath), NewRigSettings()); err != nil { + t.Fatalf("SaveRigSettings: %v", err) + } + + cmd, err := BuildStartupCommandWithAgentOverride( + map[string]string{"GT_ROLE": constants.RoleWitness}, + rigPath, + "", + "gemini", + ) + if err != nil { + t.Fatalf("BuildStartupCommandWithAgentOverride: %v", err) + } + + // Should include GT_ROOT in export + if !strings.Contains(cmd, "GT_ROOT="+townRoot) { + t.Errorf("expected GT_ROOT=%s in command, got: %q", townRoot, cmd) + } +} diff --git a/internal/refinery/manager.go b/internal/refinery/manager.go index 81484aac..0fc1c9de 100644 --- a/internal/refinery/manager.go +++ b/internal/refinery/manager.go @@ -116,7 +116,7 @@ func (m *Manager) Start(foreground bool, agentOverride string) error { if foreground { // In foreground mode, check tmux session (no PID inference per ZFC) townRoot := filepath.Dir(m.rig.Path) - agentCfg := config.ResolveAgentConfig(townRoot, m.rig.Path) + agentCfg := config.ResolveRoleAgentConfig(constants.RoleRefinery, townRoot, m.rig.Path) if running, _ := t.HasSession(sessionID); running && t.IsAgentRunning(sessionID, config.ExpectedPaneCommands(agentCfg)...) { return ErrAlreadyRunning } @@ -138,15 +138,15 @@ func (m *Manager) Start(foreground bool, agentOverride string) error { // Background mode: check if session already exists running, _ := t.HasSession(sessionID) if running { - // Session exists - check if Claude is actually running (healthy vs zombie) + // Session exists - check if agent is actually running (healthy vs zombie) townRoot := filepath.Dir(m.rig.Path) - agentCfg := config.ResolveAgentConfig(townRoot, m.rig.Path) + agentCfg := config.ResolveRoleAgentConfig(constants.RoleRefinery, townRoot, m.rig.Path) if t.IsAgentRunning(sessionID, config.ExpectedPaneCommands(agentCfg)...) { - // Healthy - Claude is running + // Healthy - agent is running return ErrAlreadyRunning } - // Zombie - tmux alive but Claude dead. Kill and recreate. - _, _ = fmt.Fprintln(m.output, "⚠ Detected zombie session (tmux alive, Claude dead). Recreating...") + // Zombie - tmux alive but agent dead. Kill and recreate. + _, _ = fmt.Fprintln(m.output, "⚠ Detected zombie session (tmux alive, agent dead). Recreating...") if err := t.KillSession(sessionID); err != nil { return fmt.Errorf("killing zombie session: %w", err) }