package cmd import ( "errors" "fmt" "time" "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/tmux" "github.com/steveyegge/gastown/internal/workspace" ) // MayorSessionName is the tmux session name for the Mayor. const MayorSessionName = "gt-mayor" var mayorCmd = &cobra.Command{ Use: "mayor", Aliases: []string{"may"}, GroupID: GroupAgents, Short: "Manage the Mayor session", Long: `Manage the Mayor tmux session. The Mayor is the global coordinator for Gas Town, running as a persistent tmux session. Use the subcommands to start, stop, attach, and check status.`, } var mayorStartCmd = &cobra.Command{ Use: "start", Short: "Start the Mayor session", Long: `Start the Mayor tmux session. Creates a new detached tmux session for the Mayor and launches Claude. The session runs in the workspace root directory.`, RunE: runMayorStart, } var mayorStopCmd = &cobra.Command{ Use: "stop", Short: "Stop the Mayor session", Long: `Stop the Mayor tmux session. Attempts graceful shutdown first (Ctrl-C), then kills the tmux session.`, RunE: runMayorStop, } var mayorAttachCmd = &cobra.Command{ Use: "attach", Aliases: []string{"at"}, Short: "Attach to the Mayor session", Long: `Attach to the running Mayor tmux session. Attaches the current terminal to the Mayor's tmux session. Detach with Ctrl-B D.`, RunE: runMayorAttach, } var mayorStatusCmd = &cobra.Command{ Use: "status", Short: "Check Mayor session status", Long: `Check if the Mayor tmux session is currently running.`, RunE: runMayorStatus, } var mayorRestartCmd = &cobra.Command{ Use: "restart", Short: "Restart the Mayor session", Long: `Restart the Mayor tmux session. Stops the current session (if running) and starts a fresh one.`, RunE: runMayorRestart, } func init() { mayorCmd.AddCommand(mayorStartCmd) mayorCmd.AddCommand(mayorStopCmd) mayorCmd.AddCommand(mayorAttachCmd) mayorCmd.AddCommand(mayorStatusCmd) mayorCmd.AddCommand(mayorRestartCmd) rootCmd.AddCommand(mayorCmd) } func runMayorStart(cmd *cobra.Command, args []string) error { t := tmux.NewTmux() // Check if session already exists running, err := t.HasSession(MayorSessionName) if err != nil { return fmt.Errorf("checking session: %w", err) } if running { return fmt.Errorf("Mayor session already running. Attach with: gt mayor attach") } if err := startMayorSession(t); err != nil { return err } fmt.Printf("%s Mayor session started. Attach with: %s\n", style.Bold.Render("✓"), style.Dim.Render("gt mayor attach")) return nil } // startMayorSession creates and initializes the Mayor tmux session. func startMayorSession(t *tmux.Tmux) error { // Find workspace root townRoot, err := workspace.FindFromCwdOrError() if err != nil { return fmt.Errorf("not in a Gas Town workspace: %w", err) } // Create session in workspace root fmt.Println("Starting Mayor session...") if err := t.NewSession(MayorSessionName, townRoot); err != nil { return fmt.Errorf("creating session: %w", err) } // Set environment _ = t.SetEnvironment(MayorSessionName, "GT_ROLE", "mayor") // Apply Mayor theme theme := tmux.MayorTheme() _ = t.ConfigureGasTownSession(MayorSessionName, theme, "", "Mayor", "coordinator") // Launch Claude - the startup hook handles 'gt prime' automatically // Use SendKeysDelayed to allow shell initialization after NewSession // Export GT_ROLE in the command since tmux SetEnvironment only affects new panes claudeCmd := `export GT_ROLE=mayor && claude --dangerously-skip-permissions` if err := t.SendKeysDelayed(MayorSessionName, claudeCmd, 200); err != nil { return fmt.Errorf("sending command: %w", err) } return nil } func runMayorStop(cmd *cobra.Command, args []string) error { t := tmux.NewTmux() // Check if session exists running, err := t.HasSession(MayorSessionName) if err != nil { return fmt.Errorf("checking session: %w", err) } if !running { return errors.New("Mayor session is not running") } fmt.Println("Stopping Mayor session...") // Try graceful shutdown first _ = t.SendKeysRaw(MayorSessionName, "C-c") time.Sleep(100 * time.Millisecond) // Kill the session if err := t.KillSession(MayorSessionName); err != nil { return fmt.Errorf("killing session: %w", err) } fmt.Printf("%s Mayor session stopped.\n", style.Bold.Render("✓")) return nil } func runMayorAttach(cmd *cobra.Command, args []string) error { t := tmux.NewTmux() // Check if session exists running, err := t.HasSession(MayorSessionName) if err != nil { return fmt.Errorf("checking session: %w", err) } if !running { // Auto-start if not running fmt.Println("Mayor session not running, starting...") if err := startMayorSession(t); err != nil { return err } } // Use shared attach helper (smart: links if inside tmux, attaches if outside) return attachToTmuxSession(MayorSessionName) } func runMayorStatus(cmd *cobra.Command, args []string) error { t := tmux.NewTmux() running, err := t.HasSession(MayorSessionName) if err != nil { return fmt.Errorf("checking session: %w", err) } if running { // Get session info for more details info, err := t.GetSessionInfo(MayorSessionName) if err == nil { status := "detached" if info.Attached { status = "attached" } fmt.Printf("%s Mayor session is %s\n", style.Bold.Render("●"), style.Bold.Render("running")) fmt.Printf(" Status: %s\n", status) fmt.Printf(" Created: %s\n", info.Created) fmt.Printf("\nAttach with: %s\n", style.Dim.Render("gt mayor attach")) } else { fmt.Printf("%s Mayor session is %s\n", style.Bold.Render("●"), style.Bold.Render("running")) } } else { fmt.Printf("%s Mayor session is %s\n", style.Dim.Render("○"), "not running") fmt.Printf("\nStart with: %s\n", style.Dim.Render("gt mayor start")) } return nil } func runMayorRestart(cmd *cobra.Command, args []string) error { t := tmux.NewTmux() running, err := t.HasSession(MayorSessionName) if err != nil { return fmt.Errorf("checking session: %w", err) } if running { // Stop the current session fmt.Println("Stopping Mayor session...") _ = t.SendKeysRaw(MayorSessionName, "C-c") time.Sleep(100 * time.Millisecond) if err := t.KillSession(MayorSessionName); err != nil { return fmt.Errorf("killing session: %w", err) } } // Start fresh return runMayorStart(cmd, args) }