From a5a03bb9ca082ea5fdefbdea6a81001c726323d5 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Wed, 17 Dec 2025 20:37:12 -0800 Subject: [PATCH] feat: gt mayor at auto-starts and restarts Claude if needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auto-start Mayor session if not running (no need for gt mayor start first) - Restart Claude if it has exited (detects shell in pane) - Prime with gt prime after start/restart - Refactor: extract startMayorSession helper 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/mayor.go | 67 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/internal/cmd/mayor.go b/internal/cmd/mayor.go index bf85f9df..26539d15 100644 --- a/internal/cmd/mayor.go +++ b/internal/cmd/mayor.go @@ -82,12 +82,6 @@ func init() { } func runMayorStart(cmd *cobra.Command, args []string) error { - // Find workspace root - townRoot, err := workspace.FindFromCwdOrError() - if err != nil { - return fmt.Errorf("not in a Gas Town workspace: %w", err) - } - t := tmux.NewTmux() // Check if session already exists @@ -99,6 +93,25 @@ func runMayorStart(cmd *cobra.Command, args []string) error { 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 { @@ -109,14 +122,14 @@ func runMayorStart(cmd *cobra.Command, args []string) error { t.SetEnvironment(MayorSessionName, "GT_ROLE", "mayor") // Launch Claude with full permissions (Mayor is trusted) - command := "claude --dangerously-skip-permissions" - if err := t.SendKeys(MayorSessionName, command); err != nil { + if err := t.SendKeys(MayorSessionName, "claude --dangerously-skip-permissions"); err != nil { return fmt.Errorf("sending command: %w", err) } - fmt.Printf("%s Mayor session started. Attach with: %s\n", - style.Bold.Render("✓"), - style.Dim.Render("gt mayor attach")) + // Prime after a delay + if err := t.SendKeysDelayed(MayorSessionName, "gt prime", 2000); err != nil { + fmt.Printf("Warning: Could not send prime command: %v\n", err) + } return nil } @@ -157,11 +170,28 @@ func runMayorAttach(cmd *cobra.Command, args []string) error { return fmt.Errorf("checking session: %w", err) } if !running { - return errors.New("Mayor session is not running. Start with: gt mayor start") + // Auto-start if not running + fmt.Println("Mayor session not running, starting...") + if err := startMayorSession(t); err != nil { + return err + } + } else { + // Session exists - check if Claude is still running + paneCmd, err := t.GetPaneCommand(MayorSessionName) + if err == nil && isMayorShellCommand(paneCmd) { + // Claude has exited, restart it + fmt.Println("Claude exited, restarting...") + if err := t.SendKeys(MayorSessionName, "claude --dangerously-skip-permissions"); err != nil { + return fmt.Errorf("restarting claude: %w", err) + } + // Prime after restart + if err := t.SendKeysDelayed(MayorSessionName, "gt prime", 2000); err != nil { + fmt.Printf("Warning: Could not send prime command: %v\n", err) + } + } } // Use exec to replace current process with tmux attach - // This is the standard pattern for attaching to tmux sessions tmuxPath, err := exec.LookPath("tmux") if err != nil { return fmt.Errorf("tmux not found: %w", err) @@ -170,6 +200,17 @@ func runMayorAttach(cmd *cobra.Command, args []string) error { return execCommand(tmuxPath, "attach-session", "-t", MayorSessionName) } +// isMayorShellCommand checks if the command is a shell (meaning Claude has exited). +func isMayorShellCommand(cmd string) bool { + shells := []string{"bash", "zsh", "sh", "fish", "tcsh", "ksh"} + for _, shell := range shells { + if cmd == shell { + return true + } + } + return false +} + // execCommand replaces the current process with the given command. // This is used for attaching to tmux sessions. func execCommand(name string, args ...string) error {