From 3e863c14313d01118c64e03a21697ccefdcf4806 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 28 Dec 2025 14:17:21 -0800 Subject: [PATCH] feat: Add gt polecat nuke command for full cleanup (gt-z99nh) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the nuclear cleanup option for post-merge polecat removal: - Kills Claude session (force mode) - Deletes git worktree (bypassing all safety checks) - Deletes polecat branch - Closes agent bead Supports --all for bulk nuke and --dry-run for preview. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/polecat.go | 197 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 4 deletions(-) diff --git a/internal/cmd/polecat.go b/internal/cmd/polecat.go index 82543e83..8dc10913 100644 --- a/internal/cmd/polecat.go +++ b/internal/cmd/polecat.go @@ -22,10 +22,12 @@ import ( // Polecat command flags var ( - polecatListJSON bool - polecatListAll bool - polecatForce bool - polecatRemoveAll bool + polecatListJSON bool + polecatListAll bool + polecatForce bool + polecatRemoveAll bool + polecatNukeAll bool + polecatNukeDryRun bool ) var polecatCmd = &cobra.Command{ @@ -241,6 +243,28 @@ Examples: RunE: runPolecatRecycle, } +var polecatNukeCmd = &cobra.Command{ + Use: "nuke /... | --all", + Short: "Completely destroy a polecat (session, worktree, branch, agent bead)", + Long: `Completely destroy a polecat and all its artifacts. + +This is the nuclear option for post-merge cleanup. It: + 1. Kills the Claude session (if running) + 2. Deletes the git worktree (bypassing all safety checks) + 3. Deletes the polecat branch + 4. Closes the agent bead (if exists) + +Use this after the Refinery has merged the polecat's work. + +Examples: + gt polecat nuke gastown/Toast + gt polecat nuke gastown/Toast gastown/Furiosa + gt polecat nuke gastown --all + gt polecat nuke gastown --all --dry-run`, + Args: cobra.MinimumNArgs(1), + RunE: runPolecatNuke, +} + var polecatGitStateCmd = &cobra.Command{ Use: "git-state /", Short: "Show git state for pre-kill verification", @@ -283,6 +307,10 @@ func init() { // GC flags polecatGCCmd.Flags().BoolVar(&polecatGCDryRun, "dry-run", false, "Show what would be deleted without deleting") + // Nuke flags + polecatNukeCmd.Flags().BoolVar(&polecatNukeAll, "all", false, "Nuke all polecats in the rig") + polecatNukeCmd.Flags().BoolVar(&polecatNukeDryRun, "dry-run", false, "Show what would be nuked without doing it") + // Add subcommands polecatCmd.AddCommand(polecatListCmd) polecatCmd.AddCommand(polecatAddCmd) @@ -296,6 +324,7 @@ func init() { polecatCmd.AddCommand(polecatGitStateCmd) polecatCmd.AddCommand(polecatGCCmd) polecatCmd.AddCommand(polecatRecycleCmd) + polecatCmd.AddCommand(polecatNukeCmd) rootCmd.AddCommand(polecatCmd) } @@ -1191,3 +1220,163 @@ func splitLines(s string) []string { } return lines } + +func runPolecatNuke(cmd *cobra.Command, args []string) error { + // Build list of polecats to nuke + type polecatToNuke struct { + rigName string + polecatName string + mgr *polecat.Manager + r *rig.Rig + } + var toNuke []polecatToNuke + + if polecatNukeAll { + // --all flag: first arg is just the rig name + rigName := args[0] + // Check if it looks like rig/polecat format + if _, _, err := parseAddress(rigName); err == nil { + return fmt.Errorf("with --all, provide just the rig name (e.g., 'gt polecat nuke gastown --all')") + } + + mgr, r, err := getPolecatManager(rigName) + if err != nil { + return err + } + + polecats, err := mgr.List() + if err != nil { + return fmt.Errorf("listing polecats: %w", err) + } + + if len(polecats) == 0 { + fmt.Println("No polecats to nuke.") + return nil + } + + for _, p := range polecats { + toNuke = append(toNuke, polecatToNuke{ + rigName: rigName, + polecatName: p.Name, + mgr: mgr, + r: r, + }) + } + } else { + // Multiple rig/polecat arguments + for _, arg := range args { + rigName, polecatName, err := parseAddress(arg) + if err != nil { + return fmt.Errorf("invalid address '%s': %w", arg, err) + } + + mgr, r, err := getPolecatManager(rigName) + if err != nil { + return err + } + + toNuke = append(toNuke, polecatToNuke{ + rigName: rigName, + polecatName: polecatName, + mgr: mgr, + r: r, + }) + } + } + + // Nuke each polecat + t := tmux.NewTmux() + var nukeErrors []string + nuked := 0 + + for _, p := range toNuke { + if polecatNukeDryRun { + fmt.Printf("Would nuke %s/%s:\n", p.rigName, p.polecatName) + fmt.Printf(" - Kill session: gt-%s-%s\n", p.rigName, p.polecatName) + fmt.Printf(" - Delete worktree: %s/polecats/%s\n", p.r.Path, p.polecatName) + fmt.Printf(" - Delete branch (if exists)\n") + fmt.Printf(" - Close agent bead: gt-polecat-%s-%s\n", p.rigName, p.polecatName) + continue + } + + fmt.Printf("Nuking %s/%s...\n", p.rigName, p.polecatName) + + // Step 1: Kill session (force mode - no graceful shutdown) + sessMgr := session.NewManager(t, p.r) + running, _ := sessMgr.IsRunning(p.polecatName) + if running { + if err := sessMgr.Stop(p.polecatName, true); err != nil { + fmt.Printf(" %s session kill failed: %v\n", style.Warning.Render("⚠"), err) + // Continue anyway - worktree removal will still work + } else { + fmt.Printf(" %s killed session\n", style.Success.Render("✓")) + } + } + + // Step 2: Get polecat info before deletion (for branch name) + polecatInfo, err := p.mgr.Get(p.polecatName) + var branchToDelete string + if err == nil && polecatInfo != nil { + branchToDelete = polecatInfo.Branch + } + + // Step 3: Delete worktree (nuclear mode - bypass all safety checks) + if err := p.mgr.RemoveWithOptions(p.polecatName, true, true); err != nil { + if errors.Is(err, polecat.ErrPolecatNotFound) { + fmt.Printf(" %s worktree already gone\n", style.Dim.Render("○")) + } else { + nukeErrors = append(nukeErrors, fmt.Sprintf("%s/%s: worktree removal failed: %v", p.rigName, p.polecatName, err)) + continue + } + } else { + fmt.Printf(" %s deleted worktree\n", style.Success.Render("✓")) + } + + // Step 4: Delete branch (if we know it) + if branchToDelete != "" { + repoGit := git.NewGit(filepath.Join(p.r.Path, "mayor", "rig")) + if err := repoGit.DeleteBranch(branchToDelete, true); err != nil { + // Non-fatal - branch might already be gone + fmt.Printf(" %s branch delete: %v\n", style.Dim.Render("○"), err) + } else { + fmt.Printf(" %s deleted branch %s\n", style.Success.Render("✓"), branchToDelete) + } + } + + // Step 5: Close agent bead (if exists) + agentBeadID := fmt.Sprintf("gt-polecat-%s-%s", p.rigName, p.polecatName) + closeCmd := exec.Command("bd", "close", agentBeadID, "--reason=nuked") + closeCmd.Dir = filepath.Join(p.r.Path, "mayor", "rig") + if err := closeCmd.Run(); err != nil { + // Non-fatal - agent bead might not exist + fmt.Printf(" %s agent bead not found or already closed\n", style.Dim.Render("○")) + } else { + fmt.Printf(" %s closed agent bead %s\n", style.Success.Render("✓"), agentBeadID) + } + + nuked++ + } + + // Report results + if polecatNukeDryRun { + fmt.Printf("\n%s Would nuke %d polecat(s).\n", style.Info.Render("ℹ"), len(toNuke)) + return nil + } + + if len(nukeErrors) > 0 { + fmt.Printf("\n%s Some nukes failed:\n", style.Warning.Render("Warning:")) + for _, e := range nukeErrors { + fmt.Printf(" - %s\n", e) + } + } + + if nuked > 0 { + fmt.Printf("\n%s Nuked %d polecat(s).\n", style.SuccessPrefix, nuked) + } + + if len(nukeErrors) > 0 { + return fmt.Errorf("%d nuke(s) failed", len(nukeErrors)) + } + + return nil +}