feat(rig): Add gt rig stop command for multi-rig shutdown
Add new `gt rig stop <rig>...` command that supports stopping multiple rigs with the same shutdown semantics as `gt rig shutdown`: - Stops all polecat sessions - Stops the refinery (if running) - Stops the witness (if running) - Checks for uncommitted work before shutdown (unless --nuclear) Includes --force flag for immediate shutdown and --nuclear flag to bypass safety checks. (gt-lhitf) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -195,6 +195,34 @@ Examples:
|
|||||||
RunE: runRigStatus,
|
RunE: runRigStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rigStopCmd = &cobra.Command{
|
||||||
|
Use: "stop <rig>...",
|
||||||
|
Short: "Stop one or more rigs (shutdown semantics)",
|
||||||
|
Long: `Stop all agents in one or more rigs.
|
||||||
|
|
||||||
|
This command is similar to 'gt rig shutdown' but supports multiple rigs.
|
||||||
|
For each rig, it gracefully shuts down:
|
||||||
|
- All polecat sessions
|
||||||
|
- The refinery (if running)
|
||||||
|
- The witness (if running)
|
||||||
|
|
||||||
|
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 stop gastown
|
||||||
|
gt rig stop gastown beads
|
||||||
|
gt rig stop --force gastown beads
|
||||||
|
gt rig stop --nuclear gastown # DANGER: loses uncommitted work`,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: runRigStop,
|
||||||
|
}
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
var (
|
var (
|
||||||
rigAddPrefix string
|
rigAddPrefix string
|
||||||
@@ -205,6 +233,8 @@ var (
|
|||||||
rigResetRole string
|
rigResetRole string
|
||||||
rigShutdownForce bool
|
rigShutdownForce bool
|
||||||
rigShutdownNuclear bool
|
rigShutdownNuclear bool
|
||||||
|
rigStopForce bool
|
||||||
|
rigStopNuclear bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -218,6 +248,7 @@ func init() {
|
|||||||
rigCmd.AddCommand(rigShutdownCmd)
|
rigCmd.AddCommand(rigShutdownCmd)
|
||||||
rigCmd.AddCommand(rigStartCmd)
|
rigCmd.AddCommand(rigStartCmd)
|
||||||
rigCmd.AddCommand(rigStatusCmd)
|
rigCmd.AddCommand(rigStatusCmd)
|
||||||
|
rigCmd.AddCommand(rigStopCmd)
|
||||||
|
|
||||||
rigAddCmd.Flags().StringVar(&rigAddPrefix, "prefix", "", "Beads issue prefix (default: derived from name)")
|
rigAddCmd.Flags().StringVar(&rigAddPrefix, "prefix", "", "Beads issue prefix (default: derived from name)")
|
||||||
|
|
||||||
@@ -231,6 +262,9 @@ func init() {
|
|||||||
rigShutdownCmd.Flags().BoolVar(&rigShutdownNuclear, "nuclear", false, "DANGER: Bypass ALL safety checks (loses uncommitted work!)")
|
rigShutdownCmd.Flags().BoolVar(&rigShutdownNuclear, "nuclear", false, "DANGER: Bypass ALL safety checks (loses uncommitted work!)")
|
||||||
|
|
||||||
rigRebootCmd.Flags().BoolVarP(&rigShutdownForce, "force", "f", false, "Force immediate shutdown during reboot")
|
rigRebootCmd.Flags().BoolVarP(&rigShutdownForce, "force", "f", false, "Force immediate shutdown during reboot")
|
||||||
|
|
||||||
|
rigStopCmd.Flags().BoolVarP(&rigStopForce, "force", "f", false, "Force immediate shutdown")
|
||||||
|
rigStopCmd.Flags().BoolVar(&rigStopNuclear, "nuclear", false, "DANGER: Bypass ALL safety checks (loses uncommitted work!)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRigAdd(cmd *cobra.Command, args []string) error {
|
func runRigAdd(cmd *cobra.Command, args []string) error {
|
||||||
@@ -1029,3 +1063,132 @@ func runRigStatus(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runRigStop(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)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all polecats for uncommitted work (unless nuclear)
|
||||||
|
if !rigStopNuclear {
|
||||||
|
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 stop %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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Stopping rig %s...\n", style.Bold.Render(rigName))
|
||||||
|
|
||||||
|
var errors []string
|
||||||
|
|
||||||
|
// 1. Stop all polecat sessions
|
||||||
|
t := tmux.NewTmux()
|
||||||
|
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(rigStopForce); err != nil {
|
||||||
|
errors = append(errors, 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 {
|
||||||
|
errors = append(errors, 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 {
|
||||||
|
errors = append(errors, fmt.Sprintf("witness: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
fmt.Printf("%s Some agents in %s failed to stop:\n", style.Warning.Render("⚠"), rigName)
|
||||||
|
for _, e := range errors {
|
||||||
|
fmt.Printf(" - %s\n", e)
|
||||||
|
}
|
||||||
|
failed = append(failed, rigName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s Rig %s stopped\n", style.Success.Render("✓"), rigName)
|
||||||
|
succeeded = append(succeeded, rigName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
if len(args) > 1 {
|
||||||
|
fmt.Println()
|
||||||
|
if len(succeeded) > 0 {
|
||||||
|
fmt.Printf("%s Stopped: %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 stop")
|
||||||
|
}
|
||||||
|
} 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 stop")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user