package cmd import ( "fmt" "os" "os/exec" "path/filepath" "strings" "time" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/crew" "github.com/steveyegge/gastown/internal/mail" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/tmux" ) func runCrewRemove(cmd *cobra.Command, args []string) error { name := args[0] // Parse rig/name format (e.g., "beads/emma" -> rig=beads, name=emma) if rig, crewName, ok := parseRigSlashName(name); ok { if crewRig == "" { crewRig = rig } name = crewName } crewMgr, r, err := getCrewManager(crewRig) if err != nil { return err } // Check for running session (unless forced) if !crewForce { t := tmux.NewTmux() sessionID := crewSessionName(r.Name, name) hasSession, _ := t.HasSession(sessionID) if hasSession { return fmt.Errorf("session '%s' is running (use --force to kill and remove)", sessionID) } } // Kill session if it exists t := tmux.NewTmux() sessionID := crewSessionName(r.Name, name) if hasSession, _ := t.HasSession(sessionID); hasSession { if err := t.KillSession(sessionID); err != nil { return fmt.Errorf("killing session: %w", err) } fmt.Printf("Killed session %s\n", sessionID) } // Remove the crew workspace if err := crewMgr.Remove(name, crewForce); err != nil { if err == crew.ErrCrewNotFound { return fmt.Errorf("crew workspace '%s' not found", name) } if err == crew.ErrHasChanges { return fmt.Errorf("crew workspace has uncommitted changes (use --force to remove anyway)") } return fmt.Errorf("removing crew workspace: %w", err) } fmt.Printf("%s Removed crew workspace: %s/%s\n", style.Bold.Render("✓"), r.Name, name) // Close the agent bead if it exists // Format: gt--crew- (matches session name format) agentBeadID := fmt.Sprintf("gt-%s-crew-%s", r.Name, name) closeCmd := exec.Command("bd", "close", agentBeadID, "--reason=Crew workspace removed") closeCmd.Dir = r.Path // Run from rig directory for proper beads resolution if output, err := closeCmd.CombinedOutput(); err != nil { // Non-fatal: bead might not exist or already be closed if !strings.Contains(string(output), "no issue found") && !strings.Contains(string(output), "already closed") { style.PrintWarning("could not close agent bead %s: %v", agentBeadID, err) } } else { fmt.Printf("Closed agent bead: %s\n", agentBeadID) } return nil } func runCrewRefresh(cmd *cobra.Command, args []string) error { name := args[0] // Parse rig/name format (e.g., "beads/emma" -> rig=beads, name=emma) if rig, crewName, ok := parseRigSlashName(name); ok { if crewRig == "" { crewRig = rig } name = crewName } crewMgr, r, err := getCrewManager(crewRig) if err != nil { return err } // Get the crew worker worker, err := crewMgr.Get(name) if err != nil { if err == crew.ErrCrewNotFound { return fmt.Errorf("crew workspace '%s' not found", name) } return fmt.Errorf("getting crew worker: %w", err) } t := tmux.NewTmux() sessionID := crewSessionName(r.Name, name) // Check if session exists hasSession, _ := t.HasSession(sessionID) // Create handoff message handoffMsg := crewMessage if handoffMsg == "" { handoffMsg = fmt.Sprintf("Context refresh for %s. Check mail and beads for current work state.", name) } // Send handoff mail to self mailDir := filepath.Join(worker.ClonePath, "mail") if _, err := os.Stat(mailDir); os.IsNotExist(err) { if err := os.MkdirAll(mailDir, 0755); err != nil { return fmt.Errorf("creating mail dir: %w", err) } } // Create and send mail mailbox := mail.NewMailbox(mailDir) msg := &mail.Message{ From: fmt.Sprintf("%s/%s", r.Name, name), To: fmt.Sprintf("%s/%s", r.Name, name), Subject: "🤝 HANDOFF: Context Refresh", Body: handoffMsg, } if err := mailbox.Append(msg); err != nil { return fmt.Errorf("sending handoff mail: %w", err) } fmt.Printf("Sent handoff mail to %s/%s\n", r.Name, name) // Kill existing session if running if hasSession { if err := t.KillSession(sessionID); err != nil { return fmt.Errorf("killing old session: %w", err) } fmt.Printf("Killed old session %s\n", sessionID) } // Start new session if err := t.NewSession(sessionID, worker.ClonePath); err != nil { return fmt.Errorf("creating session: %w", err) } // Set environment (non-fatal: session works without these) _ = t.SetEnvironment(sessionID, "GT_RIG", r.Name) _ = t.SetEnvironment(sessionID, "GT_CREW", name) // Wait for shell to be ready if err := t.WaitForShellReady(sessionID, constants.ShellReadyTimeout); err != nil { return fmt.Errorf("waiting for shell: %w", err) } // Start claude (refresh uses regular permissions, reads handoff mail) if err := t.SendKeys(sessionID, "claude"); err != nil { return fmt.Errorf("starting claude: %w", err) } fmt.Printf("%s Refreshed crew workspace: %s/%s\n", style.Bold.Render("✓"), r.Name, name) fmt.Printf("Attach with: %s\n", style.Dim.Render(fmt.Sprintf("gt crew at %s", name))) return nil } // runCrewStart is an alias for runStartCrew, handling multiple input formats. // It supports: "name", "rig/name", "rig/crew/name" formats, or auto-detection from cwd. func runCrewStart(cmd *cobra.Command, args []string) error { var name string // Determine crew name: from arg, or auto-detect from cwd if len(args) > 0 { name = args[0] // Handle rig/crew/name format (e.g., "gastown/crew/joe" -> "gastown/joe") if strings.Contains(name, "/crew/") { parts := strings.SplitN(name, "/crew/", 2) if len(parts) == 2 && parts[0] != "" && parts[1] != "" { name = parts[0] + "/" + parts[1] } } } else { // Try to detect from current directory detected, err := detectCrewFromCwd() if err != nil { return fmt.Errorf("could not detect crew workspace from current directory: %w\n\nUsage: gt crew start ", err) } name = detected.crewName if crewRig == "" { crewRig = detected.rigName } fmt.Printf("Detected crew workspace: %s/%s\n", detected.rigName, name) } // Set the start.go flags from crew.go flags before calling startCrewRig = crewRig startCrewAccount = crewAccount return runStartCrew(cmd, []string{name}) } 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 { if crewRig == "" { crewRig = rig } name = crewName } crewMgr, r, err := getCrewManager(crewRig) if err != nil { return err } // Get the crew worker, create if not exists (idempotent) worker, err := crewMgr.Get(name) if err == crew.ErrCrewNotFound { fmt.Printf("Creating crew workspace %s in %s...\n", name, r.Name) worker, err = crewMgr.Add(name, false) // No feature branch for crew if err != nil { return fmt.Errorf("creating crew workspace: %w", err) } fmt.Printf("Created crew workspace: %s/%s\n", r.Name, name) } else if err != nil { return fmt.Errorf("getting crew worker: %w", err) } t := tmux.NewTmux() sessionID := crewSessionName(r.Name, name) // 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) } fmt.Printf("Killed session %s\n", sessionID) } // Start new session if err := t.NewSession(sessionID, worker.ClonePath); err != nil { return fmt.Errorf("creating session: %w", err) } // Set environment t.SetEnvironment(sessionID, "GT_ROLE", "crew") t.SetEnvironment(sessionID, "GT_RIG", r.Name) t.SetEnvironment(sessionID, "GT_CREW", name) // Apply rig-based theming (non-fatal: theming failure doesn't affect operation) theme := getThemeForRig(r.Name) _ = t.ConfigureGasTownSession(sessionID, theme, r.Name, name, "crew") // Wait for shell to be ready if err := t.WaitForShellReady(sessionID, constants.ShellReadyTimeout); err != nil { return fmt.Errorf("waiting for shell: %w", err) } // Start claude with skip permissions (crew workers are trusted) // Export GT_ROLE and BD_ACTOR since tmux SetEnvironment only affects new panes bdActor := fmt.Sprintf("%s/crew/%s", r.Name, name) claudeCmd := fmt.Sprintf("export GT_ROLE=crew GT_RIG=%s GT_CREW=%s BD_ACTOR=%s && claude --dangerously-skip-permissions", r.Name, name, 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 := constants.SupportedShells if err := t.WaitForCommand(sessionID, shells, constants.ClaudeStartTimeout); err != nil { style.PrintWarning("Timeout waiting for Claude to start: %v", err) } // Give Claude time to initialize after process starts time.Sleep(constants.ShutdownNotifyDelay) if err := t.SendKeys(sessionID, "gt prime"); err != nil { // Non-fatal: Claude started but priming failed style.PrintWarning("Could not send prime command: %v", err) } // Send crew resume prompt after prime completes // Use NudgeSession (the canonical way to message Claude) with longer pre-delay // to ensure gt prime has finished processing time.Sleep(5 * time.Second) crewPrompt := "Read your mail, act on anything urgent, else await instructions." if err := t.NudgeSession(sessionID, crewPrompt); err != nil { style.PrintWarning("Could not send resume prompt: %v", err) } fmt.Printf("%s Restarted crew workspace: %s/%s\n", style.Bold.Render("✓"), r.Name, name) fmt.Printf("Attach with: %s\n", style.Dim.Render(fmt.Sprintf("gt crew at %s", name))) 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(constants.ShutdownNotifyDelay) } 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, constants.ShellReadyTimeout); 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 := constants.SupportedShells if err := t.WaitForCommand(sessionID, shells, constants.ClaudeStartTimeout); err != nil { // Non-fatal warning } time.Sleep(constants.ShutdownNotifyDelay) 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 }