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
err = crewMgr.Start(name, crew.StartOptions{
KillExisting: true, // Kill old session if running
Topic: "refresh", // Startup nudge topic
Interactive: true, // No --dangerously-skip-permissions
KillExisting: true, // Kill old session if running
Topic: "refresh", // Startup nudge topic
Interactive: true, // No --dangerously-skip-permissions
AgentOverride: crewAgentOverride,
})
if err != nil {
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
// Start() will create workspace if needed (idempotent)
err = crewMgr.Start(name, crew.StartOptions{
KillExisting: true, // Kill old session if running
Topic: "restart", // Startup nudge topic
KillExisting: true, // Kill old session if running
Topic: "restart", // Startup nudge topic
AgentOverride: crewAgentOverride,
})
if err != nil {
fmt.Printf("Error restarting %s: %v\n", arg, err)
@@ -425,8 +427,9 @@ func runCrewRestartAll() error {
// Use manager's Start() with restart options
err = crewMgr.Start(agent.AgentName, crew.StartOptions{
KillExisting: true, // Kill old session if running
Topic: "restart", // Startup nudge topic
KillExisting: true, // Kill old session if running
Topic: "restart", // Startup nudge topic
AgentOverride: crewAgentOverride,
})
if err != nil {
failed++

View File

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

View File

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

View File

@@ -50,6 +50,9 @@ type StartOptions struct {
// Interactive removes --dangerously-skip-permissions for interactive/refresh mode.
Interactive bool
// AgentOverride specifies an alternate agent alias (e.g., for testing).
AgentOverride string
}
// 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
// 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
if opts.Interactive {

View File

@@ -49,8 +49,9 @@ func (m *Manager) deaconDir() string {
}
// Start starts the deacon session.
// The deacon runs in a respawn loop for automatic recovery.
func (m *Manager) Start() error {
// agentOverride allows specifying an alternate agent alias (e.g., for testing).
// Restarts are handled by daemon via ensureDeaconRunning on each heartbeat.
func (m *Manager) Start(agentOverride string) error {
t := tmux.NewTmux()
sessionID := m.SessionName()
@@ -91,26 +92,25 @@ func (m *Manager) Start() error {
theme := tmux.DeaconTheme()
_ = t.ConfigureGasTownSession(sessionID, theme, "", "Deacon", "health-check")
// Launch Claude in a respawn loop for automatic recovery
// The respawn loop ensures the deacon restarts if Claude crashes
runtimeCmd := config.GetRuntimeCommand("")
respawnCmd := fmt.Sprintf(
`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`,
runtimeCmd,
)
// Launch Claude directly (no shell respawn loop)
// Restarts are handled by daemon via ensureDeaconRunning on each heartbeat
startupCmd, err := config.BuildAgentStartupCommandWithAgentOverride("deacon", "deacon", "", "", agentOverride)
if err != nil {
_ = t.KillSession(sessionID)
return fmt.Errorf("building startup command: %w", err)
}
// 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 {
_ = t.KillSession(sessionID)
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
return fmt.Errorf("starting Claude agent: %w", err)
}
// 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 {
// Non-fatal - try to continue anyway
}
@@ -120,8 +120,18 @@ func (m *Manager) Start() error {
time.Sleep(constants.ShutdownNotifyDelay)
// Note: Deacon doesn't get startup nudge due to respawn loop complexity
// The deacon uses its own patrol pattern defined in its CLAUDE.md/prime
// Inject startup nudge for predecessor discovery via /resume
_ = 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
}