feat: Add gt polecat nuke command for full cleanup (gt-z99nh)

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 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-28 14:17:21 -08:00
parent bed0dacf1f
commit 3e863c1431

View File

@@ -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 <rig>/<polecat>... | <rig> --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 <rig>/<polecat>",
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
}