Files
gastown/internal/cmd/mayor.go
Steve Yegge b172fd4787 feat: Add rig infra cycling (witness ↔ refinery)
Extended the unified cycle system to include rig infrastructure sessions:
- Witness ↔ Refinery (per rig) now cycle with C-b n/p

Also moved SetCycleBindings into ConfigureGasTownSession so ALL Gas Town
sessions automatically get the unified cycle bindings. Removed redundant
individual calls from crew, mayor, and deacon startup code.

Cycle groups are now:
- Town: Mayor ↔ Deacon
- Crew (per rig): All crew members in same rig
- Infra (per rig): Witness ↔ Refinery

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:36:38 -08:00

246 lines
6.6 KiB
Go

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 (non-fatal: session works without these)
_ = t.SetEnvironment(MayorSessionName, "GT_ROLE", "mayor")
_ = t.SetEnvironment(MayorSessionName, "BD_ACTOR", "mayor")
// Apply Mayor theme (non-fatal: theming failure doesn't affect operation)
// Note: ConfigureGasTownSession includes cycle bindings
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 and BD_ACTOR in the command since tmux SetEnvironment only affects new panes
claudeCmd := `export GT_ROLE=mayor BD_ACTOR=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 (best-effort interrupt)
_ = 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 (best-effort interrupt before kill)
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)
}