feat(down): add --polecats flag and deprecate gt stop command

Issue #336: Consolidate down/shutdown/stop commands

Changes:
- Add `gt down --polecats` flag to stop all polecat sessions
- Deprecate `gt stop` command (prints warning, directs to `gt down --polecats`)
- Update help text to clarify down vs shutdown distinction:
  - down = pause (reversible, keeps worktrees)
  - shutdown = done (permanent cleanup)
- Integrate --polecats with new --dry-run mode from recent PR

Note: The issue proposed renaming --nuke to --tmux, but PR #330 just
landed with --nuke having better safety (GT_NUKE_ACKNOWLEDGED env var),
so keeping --nuke as-is. The new --polecats flag absorbs gt stop
functionality as proposed.

Closes #336

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/crew/max
2026-01-10 23:04:37 -08:00
committed by Steve Yegge
parent 3246c7c6b7
commit dab619b3d0
3 changed files with 120 additions and 25 deletions

View File

@@ -12,8 +12,12 @@ import (
"github.com/gofrs/flock"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/beads"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/daemon"
"github.com/steveyegge/gastown/internal/events"
"github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/polecat"
"github.com/steveyegge/gastown/internal/rig"
"github.com/steveyegge/gastown/internal/session"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux"
@@ -29,10 +33,15 @@ var downCmd = &cobra.Command{
Use: "down",
GroupID: GroupServices,
Short: "Stop all Gas Town services",
Long: `Stop all Gas Town long-lived services.
Long: `Stop Gas Town services (reversible pause).
This gracefully shuts down all infrastructure agents:
Shutdown levels (progressively more aggressive):
gt down Stop infrastructure (default)
gt down --polecats Also stop all polecat sessions
gt down --all Also stop bd daemons/activity
gt down --nuke Also kill the tmux server (DESTRUCTIVE)
Infrastructure agents stopped:
• Refineries - Per-rig work processors
• Witnesses - Per-rig polecat managers
• Mayor - Global work coordinator
@@ -40,28 +49,29 @@ This gracefully shuts down all infrastructure agents:
• Deacon - Health orchestrator
• Daemon - Go background process
With --all, also stops resurrection layer (bd daemon/activity) and verifies
shutdown. Polecats are NOT stopped - use 'gt swarm stop' for that.
This is a "pause" operation - use 'gt start' to bring everything back up.
For permanent cleanup (removing worktrees), use 'gt shutdown' instead.
Flags:
--all Stop bd daemons/activity, verify complete shutdown
--nuke Kill entire tmux server (DESTRUCTIVE!)
--dry-run Preview what would be stopped
--force Skip graceful shutdown, use SIGKILL`,
Use cases:
• Taking a break (stop token consumption)
• Clean shutdown before system maintenance
• Resetting the town to a clean state`,
RunE: runDown,
}
var (
downQuiet bool
downForce bool
downAll bool
downNuke bool
downDryRun bool
downQuiet bool
downForce bool
downAll bool
downNuke bool
downDryRun bool
downPolecats bool
)
func init() {
downCmd.Flags().BoolVarP(&downQuiet, "quiet", "q", false, "Only show errors")
downCmd.Flags().BoolVarP(&downForce, "force", "f", false, "Force kill without graceful shutdown")
downCmd.Flags().BoolVarP(&downPolecats, "polecats", "p", false, "Also stop all polecat sessions")
downCmd.Flags().BoolVarP(&downAll, "all", "a", false, "Stop bd daemons/activity and verify shutdown")
downCmd.Flags().BoolVar(&downNuke, "nuke", false, "Kill entire tmux server (DESTRUCTIVE - kills non-GT sessions!)")
downCmd.Flags().BoolVar(&downDryRun, "dry-run", false, "Preview what would be stopped without taking action")
@@ -94,6 +104,32 @@ func runDown(cmd *cobra.Command, args []string) error {
fmt.Println()
}
rigs := discoverRigs(townRoot)
// Phase 0.5: Stop polecats if --polecats
if downPolecats {
if downDryRun {
fmt.Println("Would stop polecats...")
} else {
fmt.Println("Stopping polecats...")
}
polecatsStopped := stopAllPolecats(t, townRoot, rigs, downForce, downDryRun)
if downDryRun {
if polecatsStopped > 0 {
printDownStatus("Polecats", true, fmt.Sprintf("%d would stop", polecatsStopped))
} else {
printDownStatus("Polecats", true, "none running")
}
} else {
if polecatsStopped > 0 {
printDownStatus("Polecats", true, fmt.Sprintf("%d stopped", polecatsStopped))
} else {
printDownStatus("Polecats", true, "none running")
}
}
fmt.Println()
}
// Phase 1: Stop bd resurrection layer (--all only)
if downAll {
daemonsKilled, activityKilled, err := beads.StopAllBdProcesses(downDryRun, downForce)
@@ -122,8 +158,6 @@ func runDown(cmd *cobra.Command, args []string) error {
}
}
rigs := discoverRigs(townRoot)
// Phase 2a: Stop refineries
for _, rigName := range rigs {
sessionName := fmt.Sprintf("gt-%s-refinery", rigName)
@@ -261,6 +295,9 @@ func runDown(cmd *cobra.Command, args []string) error {
stoppedServices = append(stoppedServices, fmt.Sprintf("%s/refinery", rigName))
stoppedServices = append(stoppedServices, fmt.Sprintf("%s/witness", rigName))
}
if downPolecats {
stoppedServices = append(stoppedServices, "polecats")
}
if downAll {
stoppedServices = append(stoppedServices, "bd-processes")
}
@@ -276,6 +313,52 @@ func runDown(cmd *cobra.Command, args []string) error {
return nil
}
// stopAllPolecats stops all polecat sessions across all rigs.
// Returns the number of polecats stopped (or would be stopped in dry-run).
func stopAllPolecats(t *tmux.Tmux, townRoot string, rigNames []string, force bool, dryRun bool) int {
stopped := 0
// Load rigs config
rigsConfigPath := filepath.Join(townRoot, "mayor", "rigs.json")
rigsConfig, err := config.LoadRigsConfig(rigsConfigPath)
if err != nil {
rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)}
}
g := git.NewGit(townRoot)
rigMgr := rig.NewManager(townRoot, rigsConfig, g)
for _, rigName := range rigNames {
r, err := rigMgr.GetRig(rigName)
if err != nil {
continue
}
polecatMgr := polecat.NewSessionManager(t, r)
infos, err := polecatMgr.List()
if err != nil {
continue
}
for _, info := range infos {
if dryRun {
stopped++
fmt.Printf(" %s [%s] %s would stop\n", style.Dim.Render("○"), rigName, info.Polecat)
continue
}
err := polecatMgr.Stop(info.Polecat, force)
if err == nil {
stopped++
fmt.Printf(" %s [%s] %s stopped\n", style.SuccessPrefix, rigName, info.Polecat)
} else {
fmt.Printf(" %s [%s] %s: %s\n", style.ErrorPrefix, rigName, info.Polecat, err.Error())
}
}
}
return stopped
}
func printDownStatus(name string, ok bool, detail string) {
if downQuiet && ok {
return

View File

@@ -66,11 +66,15 @@ To stop Gas Town, use 'gt shutdown'.`,
var shutdownCmd = &cobra.Command{
Use: "shutdown",
GroupID: GroupServices,
Short: "Shutdown Gas Town",
Short: "Shutdown Gas Town with cleanup",
Long: `Shutdown Gas Town by stopping agents and cleaning up polecats.
By default, preserves crew sessions (your persistent workspaces).
Prompts for confirmation before stopping.
This is the "done for the day" command - it stops everything AND removes
polecat worktrees/branches. For a reversible pause, use 'gt down' instead.
Comparison:
gt down - Pause (stop processes, keep worktrees) - reversible
gt shutdown - Done (stop + cleanup worktrees) - permanent cleanup
After killing sessions, polecats are cleaned up:
- Worktrees are removed
@@ -78,9 +82,9 @@ After killing sessions, polecats are cleaned up:
- Polecats with uncommitted work are SKIPPED (protected)
Shutdown levels (progressively more aggressive):
(default) - Stop infrastructure (Mayor, Deacon, Witnesses, Refineries, Polecats)
(default) - Stop infrastructure + polecats + cleanup
--all - Also stop crew sessions
--polecats-only - Only stop polecats (leaves everything else running)
--polecats-only - Only stop polecats (leaves infrastructure running)
Use --force or --yes to skip confirmation prompt.
Use --graceful to allow agents time to save state before killing.

View File

@@ -23,15 +23,19 @@ var (
)
var stopCmd = &cobra.Command{
Use: "stop",
GroupID: GroupServices,
Short: "Emergency stop for sessions",
Long: `Emergency stop command for Gas Town sessions.
Use: "stop",
GroupID: GroupServices,
Short: "Emergency stop for sessions (deprecated: use 'gt down --polecats')",
Deprecated: "use 'gt down --polecats' instead",
Long: `DEPRECATED: This command is deprecated. Use 'gt down --polecats' instead.
Emergency stop command for Gas Town sessions.
Stops all running polecat sessions across rigs. Use for emergency shutdown
when you need to halt all agent activity immediately.
Examples:
gt down --polecats # Stop all polecats (preferred)
gt stop --all # Kill ALL sessions across all rigs
gt stop --rig wyvern # Kill all sessions in the wyvern rig
gt stop --all --graceful # Try graceful shutdown first`,
@@ -55,6 +59,10 @@ type StopResult struct {
}
func runStop(cmd *cobra.Command, args []string) error {
// Print deprecation warning
fmt.Printf("%s 'gt stop' is deprecated. Use 'gt down --polecats' instead.\n\n",
style.Warning.Render("Warning:"))
if !stopAll && stopRig == "" {
return fmt.Errorf("must specify --all or --rig <name>")
}