From 7919129ff43dc6bd8ec7c979840bc95680587f9c Mon Sep 17 00:00:00 2001 From: dag Date: Fri, 2 Jan 2026 17:19:18 -0800 Subject: [PATCH] feat(rig): Add 'gt rig restart' for multi-rig restart support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 'gt rig restart ...' command that stops then starts witness and refinery for one or more rigs. Supports --force and --nuclear flags like the existing start/stop commands. (gt-s7t1h) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/rig.go | 219 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) diff --git a/internal/cmd/rig.go b/internal/cmd/rig.go index dbd3c4b0..614b4ef6 100644 --- a/internal/cmd/rig.go +++ b/internal/cmd/rig.go @@ -223,6 +223,31 @@ Examples: RunE: runRigStop, } +var rigRestartCmd = &cobra.Command{ + Use: "restart ...", + Short: "Restart one or more rigs (stop then start)", + Long: `Restart the patrol agents (witness and refinery) for one or more rigs. + +This is equivalent to 'gt rig stop' followed by 'gt rig start' for each rig. +Useful after polecats complete work and land their changes. + +Before shutdown, checks all polecats for uncommitted work: +- Uncommitted changes (modified/untracked files) +- Stashes +- Unpushed commits + +Use --force to skip graceful shutdown and kill immediately. +Use --nuclear to bypass ALL safety checks (will lose work!). + +Examples: + gt rig restart gastown + gt rig restart gastown beads + gt rig restart --force gastown beads + gt rig restart --nuclear gastown # DANGER: loses uncommitted work`, + Args: cobra.MinimumNArgs(1), + RunE: runRigRestart, +} + // Flags var ( rigAddPrefix string @@ -235,6 +260,8 @@ var ( rigShutdownNuclear bool rigStopForce bool rigStopNuclear bool + rigRestartForce bool + rigRestartNuclear bool ) func init() { @@ -245,6 +272,7 @@ func init() { rigCmd.AddCommand(rigRebootCmd) rigCmd.AddCommand(rigRemoveCmd) rigCmd.AddCommand(rigResetCmd) + rigCmd.AddCommand(rigRestartCmd) rigCmd.AddCommand(rigShutdownCmd) rigCmd.AddCommand(rigStartCmd) rigCmd.AddCommand(rigStatusCmd) @@ -265,6 +293,9 @@ func init() { rigStopCmd.Flags().BoolVarP(&rigStopForce, "force", "f", false, "Force immediate shutdown") rigStopCmd.Flags().BoolVar(&rigStopNuclear, "nuclear", false, "DANGER: Bypass ALL safety checks (loses uncommitted work!)") + + rigRestartCmd.Flags().BoolVarP(&rigRestartForce, "force", "f", false, "Force immediate shutdown during restart") + rigRestartCmd.Flags().BoolVar(&rigRestartNuclear, "nuclear", false, "DANGER: Bypass ALL safety checks (loses uncommitted work!)") } func runRigAdd(cmd *cobra.Command, args []string) error { @@ -1203,3 +1234,191 @@ func runRigStop(cmd *cobra.Command, args []string) error { return nil } + +func runRigRestart(cmd *cobra.Command, args []string) error { + // Find workspace + townRoot, err := workspace.FindFromCwdOrError() + if err != nil { + return fmt.Errorf("not in a Gas Town workspace: %w", err) + } + + // Load rigs config + rigsPath := filepath.Join(townRoot, "mayor", "rigs.json") + rigsConfig, err := config.LoadRigsConfig(rigsPath) + if err != nil { + rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)} + } + + g := git.NewGit(townRoot) + rigMgr := rig.NewManager(townRoot, rigsConfig, g) + t := tmux.NewTmux() + + // Track results + var succeeded []string + var failed []string + + // Process each rig + for _, rigName := range args { + r, err := rigMgr.GetRig(rigName) + if err != nil { + fmt.Printf("%s Rig '%s' not found\n", style.Warning.Render("⚠"), rigName) + failed = append(failed, rigName) + continue + } + + fmt.Printf("Restarting rig %s...\n", style.Bold.Render(rigName)) + + // Check all polecats for uncommitted work (unless nuclear) + if !rigRestartNuclear { + polecatGit := git.NewGit(r.Path) + polecatMgr := polecat.NewManager(r, polecatGit) + polecats, err := polecatMgr.List() + if err == nil && len(polecats) > 0 { + var problemPolecats []struct { + name string + status *git.UncommittedWorkStatus + } + + for _, p := range polecats { + pGit := git.NewGit(p.ClonePath) + status, err := pGit.CheckUncommittedWork() + if err == nil && !status.Clean() { + problemPolecats = append(problemPolecats, struct { + name string + status *git.UncommittedWorkStatus + }{p.Name, status}) + } + } + + if len(problemPolecats) > 0 { + fmt.Printf("\n%s Cannot restart %s - polecats have uncommitted work:\n", style.Warning.Render("⚠"), rigName) + for _, pp := range problemPolecats { + fmt.Printf(" %s: %s\n", style.Bold.Render(pp.name), pp.status.String()) + } + failed = append(failed, rigName) + continue + } + } + } + + var stopErrors []string + var startErrors []string + + // === STOP PHASE === + fmt.Printf(" Stopping...\n") + + // 1. Stop all polecat sessions + sessMgr := session.NewManager(t, r) + infos, err := sessMgr.List() + if err == nil && len(infos) > 0 { + fmt.Printf(" Stopping %d polecat session(s)...\n", len(infos)) + if err := sessMgr.StopAll(rigRestartForce); err != nil { + stopErrors = append(stopErrors, fmt.Sprintf("polecat sessions: %v", err)) + } + } + + // 2. Stop the refinery + refMgr := refinery.NewManager(r) + refStatus, err := refMgr.Status() + if err == nil && refStatus.State == refinery.StateRunning { + fmt.Printf(" Stopping refinery...\n") + if err := refMgr.Stop(); err != nil { + stopErrors = append(stopErrors, fmt.Sprintf("refinery: %v", err)) + } + } + + // 3. Stop the witness + witMgr := witness.NewManager(r) + witStatus, err := witMgr.Status() + if err == nil && witStatus.State == witness.StateRunning { + fmt.Printf(" Stopping witness...\n") + if err := witMgr.Stop(); err != nil { + stopErrors = append(stopErrors, fmt.Sprintf("witness: %v", err)) + } + } + + if len(stopErrors) > 0 { + fmt.Printf(" %s Stop errors:\n", style.Warning.Render("⚠")) + for _, e := range stopErrors { + fmt.Printf(" - %s\n", e) + } + failed = append(failed, rigName) + continue + } + + // === START PHASE === + fmt.Printf(" Starting...\n") + + var started []string + var skipped []string + + // 1. Start the witness + witnessSession := fmt.Sprintf("gt-%s-witness", rigName) + witnessRunning, _ := t.HasSession(witnessSession) + if witnessRunning { + skipped = append(skipped, "witness") + } else { + fmt.Printf(" Starting witness...\n") + created, err := ensureWitnessSession(rigName, r) + if err != nil { + fmt.Printf(" %s Failed to start witness: %v\n", style.Warning.Render("⚠"), err) + startErrors = append(startErrors, fmt.Sprintf("witness: %v", err)) + } else if created { + _ = witMgr.Start() + started = append(started, "witness") + } + } + + // 2. Start the refinery + refinerySession := fmt.Sprintf("gt-%s-refinery", rigName) + refineryRunning, _ := t.HasSession(refinerySession) + if refineryRunning { + skipped = append(skipped, "refinery") + } else { + fmt.Printf(" Starting refinery...\n") + if err := refMgr.Start(false); err != nil { + fmt.Printf(" %s Failed to start refinery: %v\n", style.Warning.Render("⚠"), err) + startErrors = append(startErrors, fmt.Sprintf("refinery: %v", err)) + } else { + started = append(started, "refinery") + } + } + + // Report results for this rig + if len(started) > 0 { + fmt.Printf(" %s Started: %s\n", style.Success.Render("✓"), strings.Join(started, ", ")) + } + if len(skipped) > 0 { + fmt.Printf(" %s Skipped: %s (already running)\n", style.Dim.Render("•"), strings.Join(skipped, ", ")) + } + + if len(startErrors) > 0 { + fmt.Printf(" %s Start errors:\n", style.Warning.Render("⚠")) + for _, e := range startErrors { + fmt.Printf(" - %s\n", e) + } + failed = append(failed, rigName) + } else { + fmt.Printf("%s Rig %s restarted\n", style.Success.Render("✓"), rigName) + succeeded = append(succeeded, rigName) + } + fmt.Println() + } + + // Summary + if len(args) > 1 { + if len(succeeded) > 0 { + fmt.Printf("%s Restarted: %s\n", style.Success.Render("✓"), strings.Join(succeeded, ", ")) + } + if len(failed) > 0 { + fmt.Printf("%s Failed: %s\n", style.Warning.Render("⚠"), strings.Join(failed, ", ")) + fmt.Printf("\nUse %s to force shutdown (DANGER: will lose work!)\n", style.Bold.Render("--nuclear")) + return fmt.Errorf("some rigs failed to restart") + } + } else if len(failed) > 0 { + fmt.Printf("\nUse %s to force shutdown (DANGER: will lose work!)\n", style.Bold.Render("--nuclear")) + return fmt.Errorf("rig failed to restart") + } + + return nil +}