fix: restore agent override support lost in manager refactors

The manager refactors (ea8bef2, 72544cc0) conflicted with the agent
override feature, causing regressions:

Deacon (ea8bef2):
- Lost agentOverride parameter
- Re-added respawn loop (removed in 5f2e16f)
- Lost GUPP (startup + propulsion nudges)

Crew (72544cc0):
- Lost agentOverride wiring to StartOptions
- --agent flag had no effect on crew refresh/restart

This fix restores agent override support and GUPP while keeping
improvements from the manager refactors (zombie detection, etc).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
julianknutsen
2026-01-07 18:07:25 -08:00
committed by Steve Yegge
parent 00a59dec44
commit c91ab85457
5 changed files with 44 additions and 23 deletions

View File

@@ -236,9 +236,10 @@ func runCrewRefresh(cmd *cobra.Command, args []string) error {
// Use manager's Start() with refresh options // Use manager's Start() with refresh options
err = crewMgr.Start(name, crew.StartOptions{ err = crewMgr.Start(name, crew.StartOptions{
KillExisting: true, // Kill old session if running KillExisting: true, // Kill old session if running
Topic: "refresh", // Startup nudge topic Topic: "refresh", // Startup nudge topic
Interactive: true, // No --dangerously-skip-permissions Interactive: true, // No --dangerously-skip-permissions
AgentOverride: crewAgentOverride,
}) })
if err != nil { if err != nil {
return fmt.Errorf("starting crew session: %w", err) return fmt.Errorf("starting crew session: %w", err)
@@ -346,8 +347,9 @@ func runCrewRestart(cmd *cobra.Command, args []string) error {
// Use manager's Start() with restart options // Use manager's Start() with restart options
// Start() will create workspace if needed (idempotent) // Start() will create workspace if needed (idempotent)
err = crewMgr.Start(name, crew.StartOptions{ err = crewMgr.Start(name, crew.StartOptions{
KillExisting: true, // Kill old session if running KillExisting: true, // Kill old session if running
Topic: "restart", // Startup nudge topic Topic: "restart", // Startup nudge topic
AgentOverride: crewAgentOverride,
}) })
if err != nil { if err != nil {
fmt.Printf("Error restarting %s: %v\n", arg, err) fmt.Printf("Error restarting %s: %v\n", arg, err)
@@ -425,8 +427,9 @@ func runCrewRestartAll() error {
// Use manager's Start() with restart options // Use manager's Start() with restart options
err = crewMgr.Start(agent.AgentName, crew.StartOptions{ err = crewMgr.Start(agent.AgentName, crew.StartOptions{
KillExisting: true, // Kill old session if running KillExisting: true, // Kill old session if running
Topic: "restart", // Startup nudge topic Topic: "restart", // Startup nudge topic
AgentOverride: crewAgentOverride,
}) })
if err != nil { if err != nil {
failed++ failed++

View File

@@ -203,7 +203,7 @@ func startCoreAgents(townRoot string, agentOverride string) error {
// Start Deacon (health monitor) // Start Deacon (health monitor)
deaconMgr := deacon.NewManager(townRoot) deaconMgr := deacon.NewManager(townRoot)
if err := deaconMgr.Start(); err != nil { if err := deaconMgr.Start(agentOverride); err != nil {
if err == deacon.ErrAlreadyRunning { if err == deacon.ErrAlreadyRunning {
fmt.Printf(" %s Deacon already running\n", style.Dim.Render("○")) fmt.Printf(" %s Deacon already running\n", style.Dim.Render("○"))
} else { } else {
@@ -694,6 +694,7 @@ func runStartCrew(cmd *cobra.Command, args []string) error {
err = crewMgr.Start(name, crew.StartOptions{ err = crewMgr.Start(name, crew.StartOptions{
Account: startCrewAccount, Account: startCrewAccount,
ClaudeConfigDir: claudeConfigDir, ClaudeConfigDir: claudeConfigDir,
AgentOverride: startCrewAgentOverride,
}) })
if err != nil { if err != nil {
if errors.Is(err, crew.ErrSessionRunning) { if errors.Is(err, crew.ErrSessionRunning) {

View File

@@ -83,7 +83,7 @@ func runUp(cmd *cobra.Command, args []string) error {
// 2. Deacon (Claude agent) // 2. Deacon (Claude agent)
deaconMgr := deacon.NewManager(townRoot) deaconMgr := deacon.NewManager(townRoot)
if err := deaconMgr.Start(); err != nil { if err := deaconMgr.Start(""); err != nil {
if err == deacon.ErrAlreadyRunning { if err == deacon.ErrAlreadyRunning {
printStatus("Deacon", true, deaconMgr.SessionName()) printStatus("Deacon", true, deaconMgr.SessionName())
} else { } else {

View File

@@ -50,6 +50,9 @@ type StartOptions struct {
// Interactive removes --dangerously-skip-permissions for interactive/refresh mode. // Interactive removes --dangerously-skip-permissions for interactive/refresh mode.
Interactive bool Interactive bool
// AgentOverride specifies an alternate agent alias (e.g., for testing).
AgentOverride string
} }
// validateCrewName checks that a crew name is safe and valid. // validateCrewName checks that a crew name is safe and valid.
@@ -514,7 +517,11 @@ func (m *Manager) Start(name string, opts StartOptions) error {
// Start claude with environment exports and beacon as initial prompt // Start claude with environment exports and beacon as initial prompt
// SessionStart hook handles context loading (gt prime --hook) // SessionStart hook handles context loading (gt prime --hook)
claudeCmd := config.BuildCrewStartupCommand(m.rig.Name, name, m.rig.Path, beacon) claudeCmd, err := config.BuildCrewStartupCommandWithAgentOverride(m.rig.Name, name, m.rig.Path, beacon, opts.AgentOverride)
if err != nil {
_ = t.KillSession(sessionID)
return fmt.Errorf("building startup command: %w", err)
}
// For interactive/refresh mode, remove --dangerously-skip-permissions // For interactive/refresh mode, remove --dangerously-skip-permissions
if opts.Interactive { if opts.Interactive {

View File

@@ -49,8 +49,9 @@ func (m *Manager) deaconDir() string {
} }
// Start starts the deacon session. // Start starts the deacon session.
// The deacon runs in a respawn loop for automatic recovery. // agentOverride allows specifying an alternate agent alias (e.g., for testing).
func (m *Manager) Start() error { // Restarts are handled by daemon via ensureDeaconRunning on each heartbeat.
func (m *Manager) Start(agentOverride string) error {
t := tmux.NewTmux() t := tmux.NewTmux()
sessionID := m.SessionName() sessionID := m.SessionName()
@@ -91,26 +92,25 @@ func (m *Manager) Start() error {
theme := tmux.DeaconTheme() theme := tmux.DeaconTheme()
_ = t.ConfigureGasTownSession(sessionID, theme, "", "Deacon", "health-check") _ = t.ConfigureGasTownSession(sessionID, theme, "", "Deacon", "health-check")
// Launch Claude in a respawn loop for automatic recovery // Launch Claude directly (no shell respawn loop)
// The respawn loop ensures the deacon restarts if Claude crashes // Restarts are handled by daemon via ensureDeaconRunning on each heartbeat
runtimeCmd := config.GetRuntimeCommand("") startupCmd, err := config.BuildAgentStartupCommandWithAgentOverride("deacon", "deacon", "", "", agentOverride)
respawnCmd := fmt.Sprintf( if err != nil {
`export GT_ROLE=deacon BD_ACTOR=deacon GIT_AUTHOR_NAME=deacon && while true; do echo "⛪ Starting Deacon session..."; %s; echo ""; echo "Deacon exited. Restarting in 2s... (Ctrl-C to stop)"; sleep 2; done`, _ = t.KillSession(sessionID)
runtimeCmd, return fmt.Errorf("building startup command: %w", err)
) }
// Wait for shell to be ready before sending keys (prevents "can't find pane" under load) // Wait for shell to be ready before sending keys (prevents "can't find pane" under load)
if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil { if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil {
_ = t.KillSession(sessionID) _ = t.KillSession(sessionID)
return fmt.Errorf("waiting for shell: %w", err) return fmt.Errorf("waiting for shell: %w", err)
} }
if err := t.SendKeysDelayed(sessionID, respawnCmd, 200); err != nil { if err := t.SendKeysDelayed(sessionID, startupCmd, 200); err != nil {
_ = t.KillSession(sessionID) // best-effort cleanup _ = t.KillSession(sessionID) // best-effort cleanup
return fmt.Errorf("starting Claude agent: %w", err) return fmt.Errorf("starting Claude agent: %w", err)
} }
// Wait for Claude to start (non-fatal) // Wait for Claude to start (non-fatal)
// Note: Deacon respawn loop makes this tricky - Claude restarts multiple times
if err := t.WaitForCommand(sessionID, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil { if err := t.WaitForCommand(sessionID, constants.SupportedShells, constants.ClaudeStartTimeout); err != nil {
// Non-fatal - try to continue anyway // Non-fatal - try to continue anyway
} }
@@ -120,8 +120,18 @@ func (m *Manager) Start() error {
time.Sleep(constants.ShutdownNotifyDelay) time.Sleep(constants.ShutdownNotifyDelay)
// Note: Deacon doesn't get startup nudge due to respawn loop complexity // Inject startup nudge for predecessor discovery via /resume
// The deacon uses its own patrol pattern defined in its CLAUDE.md/prime _ = session.StartupNudge(t, sessionID, session.StartupNudgeConfig{
Recipient: "deacon",
Sender: "daemon",
Topic: "patrol",
}) // Non-fatal
// GUPP: Gas Town Universal Propulsion Principle
// Send the propulsion nudge to trigger autonomous patrol execution.
// Wait for beacon to be fully processed (needs to be separate prompt)
time.Sleep(2 * time.Second)
_ = t.NudgeSession(sessionID, session.PropulsionNudgeForRole("deacon", deaconDir)) // Non-fatal
return nil return nil
} }