From 67e1b1b06e89d065851810099a10802f3bf4fa86 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 27 Dec 2025 14:49:51 -0800 Subject: [PATCH] Add gt crew restart --all for batch crew restarts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add --all flag to restart all running crew sessions - Add --dry-run flag to preview without restarting - Add --rig filter to target specific rig - Extract restartCrewSession helper for reuse 🤝 Filed gt-1kljv for adding tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/crew.go | 30 ++++++- internal/cmd/crew_lifecycle.go | 160 +++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 4 deletions(-) diff --git a/internal/cmd/crew.go b/internal/cmd/crew.go index e21912fb..1f9c62ec 100644 --- a/internal/cmd/crew.go +++ b/internal/cmd/crew.go @@ -1,6 +1,8 @@ package cmd import ( + "fmt" + "github.com/spf13/cobra" ) @@ -14,6 +16,8 @@ var ( crewDetached bool crewMessage string crewAccount string + crewAll bool + crewDryRun bool ) var crewCmd = &cobra.Command{ @@ -148,7 +152,7 @@ Examples: } var crewRestartCmd = &cobra.Command{ - Use: "restart ", + Use: "restart [name]", Aliases: []string{"rs"}, Short: "Kill and restart crew workspace session", Long: `Kill the tmux session and restart fresh with Claude. @@ -161,10 +165,26 @@ The command will: 2. Start fresh session with Claude 3. Run gt prime to reinitialize context +Use --all to restart all running crew sessions across all rigs. + Examples: gt crew restart dave # Restart dave's session - gt crew rs emma # Same, using alias`, - Args: cobra.ExactArgs(1), + gt crew rs emma # Same, using alias + gt crew restart --all # Restart all running crew sessions + gt crew restart --all --rig beads # Restart all crew in beads rig + gt crew restart --all --dry-run # Preview what would be restarted`, + Args: func(cmd *cobra.Command, args []string) error { + if crewAll { + if len(args) > 0 { + return fmt.Errorf("cannot specify both --all and a name") + } + return nil + } + if len(args) != 1 { + return fmt.Errorf("requires exactly 1 argument (or --all)") + } + return nil + }, RunE: runCrewRestart, } @@ -258,7 +278,9 @@ func init() { crewPristineCmd.Flags().StringVar(&crewRig, "rig", "", "Filter by rig name") crewPristineCmd.Flags().BoolVar(&crewJSON, "json", false, "Output as JSON") - crewRestartCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to use") + crewRestartCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to use (filter when using --all)") + crewRestartCmd.Flags().BoolVar(&crewAll, "all", false, "Restart all running crew sessions") + crewRestartCmd.Flags().BoolVar(&crewDryRun, "dry-run", false, "Show what would be restarted without restarting") crewStartCmd.Flags().StringVar(&crewRig, "rig", "", "Rig to use") crewStartCmd.Flags().StringVar(&crewAccount, "account", "", "Claude Code account handle to use") diff --git a/internal/cmd/crew_lifecycle.go b/internal/cmd/crew_lifecycle.go index cc8fda92..2b2e09af 100644 --- a/internal/cmd/crew_lifecycle.go +++ b/internal/cmd/crew_lifecycle.go @@ -177,6 +177,11 @@ func runCrewStart(cmd *cobra.Command, args []string) error { } func runCrewRestart(cmd *cobra.Command, args []string) error { + // Handle --all flag + if crewAll { + return runCrewRestartAll() + } + name := args[0] // Parse rig/name format (e.g., "beads/emma" -> rig=beads, name=emma) if rig, crewName, ok := parseRigSlashName(name); ok { @@ -269,3 +274,158 @@ func runCrewRestart(cmd *cobra.Command, args []string) error { return nil } + +// runCrewRestartAll restarts all running crew sessions. +// If crewRig is set, only restarts crew in that rig. +func runCrewRestartAll() error { + // Get all agent sessions (including polecats to find crew) + agents, err := getAgentSessions(true) + if err != nil { + return fmt.Errorf("listing sessions: %w", err) + } + + // Filter to crew agents only + var targets []*AgentSession + for _, agent := range agents { + if agent.Type != AgentCrew { + continue + } + // Filter by rig if specified + if crewRig != "" && agent.Rig != crewRig { + continue + } + targets = append(targets, agent) + } + + if len(targets) == 0 { + fmt.Println("No running crew sessions to restart.") + if crewRig != "" { + fmt.Printf(" (filtered by rig: %s)\n", crewRig) + } + return nil + } + + // Dry run - just show what would be restarted + if crewDryRun { + fmt.Printf("Would restart %d crew session(s):\n\n", len(targets)) + for _, agent := range targets { + fmt.Printf(" %s %s/crew/%s\n", AgentTypeIcons[AgentCrew], agent.Rig, agent.AgentName) + } + return nil + } + + fmt.Printf("Restarting %d crew session(s)...\n\n", len(targets)) + + var succeeded, failed int + var failures []string + + for _, agent := range targets { + agentName := fmt.Sprintf("%s/crew/%s", agent.Rig, agent.AgentName) + + // Use crewRig temporarily to get the right crew manager + savedRig := crewRig + crewRig = agent.Rig + + crewMgr, r, err := getCrewManager(crewRig) + if err != nil { + failed++ + failures = append(failures, fmt.Sprintf("%s: %v", agentName, err)) + fmt.Printf(" %s %s\n", style.ErrorPrefix, agentName) + crewRig = savedRig + continue + } + + worker, err := crewMgr.Get(agent.AgentName) + if err != nil { + failed++ + failures = append(failures, fmt.Sprintf("%s: %v", agentName, err)) + fmt.Printf(" %s %s\n", style.ErrorPrefix, agentName) + crewRig = savedRig + continue + } + + // Restart the session + if err := restartCrewSession(r.Name, agent.AgentName, worker.ClonePath); err != nil { + failed++ + failures = append(failures, fmt.Sprintf("%s: %v", agentName, err)) + fmt.Printf(" %s %s\n", style.ErrorPrefix, agentName) + } else { + succeeded++ + fmt.Printf(" %s %s\n", style.SuccessPrefix, agentName) + } + + crewRig = savedRig + + // Small delay between restarts to avoid overwhelming the system + time.Sleep(500 * time.Millisecond) + } + + fmt.Println() + if failed > 0 { + fmt.Printf("%s Restart complete: %d succeeded, %d failed\n", + style.WarningPrefix, succeeded, failed) + for _, f := range failures { + fmt.Printf(" %s\n", style.Dim.Render(f)) + } + return fmt.Errorf("%d restart(s) failed", failed) + } + + fmt.Printf("%s Restart complete: %d crew session(s) restarted\n", style.SuccessPrefix, succeeded) + return nil +} + +// restartCrewSession handles the core restart logic for a single crew session. +func restartCrewSession(rigName, crewName, clonePath string) error { + t := tmux.NewTmux() + sessionID := crewSessionName(rigName, crewName) + + // Kill existing session if running + if hasSession, _ := t.HasSession(sessionID); hasSession { + if err := t.KillSession(sessionID); err != nil { + return fmt.Errorf("killing old session: %w", err) + } + } + + // Start new session + if err := t.NewSession(sessionID, clonePath); err != nil { + return fmt.Errorf("creating session: %w", err) + } + + // Set environment + t.SetEnvironment(sessionID, "GT_ROLE", "crew") + t.SetEnvironment(sessionID, "GT_RIG", rigName) + t.SetEnvironment(sessionID, "GT_CREW", crewName) + + // Apply rig-based theming + theme := getThemeForRig(rigName) + _ = t.ConfigureGasTownSession(sessionID, theme, rigName, crewName, "crew") + + // Wait for shell to be ready + if err := t.WaitForShellReady(sessionID, 5*time.Second); err != nil { + return fmt.Errorf("waiting for shell: %w", err) + } + + // Start claude with skip permissions + bdActor := fmt.Sprintf("%s/crew/%s", rigName, crewName) + claudeCmd := fmt.Sprintf("export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s && claude --dangerously-skip-permissions", rigName, crewName, bdActor) + if err := t.SendKeys(sessionID, claudeCmd); err != nil { + return fmt.Errorf("starting claude: %w", err) + } + + // Wait for Claude to start, then prime it + shells := []string{"bash", "zsh", "sh", "fish", "tcsh", "ksh"} + if err := t.WaitForCommand(sessionID, shells, 15*time.Second); err != nil { + // Non-fatal warning + } + time.Sleep(500 * time.Millisecond) + if err := t.SendKeys(sessionID, "gt prime"); err != nil { + // Non-fatal + } + + // Send crew resume prompt after prime completes + time.Sleep(5 * time.Second) + crewPrompt := "Read your mail, act on anything urgent, else await instructions." + _ = t.NudgeSession(sessionID, crewPrompt) + + return nil +}