From 02ca9e43fa9150e6feae4ed77aa617e783be06e0 Mon Sep 17 00:00:00 2001 From: jv Date: Wed, 7 Jan 2026 12:47:44 +1300 Subject: [PATCH] fix: honor rig agent when starting witness/refinery --- internal/cmd/start.go | 2 +- internal/config/loader_test.go | 47 ++++++++++++++++++++++++++++++++++ internal/daemon/lifecycle.go | 18 ++++++++----- internal/refinery/manager.go | 15 +++++------ 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/internal/cmd/start.go b/internal/cmd/start.go index 32124921..59da6ef3 100644 --- a/internal/cmd/start.go +++ b/internal/cmd/start.go @@ -371,7 +371,7 @@ func ensureRefinerySession(rigName string, r *rig.Rig) (bool, error) { // Launch Claude directly (no respawn loop - daemon handles restart) // Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes - if err := t.SendKeys(sessionName, config.BuildAgentStartupCommand("refinery", bdActor, "", "")); err != nil { + if err := t.SendKeys(sessionName, config.BuildAgentStartupCommand("refinery", bdActor, r.Path, "")); err != nil { return false, fmt.Errorf("sending command: %w", err) } diff --git a/internal/config/loader_test.go b/internal/config/loader_test.go index 83752f60..1bc30cf2 100644 --- a/internal/config/loader_test.go +++ b/internal/config/loader_test.go @@ -1118,6 +1118,53 @@ func TestBuildCrewStartupCommandWithAgentOverride(t *testing.T) { } } +func TestBuildStartupCommand_UsesRigAgentWhenRigPathProvided(t *testing.T) { + townRoot := t.TempDir() + rigPath := filepath.Join(townRoot, "testrig") + + townSettings := NewTownSettings() + townSettings.DefaultAgent = "gemini" + if err := SaveTownSettings(TownSettingsPath(townRoot), townSettings); err != nil { + t.Fatalf("SaveTownSettings: %v", err) + } + + rigSettings := NewRigSettings() + rigSettings.Agent = "codex" + if err := SaveRigSettings(RigSettingsPath(rigPath), rigSettings); err != nil { + t.Fatalf("SaveRigSettings: %v", err) + } + + cmd := BuildStartupCommand(map[string]string{"GT_ROLE": "witness"}, rigPath, "") + if !strings.Contains(cmd, "codex") { + t.Fatalf("expected rig agent (codex) in command: %q", cmd) + } + if strings.Contains(cmd, "gemini --approval-mode yolo") { + t.Fatalf("did not expect town default agent in command: %q", cmd) + } +} + +func TestGetRuntimeCommand_UsesRigAgentWhenRigPathProvided(t *testing.T) { + townRoot := t.TempDir() + rigPath := filepath.Join(townRoot, "testrig") + + townSettings := NewTownSettings() + townSettings.DefaultAgent = "gemini" + if err := SaveTownSettings(TownSettingsPath(townRoot), townSettings); err != nil { + t.Fatalf("SaveTownSettings: %v", err) + } + + rigSettings := NewRigSettings() + rigSettings.Agent = "codex" + if err := SaveRigSettings(RigSettingsPath(rigPath), rigSettings); err != nil { + t.Fatalf("SaveRigSettings: %v", err) + } + + cmd := GetRuntimeCommand(rigPath) + if !strings.HasPrefix(cmd, "codex") { + t.Fatalf("GetRuntimeCommand() = %q, want prefix %q", cmd, "codex") + } +} + func TestLoadRuntimeConfigFromSettings(t *testing.T) { // Create temp rig with custom runtime config dir := t.TempDir() diff --git a/internal/daemon/lifecycle.go b/internal/daemon/lifecycle.go index 39c0b25f..2b28016e 100644 --- a/internal/daemon/lifecycle.go +++ b/internal/daemon/lifecycle.go @@ -211,8 +211,8 @@ func (d *Daemon) executeLifecycleAction(request *LifecycleRequest) error { // ParsedIdentity holds the components extracted from an agent identity string. // This is used to look up the appropriate role bead for lifecycle config. type ParsedIdentity struct { - RoleType string // mayor, deacon, witness, refinery, crew, polecat - RigName string // Empty for town-level agents (mayor, deacon) + RoleType string // mayor, deacon, witness, refinery, crew, polecat + RigName string // Empty for town-level agents (mayor, deacon) AgentName string // Empty for singletons (mayor, deacon, witness, refinery) } @@ -436,12 +436,17 @@ func (d *Daemon) getStartCommand(roleConfig *beads.RoleConfig, parsed *ParsedIde return beads.ExpandRolePattern(roleConfig.StartCommand, d.config.TownRoot, parsed.RigName, parsed.AgentName, parsed.RoleType) } + rigPath := "" + if parsed != nil && parsed.RigName != "" { + rigPath = filepath.Join(d.config.TownRoot, parsed.RigName) + } + // Default command for all agents - use runtime config - defaultCmd := "exec " + config.GetRuntimeCommand("") + defaultCmd := "exec " + config.GetRuntimeCommand(rigPath) // Polecats need environment variables set in the command if parsed.RoleType == "polecat" { - return config.BuildPolecatStartupCommand(parsed.RigName, parsed.AgentName, "", "") + return config.BuildPolecatStartupCommand(parsed.RigName, parsed.AgentName, rigPath, "") } return defaultCmd @@ -572,7 +577,7 @@ func (d *Daemon) getAgentBeadInfo(agentBeadID string) (*AgentBeadInfo, error) { Type string `json:"issue_type"` Description string `json:"description"` UpdatedAt string `json:"updated_at"` - HookBead string `json:"hook_bead"` // Read from database column + HookBead string `json:"hook_bead"` // Read from database column AgentState string `json:"agent_state"` // Read from database column } @@ -913,7 +918,7 @@ func (d *Daemon) getDeadAgents() []deadAgentInfo { var agents []struct { ID string `json:"id"` Type string `json:"issue_type"` - HookBead string `json:"hook_bead"` // Read from database column + HookBead string `json:"hook_bead"` // Read from database column AgentState string `json:"agent_state"` // Read from database column } @@ -972,4 +977,3 @@ Action needed: Either restart the agent or reassign the work.`, d.logger.Printf("Notified %s of orphaned work for %s", witnessAddr, agentID) } } - diff --git a/internal/refinery/manager.go b/internal/refinery/manager.go index b461de3f..2b8cd48d 100644 --- a/internal/refinery/manager.go +++ b/internal/refinery/manager.go @@ -26,9 +26,9 @@ import ( // Common errors var ( - ErrNotRunning = errors.New("refinery not running") + ErrNotRunning = errors.New("refinery not running") ErrAlreadyRunning = errors.New("refinery already running") - ErrNoQueue = errors.New("no items in queue") + ErrNoQueue = errors.New("no items in queue") ) // Manager handles refinery lifecycle and queue operations. @@ -205,7 +205,7 @@ func (m *Manager) Start(foreground bool) error { // NOTE: No gt prime injection needed - SessionStart hook handles it automatically // Restarts are handled by daemon via LIFECYCLE mail, not shell loops // Export GT_ROLE and BD_ACTOR in the command since tmux SetEnvironment only affects new panes - command := config.BuildAgentStartupCommand("refinery", bdActor, "", "") + command := config.BuildAgentStartupCommand("refinery", bdActor, m.rig.Path, "") if err := t.SendKeys(sessionID, command); err != nil { // Clean up the session on failure (best-effort cleanup) _ = t.KillSession(sessionID) @@ -566,7 +566,6 @@ func (m *Manager) pushWithRetry(targetBranch string, config MergeConfig) error { return fmt.Errorf("push failed after %d retries: %v", config.PushRetryCount, lastErr) } - // formatAge formats a duration since the given time. func formatAge(t time.Time) string { d := time.Since(t) @@ -587,8 +586,8 @@ func formatAge(t time.Time) string { func (m *Manager) notifyWorkerConflict(mr *MergeRequest) { router := mail.NewRouter(m.workDir) msg := &mail.Message{ - From: fmt.Sprintf("%s/refinery", m.rig.Name), - To: fmt.Sprintf("%s/%s", m.rig.Name, mr.Worker), + From: fmt.Sprintf("%s/refinery", m.rig.Name), + To: fmt.Sprintf("%s/%s", m.rig.Name, mr.Worker), Subject: "Merge conflict - rebase required", Body: fmt.Sprintf(`Your branch %s has conflicts with %s. @@ -608,8 +607,8 @@ Then the Refinery will retry the merge.`, func (m *Manager) notifyWorkerMerged(mr *MergeRequest) { router := mail.NewRouter(m.workDir) msg := &mail.Message{ - From: fmt.Sprintf("%s/refinery", m.rig.Name), - To: fmt.Sprintf("%s/%s", m.rig.Name, mr.Worker), + From: fmt.Sprintf("%s/refinery", m.rig.Name), + To: fmt.Sprintf("%s/%s", m.rig.Name, mr.Worker), Subject: "Work merged successfully", Body: fmt.Sprintf(`Your branch %s has been merged to %s.